diff --git a/samples/sqlservices/src/controllers/mainController.ts b/samples/sqlservices/src/controllers/mainController.ts index 9d2dec1232..c41cf58c01 100644 --- a/samples/sqlservices/src/controllers/mainController.ts +++ b/samples/sqlservices/src/controllers/mainController.ts @@ -301,6 +301,7 @@ export default class MainController implements vscode.Disposable { page1.registerContent(async (view) => { await this.getTabContent(view, customButton1, customButton2, 800); }); + /* wizard.registerOperation({ displayName: 'test task', description: 'task description', @@ -311,7 +312,7 @@ export default class MainController implements vscode.Disposable { setTimeout(() => { op.updateStatus(sqlops.TaskStatus.Succeeded); }, 5000); - }); + });*/ wizard.pages = [page1, page2]; wizard.open(); } @@ -354,15 +355,32 @@ export default class MainController implements vscode.Disposable { webview2.message = count; }); - let flexModel = view.modelBuilder.flexContainer() - .withLayout({ - flexFlow: 'column', - alignItems: 'flex-start', - height: 500 - }).withItems([ - webview1, webview2 - ], { flex: '1 1 50%' }) + let editor1 = view.modelBuilder.editor() + .withProperties({ + content: 'select * from sys.tables' + }) .component(); + + let editor2 = view.modelBuilder.editor() + .withProperties({ + content: 'print("Hello World !")', + languageMode: 'python' + }) + .component(); + + let flexModel = view.modelBuilder.flexContainer().component(); + flexModel.addItem(editor1, { flex: '1' }); + flexModel.addItem(editor2, { flex: '1' }); + flexModel.setLayout({ + flexFlow: 'column', + alignItems: 'stretch', + height: '100%' + }); + + view.onClosed((params) => { + vscode.window.showInformationMessage('editor1: language: ' + editor1.languageMode + ' Content1: ' + editor1.content); + vscode.window.showInformationMessage('editor2: language: ' + editor2.languageMode + ' Content2: ' + editor2.content); + }); await view.initializeModel(flexModel); }); editor.openEditor(); diff --git a/src/sql/parts/modelComponents/components.contribution.ts b/src/sql/parts/modelComponents/components.contribution.ts index fc91037971..a8d26c86a4 100644 --- a/src/sql/parts/modelComponents/components.contribution.ts +++ b/src/sql/parts/modelComponents/components.contribution.ts @@ -20,6 +20,7 @@ import TableComponent from './table.component'; import TextComponent from './text.component'; import LoadingComponent from './loadingComponent.component'; import FileBrowserTreeComponent from './fileBrowserTree.component'; +import EditorComponent from './editor.component'; import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -74,3 +75,6 @@ registerComponentType(LOADING_COMPONENT, ModelComponentTypes.LoadingComponent, L export const FILEBROWSERTREE_COMPONENT = 'filebrowsertree-component'; registerComponentType(FILEBROWSERTREE_COMPONENT, ModelComponentTypes.FileBrowserTree, FileBrowserTreeComponent); + +export const EDITOR_COMPONENT = 'editor-component'; +registerComponentType(EDITOR_COMPONENT, ModelComponentTypes.Editor, EditorComponent); diff --git a/src/sql/parts/modelComponents/editor.component.ts b/src/sql/parts/modelComponents/editor.component.ts new file mode 100644 index 0000000000..74c8c4650b --- /dev/null +++ b/src/sql/parts/modelComponents/editor.component.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./editor'; +import { + Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver, + ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList +} from '@angular/core'; + +import * as sqlops from 'sqlops'; +import * as DOM from 'vs/base/browser/dom'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITextModel } from 'vs/editor/common/model'; +import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; +import URI from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IModelService } from 'vs/editor/common/services/modelService'; + +import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore } from 'sql/parts/modelComponents/interfaces'; +import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor'; + +@Component({ + template: '', + selector: 'modelview-editor-component' +}) +export default class EditorComponent extends ComponentBase implements IComponent, OnDestroy { + @Input() descriptor: IComponentDescriptor; + @Input() modelStore: IModelStore; + private _editor: QueryTextEditor; + private _editorInput: UntitledEditorInput; + private _editorModel: ITextModel; + private _renderedContent: string; + private _langaugeMode: string; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, + @Inject(forwardRef(() => ElementRef)) private _el: ElementRef, + @Inject(IInstantiationService) private _instantiationService: IInstantiationService, + @Inject(IModelService) private _modelService: IModelService, + @Inject(IModeService) private _modeService: IModeService + ) { + super(changeRef); + } + + ngOnInit(): void { + this.baseInit(); + this._createEditor(); + this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => { + this.layout(); + })); + } + + private _createEditor(): void { + this._editor = this._instantiationService.createInstance(QueryTextEditor); + this._editor.create(this._el.nativeElement); + this._editor.setVisible(true); + this._editorInput = this._instantiationService.createInstance(UntitledEditorInput, URI.from({ scheme: Schemas.untitled, path: `${this.descriptor.type}-${this.descriptor.id}` }), false, 'sql', '', ''); + this._editor.setInput(this._editorInput, undefined); + this._editorInput.resolve().then(model => this._editorModel = model.textEditorModel); + + this._register(this._editor); + this._register(this._editorInput); + this._register(this._editorModel.onDidChangeContent(e => { + this.content = this._editorModel.getValue(); + })); + } + + ngOnDestroy(): void { + this.baseDestroy(); + } + + /// IComponent implementation + + public layout(): void { + let width: number = this.convertSizeToNumber(this.width); + let height: number = this.convertSizeToNumber(this.height); + this._editor.layout(new DOM.Dimension( + width && width > 0 ? width : DOM.getContentWidth(this._el.nativeElement), + height && height > 0 ? height : DOM.getContentHeight(this._el.nativeElement))); + } + + /// Editor Functions + private updateModel() { + if (this._editorModel) { + this._renderedContent = this.content; + this._modelService.updateModel(this._editorModel, this._renderedContent); + } + } + + private updateLanguageMode() { + if (this._editorModel && this._editor) { + this._langaugeMode = this.languageMode; + this._modeService.getOrCreateMode(this._langaugeMode).then((modeValue) => { + this._modelService.setMode(this._editorModel, modeValue); + }); + } + } + + /// IComponent implementation + + public setLayout(layout: any): void { + // TODO allow configuring the look and feel + this.layout(); + } + + public setProperties(properties: { [key: string]: any; }): void { + super.setProperties(properties); + if (this.content !== this._renderedContent) { + this.updateModel(); + } + if (this.languageMode !== this._langaugeMode) { + this.updateLanguageMode(); + } + } + + // CSS-bound properties + public get content(): string { + return this.getPropertyOrDefault((props) => props.content, undefined); + } + + public set content(newValue: string) { + this.setPropertyFromUI((properties, content) => { properties.content = content; }, newValue); + } + + public get languageMode(): string { + return this.getPropertyOrDefault((props) => props.languageMode, undefined); + } + + public set languageMode(newValue: string) { + this.setPropertyFromUI((properties, languageMode) => { properties.languageMode = languageMode; }, newValue); + } +} diff --git a/src/sql/parts/modelComponents/editor.css b/src/sql/parts/modelComponents/editor.css new file mode 100644 index 0000000000..f5831f94d1 --- /dev/null +++ b/src/sql/parts/modelComponents/editor.css @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + modelview-editor-component { + height: 100%; + width : 100%; + display: block; +} \ No newline at end of file diff --git a/src/sql/parts/modelComponents/queryTextEditor.ts b/src/sql/parts/modelComponents/queryTextEditor.ts new file mode 100644 index 0000000000..b9e006816c --- /dev/null +++ b/src/sql/parts/modelComponents/queryTextEditor.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; +import * as editorCommon from 'vs/editor/common/editorCommon'; + +import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { EditorOptions } from 'vs/workbench/common/editor'; +import { CodeEditor } from 'vs/editor/browser/codeEditor'; +import { IEditorContributionCtor } from 'vs/editor/browser/editorExtensions'; +import { FoldingController } from 'vs/editor/contrib/folding/folding'; + +class QueryCodeEditor extends CodeEditor { + + protected _getContributions(): IEditorContributionCtor[] { + let contributions = super._getContributions(); + let skipContributions = [FoldingController.prototype]; + contributions = contributions.filter(c => skipContributions.indexOf(c.prototype) === -1); + return contributions; + } + +} + +/** + * Extension of TextResourceEditor that is always readonly rather than only with non UntitledInputs + */ +export class QueryTextEditor extends BaseTextEditor { + + public static ID = 'modelview.editors.textEditor'; + private _dimension: DOM.Dimension; + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IInstantiationService instantiationService: IInstantiationService, + @IStorageService storageService: IStorageService, + @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, + @IThemeService themeService: IThemeService, + @IModeService modeService: IModeService, + @ITextFileService textFileService: ITextFileService, + @IEditorGroupService editorGroupService: IEditorGroupService + + ) { + super(QueryTextEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorGroupService); + } + + public createEditorControl(parent: HTMLElement, configuration: IEditorOptions): editorCommon.IEditor { + return this.instantiationService.createInstance(QueryCodeEditor, parent, configuration); + } + + protected getConfigurationOverrides(): IEditorOptions { + const options = super.getConfigurationOverrides(); + if (this.input) { + options.inDiffEditor = true; + options.scrollBeyondLastLine = false; + options.folding = false; + options.renderWhitespace = 'none'; + options.wordWrap = 'on'; + options.renderIndentGuides = false; + options.rulers = []; + options.glyphMargin = true; + options.minimap = { + enabled: false + }; + } + return options; + } + + setInput(input: UntitledEditorInput, options: EditorOptions): TPromise { + return super.setInput(input, options) + .then(() => this.input.resolve() + .then(editorModel => editorModel.load()) + .then(editorModel => this.getControl().setModel((editorModel).textEditorModel))); + } + + protected getAriaLabel(): string { + return nls.localize('queryTextEditorAriaLabel', 'modelview code editor for view model.'); + } + + public layout(dimension?: DOM.Dimension){ + if (dimension) { + this._dimension = dimension; + } + this.getControl().layout(dimension); + } + + public setWidth(width: number) { + if (this._dimension) { + this._dimension.width = width; + this.layout(); + } + } + + public setHeight(height: number) { + if (this._dimension) { + this._dimension.height = height; + this.layout(); + } + } +} diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 66aa18905f..6697a7ae35 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -23,6 +23,7 @@ declare module 'sqlops' { checkBox(): ComponentBuilder; radioButton(): ComponentBuilder; webView(): ComponentBuilder; + editor(): ComponentBuilder; text(): ComponentBuilder; button(): ComponentBuilder; dropDown(): ComponentBuilder; @@ -435,6 +436,20 @@ declare module 'sqlops' { html?: string; } + /** + * Editor properties for the editor component + */ + export interface EditorProperties { + /** + * The content inside the text editor + */ + content?: string; + /** + * The languge mode for this text editor. The language mode is SQL by default. + */ + languageMode?: string + } + export interface ButtonProperties extends ComponentProperties, ComponentWithIcon { label?: string; isFile?: boolean; @@ -503,6 +518,20 @@ declare module 'sqlops' { onMessage: vscode.Event; } + /** + * Editor component for displaying the text code editor + */ + export interface EditorComponent extends Component { + /** + * The content inside the text editor + */ + content: string; + /** + * The languge mode for this text editor. The language mode is SQL by default. + */ + languageMode: string; + } + export interface ButtonComponent extends Component { label: string; iconPath: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri }; diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 3b149a2f13..4b25964431 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -146,7 +146,8 @@ export enum ModelComponentTypes { Group, Toolbar, LoadingComponent, - FileBrowserTree + FileBrowserTree, + Editor } export interface IComponentShape { diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index d2f79f3ac3..3340204822 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -101,6 +101,13 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { return builder; } + editor(): sqlops.ComponentBuilder { + let id = this.getNextComponentId(); + let builder: ComponentBuilderImpl = this.getComponentBuilder(new EditorWrapper(this._proxy, this._handle, id), id); + this._componentBuilders.set(id, builder); + return builder; + } + button(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new ButtonWrapper(this._proxy, this._handle, id), id); @@ -740,6 +747,28 @@ class WebViewWrapper extends ComponentWrapper implements sqlops.WebViewComponent } } +class EditorWrapper extends ComponentWrapper implements sqlops.EditorComponent { + + constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { + super(proxy, handle, ModelComponentTypes.Editor, id); + this.properties = {}; + } + + public get content(): string { + return this.properties['content']; + } + public set content(v: string) { + this.setProperty('content', v); + } + + public get languageMode(): string { + return this.properties['languageMode']; + } + public set languageMode(v: string) { + this.setProperty('languageMode', v); + } +} + class RadioButtonWrapper extends ComponentWrapper implements sqlops.RadioButtonComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {