/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import URI from 'vs/base/common/uri'; import Event, {Emitter} from 'vs/base/common/event'; import {IDisposable, disposeAll} from 'vs/base/common/lifecycle'; import {TPromise} from 'vs/base/common/winjs.base'; import {Remotable, IThreadService} from 'vs/platform/thread/common/thread'; import {ExtHostModelService, ExtHostDocumentData} from 'vs/workbench/api/node/extHostDocuments'; import {Selection, Range, Position, EditorOptions} from './extHostTypes'; import {ISingleEditOperation, ISelection, IRange, IEditor, EditorType, ICommonCodeEditor, ICommonDiffEditor, IDecorationRenderOptions, IRangeWithMessage} from 'vs/editor/common/editorCommon'; import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; import {Position as EditorPosition} from 'vs/platform/editor/common/editor'; import {IModelService} from 'vs/editor/common/services/modelService'; import {MainThreadEditorsTracker, TextEditorRevealType, MainThreadTextEditor, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration} from 'vs/workbench/api/node/mainThreadEditors'; import * as TypeConverters from './extHostTypeConverters'; import {TextDocument, TextEditorSelectionChangeEvent, TextEditorOptionsChangeEvent, TextEditorOptions, TextEditorViewColumnChangeEvent, ViewColumn} from 'vscode'; import {EventType} from 'vs/workbench/common/events'; import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; import {IEventService} from 'vs/platform/event/common/event'; import {equals as arrayEquals} from 'vs/base/common/arrays'; import {equals as objectEquals} from 'vs/base/common/objects'; export interface ITextEditorAddData { id: string; document: URI; options: IResolvedTextEditorConfiguration; selections: ISelection[]; editorPosition: EditorPosition; } export interface ITextEditorPositionData { [id: string]: EditorPosition; } @Remotable.ExtHostContext('ExtHostEditors') export class ExtHostEditors { public onDidChangeTextEditorSelection: Event; private _onDidChangeTextEditorSelection: Emitter; public onDidChangeTextEditorOptions: Event; private _onDidChangeTextEditorOptions: Emitter; public onDidChangeTextEditorViewColumn: Event; private _onDidChangeTextEditorViewColumn: Emitter; private _editors: { [id: string]: ExtHostTextEditor }; private _proxy: MainThreadEditors; private _onDidChangeActiveTextEditor: Emitter; private _modelService: ExtHostModelService; private _activeEditorId: string; private _visibleEditorIds: string[]; constructor( @IThreadService threadService: IThreadService ) { this._onDidChangeTextEditorSelection = new Emitter(); this.onDidChangeTextEditorSelection = this._onDidChangeTextEditorSelection.event; this._onDidChangeTextEditorOptions = new Emitter(); this.onDidChangeTextEditorOptions = this._onDidChangeTextEditorOptions.event; this._onDidChangeTextEditorViewColumn = new Emitter(); this.onDidChangeTextEditorViewColumn = this._onDidChangeTextEditorViewColumn.event; this._modelService = threadService.getRemotable(ExtHostModelService); this._proxy = threadService.getRemotable(MainThreadEditors); this._onDidChangeActiveTextEditor = new Emitter(); this._editors = Object.create(null); this._visibleEditorIds = []; } getActiveTextEditor(): vscode.TextEditor { return this._activeEditorId && this._editors[this._activeEditorId]; } getVisibleTextEditors(): vscode.TextEditor[] { return this._visibleEditorIds.map(id => this._editors[id]); } get onDidChangeActiveTextEditor(): Event { return this._onDidChangeActiveTextEditor && this._onDidChangeActiveTextEditor.event; } showTextDocument(document: TextDocument, column: ViewColumn, preserveFocus: boolean): TPromise { return this._proxy._tryShowTextDocument( document.uri, TypeConverters.fromViewColumn(column), preserveFocus).then(id => { let editor = this._editors[id]; if (editor) { return editor; } else { throw new Error('Failed to create editor with id: ' + id); } }); } createTextEditorDecorationType(options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { return new TextEditorDecorationType(this._proxy, options); } // --- called from main thread _acceptTextEditorAdd(data: ITextEditorAddData): void { let document = this._modelService.getDocumentData(data.document); let newEditor = new ExtHostTextEditor(this._proxy, data.id, document, data.selections.map(TypeConverters.toSelection), data.options, TypeConverters.toViewColumn(data.editorPosition)); this._editors[data.id] = newEditor; } _acceptOptionsChanged(id: string, opts: IResolvedTextEditorConfiguration): void { let editor = this._editors[id]; editor._acceptOptions(opts); this._onDidChangeTextEditorOptions.fire({ textEditor: editor, options: opts }); } _acceptSelectionsChanged(id: string, _selections: ISelection[]): void { let selections = _selections.map(TypeConverters.toSelection); let editor = this._editors[id]; editor._acceptSelections(selections); this._onDidChangeTextEditorSelection.fire({ textEditor: editor, selections: selections }); } _acceptActiveEditorAndVisibleEditors(id: string, visibleIds: string[]): void { this._visibleEditorIds = visibleIds; if (this._activeEditorId === id) { // nothing to do return; } this._activeEditorId = id; this._onDidChangeActiveTextEditor.fire(this.getActiveTextEditor()); } _acceptEditorPositionData(data: ITextEditorPositionData): void { for (let id in data) { let textEditor = this._editors[id]; let viewColumn = TypeConverters.toViewColumn(data[id]); if (textEditor.viewColumn !== viewColumn) { textEditor._acceptViewColumn(viewColumn); this._onDidChangeTextEditorViewColumn.fire({ textEditor, viewColumn }); } } } _acceptTextEditorRemove(id: string): void { // make sure the removed editor is not visible let newVisibleEditors = this._visibleEditorIds.filter(visibleEditorId => visibleEditorId !== id); if (this._activeEditorId === id) { // removing the current active editor this._acceptActiveEditorAndVisibleEditors(undefined, newVisibleEditors); } else { this._acceptActiveEditorAndVisibleEditors(this._activeEditorId, newVisibleEditors); } let editor = this._editors[id]; editor.dispose(); delete this._editors[id]; } } class TextEditorDecorationType implements vscode.TextEditorDecorationType { private static LAST_ID: number = 0; private _proxy: MainThreadEditors; public key: string; constructor(proxy: MainThreadEditors, options: vscode.DecorationRenderOptions) { this.key = 'TextEditorDecorationType' + (++TextEditorDecorationType.LAST_ID); this._proxy = proxy; this._proxy._registerTextEditorDecorationType(this.key, options); } public dispose(): void { this._proxy._removeTextEditorDecorationType(this.key); } } export interface ITextEditOperation { range: Range; text: string; forceMoveMarkers: boolean; } export interface IEditData { documentVersionId: number; edits: ITextEditOperation[]; } export class TextEditorEdit { private _documentVersionId: number; private _collectedEdits: ITextEditOperation[]; constructor(document: vscode.TextDocument) { this._documentVersionId = document.version; this._collectedEdits = []; } finalize(): IEditData { return { documentVersionId: this._documentVersionId, edits: this._collectedEdits }; } replace(location: Position | Range | Selection, value: string): void { let range: Range = null; if (location instanceof Position) { range = new Range(location, location); } else if (location instanceof Range) { range = location; } else if (location instanceof Selection) { range = new Range(location.start, location.end); } else { throw new Error('Unrecognized location'); } this._collectedEdits.push({ range: range, text: value, forceMoveMarkers: false }); } insert(location: Position, value: string): void { this._collectedEdits.push({ range: new Range(location, location), text: value, forceMoveMarkers: true }); } delete(location: Range | Selection): void { let range: Range = null; if (location instanceof Range) { range = location; } else if (location instanceof Selection) { range = new Range(location.start, location.end); } else { throw new Error('Unrecognized location'); } this._collectedEdits.push({ range: range, text: null, forceMoveMarkers: true }); } } function readonly(name: string, alt?: string) { let message = `The property '${name}' is readonly.`; if (alt) { message += ` Use '${alt}' instead.`; } return new Error(message); } function illegalArg(name: string) { return new Error(`illgeal argument '${name}'`); } function deprecated(name: string, message: string = 'Refer to the documentation for further details.') { return (target: Object, key: string, descriptor: TypedPropertyDescriptor) => { const originalMethod = descriptor.value; descriptor.value = function(...args: any[]) { console.warn(`[Deprecation Warning] method '${name}' is deprecated and should no longer be used. ${message}`); return originalMethod.apply(this, args); }; return descriptor; }; } class ExtHostTextEditor implements vscode.TextEditor { private _proxy: MainThreadEditors; private _id: string; private _documentData: ExtHostDocumentData; private _selections: Selection[]; private _options: TextEditorOptions; private _viewColumn: vscode.ViewColumn; constructor(proxy: MainThreadEditors, id: string, document: ExtHostDocumentData, selections: Selection[], options: EditorOptions, viewColumn: vscode.ViewColumn) { this._proxy = proxy; this._id = id; this._documentData = document; this._selections = selections; this._options = options; this._viewColumn = viewColumn; } dispose() { this._documentData = null; } @deprecated('TextEditor.show') show(column: vscode.ViewColumn) { this._proxy._tryShowEditor(this._id, TypeConverters.fromViewColumn(column)); } @deprecated('TextEditor.hide') hide() { this._proxy._tryHideEditor(this._id); } // ---- the document get document(): vscode.TextDocument { return this._documentData.document; } set document(value) { throw readonly('document'); } // ---- options get options(): TextEditorOptions { return this._options; } set options(value: TextEditorOptions) { this._options = value; this._runOnProxy(() => { return this._proxy._trySetOptions(this._id, this._options); }, true); } _acceptOptions(options: EditorOptions): void { this._options = options; } // ---- view column get viewColumn(): vscode.ViewColumn { return this._viewColumn; } set viewColumn(value) { throw readonly('viewColumn'); } _acceptViewColumn(value: vscode.ViewColumn) { this._viewColumn = value; } // ---- selections get selection(): Selection { return this._selections && this._selections[0]; } set selection(value: Selection) { if (!(value instanceof Selection)) { throw illegalArg('selection'); } this._selections = [value]; this._trySetSelection(true); } get selections(): Selection[] { return this._selections; } set selections(value: Selection[]) { if (!Array.isArray(value) || value.some(a => !(a instanceof Selection))) { throw illegalArg('selections'); } this._selections = value; this._trySetSelection(true); } setDecorations(decorationType: vscode.TextEditorDecorationType, ranges: Range[] | vscode.DecorationOptions[]): void { this._runOnProxy( () => this._proxy._trySetDecorations( this._id, decorationType.key, TypeConverters.fromRangeOrRangeWithMessage(ranges) ), true ); } revealRange(range: Range, revealType: vscode.TextEditorRevealType): void { this._runOnProxy( () => this._proxy._tryRevealRange( this._id, TypeConverters.fromRange(range), (revealType) || TextEditorRevealType.Default ), true ); } private _trySetSelection(silent: boolean): TPromise { let selection = this._selections.map(TypeConverters.fromSelection); return this._runOnProxy(() => this._proxy._trySetSelections(this._id, selection), silent); } _acceptSelections(selections: Selection[]): void { this._selections = selections; } // ---- editing edit(callback: (edit: TextEditorEdit) => void): Thenable { let edit = new TextEditorEdit(this._documentData.document); callback(edit); return this._applyEdit(edit); } _applyEdit(editBuilder: TextEditorEdit): TPromise { let editData = editBuilder.finalize(); // prepare data for serialization let edits: ISingleEditOperation[] = editData.edits.map((edit) => { return { range: TypeConverters.fromRange(edit.range), text: edit.text, forceMoveMarkers: edit.forceMoveMarkers }; }); return this._proxy._tryApplyEdits(this._id, editData.documentVersionId, edits); } // ---- util private _runOnProxy(callback: () => TPromise, silent: boolean): TPromise { return callback().then(() => this, err => { if (!silent) { return TPromise.wrapError(silent); } console.warn(err); }); } } @Remotable.MainContext('MainThreadEditors') export class MainThreadEditors { private _proxy: ExtHostEditors; private _workbenchEditorService: IWorkbenchEditorService; private _telemetryService: ITelemetryService; private _editorTracker: MainThreadEditorsTracker; private _toDispose: IDisposable[]; private _textEditorsListenersMap: { [editorId: string]: IDisposable[]; }; private _textEditorsMap: { [editorId: string]: MainThreadTextEditor; }; private _activeTextEditor: string; private _visibleEditors: string[]; private _editorPositionData: ITextEditorPositionData; constructor( @IThreadService threadService: IThreadService, @IWorkbenchEditorService workbenchEditorService: IWorkbenchEditorService, @ITelemetryService telemetryService: ITelemetryService, @ICodeEditorService editorService: ICodeEditorService, @IEventService eventService: IEventService, @IModelService modelService: IModelService ) { this._proxy = threadService.getRemotable(ExtHostEditors); this._workbenchEditorService = workbenchEditorService; this._telemetryService = telemetryService; this._toDispose = []; this._textEditorsListenersMap = Object.create(null); this._textEditorsMap = Object.create(null); this._activeTextEditor = null; this._visibleEditors = []; this._editorPositionData = null; this._editorTracker = new MainThreadEditorsTracker(editorService, modelService); this._toDispose.push(this._editorTracker); this._toDispose.push(this._editorTracker.onTextEditorAdd((textEditor) => this._onTextEditorAdd(textEditor))); this._toDispose.push(this._editorTracker.onTextEditorRemove((textEditor) => this._onTextEditorRemove(textEditor))); this._toDispose.push(this._editorTracker.onDidUpdateTextEditors(() => this._updateActiveAndVisibleTextEditors())); this._toDispose.push(this._editorTracker.onChangedFocusedTextEditor((focusedTextEditorId) => this._updateActiveAndVisibleTextEditors())); this._toDispose.push(eventService.addListener2(EventType.EDITOR_INPUT_CHANGED, () => this._updateActiveAndVisibleTextEditors())); this._toDispose.push(eventService.addListener2(EventType.EDITOR_POSITION_CHANGED, () => this._updateActiveAndVisibleTextEditors())); } public dispose(): void { Object.keys(this._textEditorsListenersMap).forEach((editorId) => { disposeAll(this._textEditorsListenersMap[editorId]); }); this._textEditorsListenersMap = Object.create(null); this._toDispose = disposeAll(this._toDispose); } private _onTextEditorAdd(textEditor: MainThreadTextEditor): void { let id = textEditor.getId(); let toDispose: IDisposable[] = []; toDispose.push(textEditor.onConfigurationChanged((opts) => { this._proxy._acceptOptionsChanged(id, opts); })); toDispose.push(textEditor.onSelectionChanged((selection) => { this._proxy._acceptSelectionsChanged(id, selection); })); this._proxy._acceptTextEditorAdd({ id: id, document: textEditor.getModel().getAssociatedResource(), options: textEditor.getConfiguration(), selections: textEditor.getSelections(), editorPosition: this._findEditorPosition(textEditor) }); this._textEditorsListenersMap[id] = toDispose; this._textEditorsMap[id] = textEditor; } private _onTextEditorRemove(textEditor: MainThreadTextEditor): void { let id = textEditor.getId(); disposeAll(this._textEditorsListenersMap[id]); delete this._textEditorsListenersMap[id]; delete this._textEditorsMap[id]; this._proxy._acceptTextEditorRemove(id); } private _updateActiveAndVisibleTextEditors(): void { // active and visible editors let visibleEditors = this._editorTracker.getVisibleTextEditorIds(); let activeEditor = this._findActiveTextEditorId(); if (activeEditor !== this._activeTextEditor || !arrayEquals(this._visibleEditors, visibleEditors, (a, b) => a === b)) { this._activeTextEditor = activeEditor; this._visibleEditors = visibleEditors; this._proxy._acceptActiveEditorAndVisibleEditors(this._activeTextEditor, this._visibleEditors); } // editor columns let editorPositionData = this._getTextEditorPositionData(); if (!objectEquals(this._editorPositionData, editorPositionData)) { this._editorPositionData = editorPositionData; this._proxy._acceptEditorPositionData(this._editorPositionData); } } private _findActiveTextEditorId(): string { let focusedTextEditorId = this._editorTracker.getFocusedTextEditorId(); if (focusedTextEditorId) { return focusedTextEditorId; } let activeEditor = this._workbenchEditorService.getActiveEditor(); if (!activeEditor) { return null; } let editor = activeEditor.getControl(); // Substitute for (editor instanceof ICodeEditor) if (!editor || typeof editor.getEditorType !== 'function') { // Not a text editor... return null; } if (editor.getEditorType() === EditorType.ICodeEditor) { return this._editorTracker.findTextEditorIdFor(editor); } // Must be a diff editor => use the modified side return this._editorTracker.findTextEditorIdFor((editor).getModifiedEditor()); } private _findEditorPosition(editor: MainThreadTextEditor): EditorPosition { for (let workbenchEditor of this._workbenchEditorService.getVisibleEditors()) { if (editor.matches(workbenchEditor)) { return workbenchEditor.position; } } } private _getTextEditorPositionData(): ITextEditorPositionData { let result: ITextEditorPositionData = Object.create(null); for (let workbenchEditor of this._workbenchEditorService.getVisibleEditors()) { let editor = workbenchEditor.getControl(); // Substitute for (editor instanceof ICodeEditor) if (!editor || typeof editor.getEditorType !== 'function') { // Not a text editor... continue; } if (editor.getEditorType() === EditorType.ICodeEditor) { let id = this._editorTracker.findTextEditorIdFor(editor); if (id) { result[id] = workbenchEditor.position; } } } return result; } // --- from extension host process _tryShowTextDocument(resource: URI, position: EditorPosition, preserveFocus: boolean): TPromise { const input = { resource, options: { preserveFocus } }; return this._workbenchEditorService.openEditor(input, position).then(editor => { return new TPromise(c => { // not very nice but the way it is: changes to the editor state aren't // send to the ext host as they happen but stuff is delayed a little. in // order to provide the real editor on #openTextEditor we need to sync on // that update let subscription: IDisposable; let handle: number; function contd() { subscription.dispose(); clearTimeout(handle); c(undefined); } subscription = this._editorTracker.onDidUpdateTextEditors(() => { contd(); }); handle = setTimeout(() => { contd(); }, 1000); }).then(() => { // find the editor we have just opened and return the // id we have assigned to it. for (let id in this._textEditorsMap) { if (this._textEditorsMap[id].matches(editor)) { return id; } } }); }); } _tryShowEditor(id: string, position: EditorPosition): TPromise { // check how often this is used this._telemetryService.publicLog('api.deprecated', { function: 'TextEditor.show' }); let mainThreadEditor = this._textEditorsMap[id]; if (mainThreadEditor) { let model = mainThreadEditor.getModel(); return this._workbenchEditorService.openEditor({ resource: model.getAssociatedResource(), options: { preserveFocus: false } }, position).then(() => { return; }); } } _tryHideEditor(id: string): TPromise { // check how often this is used this._telemetryService.publicLog('api.deprecated', { function: 'TextEditor.hide' }); let mainThreadEditor = this._textEditorsMap[id]; if (mainThreadEditor) { let editors = this._workbenchEditorService.getVisibleEditors(); for (let editor of editors) { if (mainThreadEditor.matches(editor)) { return this._workbenchEditorService.closeEditor(editor).then(() => { return; }); } } } } _trySetSelections(id: string, selections: ISelection[]): TPromise { if (!this._textEditorsMap[id]) { return TPromise.wrapError('TextEditor disposed'); } this._textEditorsMap[id].setSelections(selections); return TPromise.as(null); } _trySetDecorations(id: string, key: string, ranges: IRangeWithMessage[]): TPromise { if (!this._textEditorsMap[id]) { return TPromise.wrapError('TextEditor disposed'); } this._textEditorsMap[id].setDecorations(key, ranges); return TPromise.as(null); } _tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise { if (!this._textEditorsMap[id]) { return TPromise.wrapError('TextEditor disposed'); } this._textEditorsMap[id].revealRange(range, revealType); } _trySetOptions(id: string, options: ITextEditorConfigurationUpdate): TPromise { if (!this._textEditorsMap[id]) { return TPromise.wrapError('TextEditor disposed'); } this._textEditorsMap[id].setConfiguration(options); return TPromise.as(null); } _tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[]): TPromise { if (!this._textEditorsMap[id]) { return TPromise.wrapError('TextEditor disposed'); } return TPromise.as(this._textEditorsMap[id].applyEdits(modelVersionId, edits)); } _registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void { this._editorTracker.registerTextEditorDecorationType(key, options); } _removeTextEditorDecorationType(key: string): void { this._editorTracker.removeTextEditorDecorationType(key); } }