diff --git a/extensions/vscode-api-tests/src/editor.test.ts b/extensions/vscode-api-tests/src/editor.test.ts index 0e06cc292eb73fb57dab42b50861a7b8253c1699..0631d2b4b038db28cd26702eb37ea131fb6c4a17 100644 --- a/extensions/vscode-api-tests/src/editor.test.ts +++ b/extensions/vscode-api-tests/src/editor.test.ts @@ -6,7 +6,8 @@ 'use strict'; import * as assert from 'assert'; -import { workspace, window, Position, Range, commands, TextEditor, TextDocument, TextEditorCursorStyle, TextEditorLineNumbersStyle, SnippetString, Selection } from 'vscode'; +import { join } from 'path'; +import { workspace, window, Position, Range, commands, TextEditor, TextDocument, TextEditorCursorStyle, TextEditorLineNumbersStyle, SnippetString, Selection, ViewColumn } from 'vscode'; import { createRandomFile, deleteFile, cleanUp } from './utils'; suite('editor tests', () => { @@ -109,6 +110,66 @@ suite('editor tests', () => { }); }); + test('issue #20867: vscode.window.visibleTextEditors returns closed document 1/2', () => { + + return withRandomFileEditor('Hello world!', editor => { + + const p = new Promise((resolve, reject) => { + const sub = workspace.onDidCloseTextDocument(doc => { + try { + sub.dispose(); + assert.ok(window.activeTextEditor === undefined); + assert.equal(window.visibleTextEditors.length, 0); + resolve(); + } catch (e) { + reject(e); + } + }); + }); + + return Promise.all([ + commands.executeCommand('workbench.action.closeAllEditors'), + p + ]).then(() => undefined); + }); + }); + + test('issue #20867: vscode.window.visibleTextEditors returns closed document 2/2', () => { + + const file10Path = join(workspace.rootPath || '', './10linefile.ts'); + const file30Path = join(workspace.rootPath || '', './30linefile.ts'); + + return Promise.all([ + workspace.openTextDocument(file10Path), + workspace.openTextDocument(file30Path) + ]).then(docs => { + return Promise.all([ + window.showTextDocument(docs[0], ViewColumn.One), + window.showTextDocument(docs[1], ViewColumn.Two), + ]); + }).then(editors => { + + const p = new Promise((resolve, reject) => { + const sub = workspace.onDidCloseTextDocument(doc => { + try { + sub.dispose(); + assert.ok(window.activeTextEditor === editors[1]); + assert.ok(window.visibleTextEditors[0] === editors[1]); + assert.equal(window.visibleTextEditors.length, 1); + resolve(); + } catch (e) { + reject(e); + } + }); + }); + + // hide doesn't what it means because it triggers a close event and because it + // detached the editor. For this test that's what we want. + editors[0].hide(); + return p; + }); + }); + function executeReplace(editor: TextEditor, range: Range, text: string, undoStopBefore: boolean, undoStopAfter: boolean): Thenable { return editor.edit((builder) => { builder.replace(range, text); diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 176cc70a5016f7dc2c2029f021ab510254bb0a92..6364fd4331882355d9fd0b7ff1e0f4f89920e357 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -66,6 +66,52 @@ export function findFirst(array: T[], p: (x: T) => boolean): number { return low; } +/** + * Takes two *sorted* arrays and computes their delta (removed, added elements). + * Finishes in `Math.min(before.length, after.length)` steps. + * @param before + * @param after + * @param compare + */ +export function delta(before: T[], after: T[], compare: (a: T, b: T) => number) { + + const removed: T[] = []; + const added: T[] = []; + + let beforeIdx = 0; + let afterIdx = 0; + + while (true) { + if (beforeIdx === before.length) { + added.push(...after.slice(afterIdx)); + break; + } + if (afterIdx === after.length) { + removed.push(...before.slice(beforeIdx)); + break; + } + + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + const n = compare(beforeElement, afterElement); + if (n === 0) { + // equal + beforeIdx += 1; + afterIdx += 1; + } else if (n < 0) { + // beforeElement is smaller -> before element removed + removed.push(beforeElement); + beforeIdx += 1; + } else if (n > 0) { + // beforeElement is greater -> after element added + added.push(afterElement); + afterIdx += 1; + } + } + + return { removed, added }; +} + /** * Returns the top N elements from the array. * diff --git a/src/vs/base/test/common/arrays.test.ts b/src/vs/base/test/common/arrays.test.ts index 46d1177dfb46c4e041719cc8fefc659ec2839003..95b7108f9884213223e90664e8d8c8b5191b6328 100644 --- a/src/vs/base/test/common/arrays.test.ts +++ b/src/vs/base/test/common/arrays.test.ts @@ -33,6 +33,40 @@ suite('Arrays', () => { assert.equal(array[idx], 1); }); + test('delta', function () { + function compare(a: number, b: number): number { + return a - b; + } + + let d = arrays.delta([1, 2, 4], [], compare); + assert.deepEqual(d.removed, [1, 2, 4]); + assert.deepEqual(d.added, []); + + d = arrays.delta([], [1, 2, 4], compare); + assert.deepEqual(d.removed, []); + assert.deepEqual(d.added, [1, 2, 4]); + + d = arrays.delta([1, 2, 4], [1, 2, 4], compare); + assert.deepEqual(d.removed, []); + assert.deepEqual(d.added, []); + + d = arrays.delta([1, 2, 4], [2, 3, 4, 5], compare); + assert.deepEqual(d.removed, [1]); + assert.deepEqual(d.added, [3, 5]); + + d = arrays.delta([2, 3, 4, 5], [1, 2, 4], compare); + assert.deepEqual(d.removed, [3, 5]); + assert.deepEqual(d.added, [1]); + + d = arrays.delta([1, 3, 5, 7], [5, 9, 11], compare); + assert.deepEqual(d.removed, [1, 3, 7]); + assert.deepEqual(d.added, [9, 11]); + + d = arrays.delta([1, 3, 7], [5, 9, 11], compare); + assert.deepEqual(d.removed, [1, 3, 7]); + assert.deepEqual(d.added, [5, 9, 11]); + }); + test('binarySearch', function () { function compare(a: number, b: number): number { return a - b; diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 0d69b51aad9dc5291357517cec076e77ef1456cf..5568004e93874a7dfe3564e29f0b53f16e7ba2e2 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -13,6 +13,7 @@ import * as errors from 'vs/base/common/errors'; import product from 'vs/platform/node/product'; import pkg from 'vs/platform/node/package'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/node/extHostFileSystemEventService'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; @@ -28,7 +29,7 @@ import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; import { ExtHostOutputService } from 'vs/workbench/api/node/extHostOutputService'; import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService'; import { ExtHostMessageService } from 'vs/workbench/api/node/extHostMessageService'; -import { ExtHostEditors } from 'vs/workbench/api/node/extHostEditors'; +import { ExtHostEditors } from 'vs/workbench/api/node/extHostTextEditors'; import { ExtHostLanguages } from 'vs/workbench/api/node/extHostLanguages'; import { ExtHostLanguageFeatures } from 'vs/workbench/api/node/extHostLanguageFeatures'; import { ExtHostApiCommands } from 'vs/workbench/api/node/extHostApiCommands'; @@ -97,9 +98,10 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ // Addressable instances const col = new InstanceCollection(); const extHostHeapService = col.define(ExtHostContext.ExtHostHeapService).set(new ExtHostHeapService()); - const extHostDocuments = col.define(ExtHostContext.ExtHostDocuments).set(new ExtHostDocuments(threadService)); + const extHostDocumentsAndEditors = col.define(ExtHostContext.ExtHostDocumentsAndEditors).set(new ExtHostDocumentsAndEditors(threadService)); + const extHostDocuments = col.define(ExtHostContext.ExtHostDocuments).set(new ExtHostDocuments(threadService, extHostDocumentsAndEditors)); const extHostDocumentSaveParticipant = col.define(ExtHostContext.ExtHostDocumentSaveParticipant).set(new ExtHostDocumentSaveParticipant(extHostDocuments, threadService.get(MainContext.MainThreadWorkspace))); - const extHostEditors = col.define(ExtHostContext.ExtHostEditors).set(new ExtHostEditors(threadService, extHostDocuments)); + const extHostEditors = col.define(ExtHostContext.ExtHostEditors).set(new ExtHostEditors(threadService, extHostDocumentsAndEditors)); const extHostCommands = col.define(ExtHostContext.ExtHostCommands).set(new ExtHostCommands(threadService, extHostEditors, extHostHeapService)); const extHostExplorers = col.define(ExtHostContext.ExtHostExplorers).set(new ExtHostTreeExplorers(threadService, extHostCommands)); const extHostConfiguration = col.define(ExtHostContext.ExtHostConfiguration).set(new ExtHostConfiguration(threadService.get(MainContext.MainThreadConfiguration), initData.configuration)); diff --git a/src/vs/workbench/api/node/extHost.contribution.ts b/src/vs/workbench/api/node/extHost.contribution.ts index 2f4695163cf3668cb58885d5a2d19b594bbfa912..c73cce87b513ef1f0ad0b96c12a37eaa40691847 100644 --- a/src/vs/workbench/api/node/extHost.contribution.ts +++ b/src/vs/workbench/api/node/extHost.contribution.ts @@ -36,6 +36,7 @@ import { MainThreadFileSystemEventService } from './mainThreadFileSystemEventSer import { MainThreadSCM } from './mainThreadSCM'; // --- other interested parties +import { MainThreadDocumentsAndEditors } from './mainThreadDocumentsAndEditors'; import { JSONValidationExtensionPoint } from 'vs/platform/jsonschemas/common/jsonValidationExtensionPoint'; import { LanguageConfigurationFileHandler } from 'vs/editor/node/languageConfigurationExtensionPoint'; import { SaveParticipant } from './mainThreadSaveParticipant'; @@ -62,13 +63,15 @@ export class ExtHostContribution implements IWorkbenchContribution { return this.instantiationService.createInstance(ctor); }; + const documentsAndEditors = this.instantiationService.createInstance(MainThreadDocumentsAndEditors); + // Addressable instances const col = new InstanceCollection(); col.define(MainContext.MainThreadCommands).set(create(MainThreadCommands)); col.define(MainContext.MainThreadConfiguration).set(create(MainThreadConfiguration)); col.define(MainContext.MainThreadDiagnostics).set(create(MainThreadDiagnostics)); - col.define(MainContext.MainThreadDocuments).set(create(MainThreadDocuments)); - col.define(MainContext.MainThreadEditors).set(create(MainThreadEditors)); + col.define(MainContext.MainThreadDocuments).set(this.instantiationService.createInstance(MainThreadDocuments, documentsAndEditors)); + col.define(MainContext.MainThreadEditors).set(this.instantiationService.createInstance(MainThreadEditors, documentsAndEditors)); col.define(MainContext.MainThreadErrors).set(create(MainThreadErrors)); col.define(MainContext.MainThreadExplorers).set(create(MainThreadTreeExplorers)); col.define(MainContext.MainThreadLanguageFeatures).set(create(MainThreadLanguageFeatures)); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index f0fdb9bdfb0af40a9fefee626482639caa75cb5b..57607bb36088e3976c495d88b18a5c2524314e4a 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -35,7 +35,7 @@ import { IWorkspaceConfigurationValues } from 'vs/workbench/services/configurati import { IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceSymbol } from 'vs/workbench/parts/search/common/search'; -import { IApplyEditsOptions, IUndoStopOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent } from './mainThreadEditorsTracker'; +import { IApplyEditsOptions, IUndoStopOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent } from './mainThreadEditor'; import { InternalTreeExplorerNodeContent } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; @@ -290,12 +290,10 @@ export interface IModelAddedData { } export abstract class ExtHostDocumentsShape { $provideTextDocumentContent(handle: number, uri: URI): TPromise { throw ni(); } - $acceptModelAdd(initData: IModelAddedData): void { throw ni(); } $acceptModelModeChanged(strURL: string, oldModeId: string, newModeId: string): void { throw ni(); } $acceptModelSaved(strURL: string): void { throw ni(); } $acceptModelDirty(strURL: string): void { throw ni(); } $acceptModelReverted(strURL: string): void { throw ni(); } - $acceptModelRemoved(strURL: string): void { throw ni(); } $acceptModelChanged(strURL: string, events: editorCommon.IModelContentChangedEvent2[], isDirty: boolean): void { throw ni(); } } @@ -314,14 +312,24 @@ export interface ITextEditorPositionData { [id: string]: EditorPosition; } export abstract class ExtHostEditorsShape { - $acceptTextEditorAdd(data: ITextEditorAddData): void { throw ni(); } $acceptOptionsChanged(id: string, opts: IResolvedTextEditorConfiguration): void { throw ni(); } $acceptSelectionsChanged(id: string, event: ISelectionChangeEvent): void { throw ni(); } - $acceptActiveEditorAndVisibleEditors(id: string, visibleIds: string[]): void { throw ni(); } $acceptEditorPositionData(data: ITextEditorPositionData): void { throw ni(); } - $acceptTextEditorRemove(id: string): void { throw ni(); } } +export interface IDocumentsAndEditorsDelta { + removedDocuments?: string[]; + addedDocuments?: IModelAddedData[]; + removedEditors?: string[]; + addedEditors?: ITextEditorAddData[]; + newActiveEditor?: string; +} + +export abstract class ExtHostDocumentsAndEditorsShape { + $acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta): void { throw ni(); } +} + + export abstract class ExtHostTreeExplorersShape { $provideRootNode(providerId: string): TPromise { throw ni(); }; $resolveChildren(providerId: string, node: InternalTreeExplorerNodeContent): TPromise { throw ni(); } @@ -432,6 +440,7 @@ export const ExtHostContext = { ExtHostCommands: createExtId('ExtHostCommands', ExtHostCommandsShape), ExtHostConfiguration: createExtId('ExtHostConfiguration', ExtHostConfigurationShape), ExtHostDiagnostics: createExtId('ExtHostDiagnostics', ExtHostDiagnosticsShape), + ExtHostDocumentsAndEditors: createExtId('ExtHostDocumentsAndEditors', ExtHostDocumentsAndEditorsShape), ExtHostDocuments: createExtId('ExtHostDocuments', ExtHostDocumentsShape), ExtHostDocumentSaveParticipant: createExtId('ExtHostDocumentSaveParticipant', ExtHostDocumentSaveParticipantShape), ExtHostEditors: createExtId('ExtHostEditors', ExtHostEditorsShape), diff --git a/src/vs/workbench/api/node/extHostCommands.ts b/src/vs/workbench/api/node/extHostCommands.ts index 816442d5cdb691bafd958ba1299008402005a87d..1552104b3eaf7bea2520dd6867be2f92985255c1 100644 --- a/src/vs/workbench/api/node/extHostCommands.ts +++ b/src/vs/workbench/api/node/extHostCommands.ts @@ -8,7 +8,7 @@ import { IThreadService } from 'vs/workbench/services/thread/common/threadServic import { validateConstraint } from 'vs/base/common/types'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ExtHostEditors } from 'vs/workbench/api/node/extHostEditors'; +import { ExtHostEditors } from 'vs/workbench/api/node/extHostTextEditors'; import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; import * as extHostTypeConverter from 'vs/workbench/api/node/extHostTypeConverters'; import { cloneAndChange } from 'vs/base/common/objects'; @@ -102,7 +102,7 @@ export class ExtHostCommands extends ExtHostCommandsShape { return TPromise.wrapError(`Contributed command '${id}' does not exist.`); } - let {callback, thisArg, description} = command; + let { callback, thisArg, description } = command; if (description) { for (let i = 0; i < description.args.length; i++) { @@ -140,7 +140,7 @@ export class ExtHostCommands extends ExtHostCommandsShape { $getContributedCommandHandlerDescriptions(): TPromise<{ [id: string]: string | ICommandHandlerDescription }> { const result: { [id: string]: string | ICommandHandlerDescription } = Object.create(null); this._commands.forEach((command, id) => { - let {description} = command; + let { description } = command; if (description) { result[id] = description; } diff --git a/src/vs/workbench/api/node/extHostDocumentData.ts b/src/vs/workbench/api/node/extHostDocumentData.ts new file mode 100644 index 0000000000000000000000000000000000000000..29735c3ac8f8526853f7e1f0652507b616f989eb --- /dev/null +++ b/src/vs/workbench/api/node/extHostDocumentData.ts @@ -0,0 +1,244 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; +import { MirrorModel2 } from 'vs/editor/common/model/mirrorModel2'; +import URI from 'vs/base/common/uri'; +import { Range, Position } from 'vs/workbench/api/node/extHostTypes'; +import * as vscode from 'vscode'; +import { getWordAtText, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; +import { MainThreadDocumentsShape } from './extHost.protocol'; +import { ITextSource } from 'vs/editor/common/model/textSource'; + +const _modeId2WordDefinition = new Map(); +export function setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void { + _modeId2WordDefinition.set(modeId, wordDefinition); +} +export function getWordDefinitionFor(modeId: string): RegExp { + return _modeId2WordDefinition.get(modeId); +} + +export class ExtHostDocumentData extends MirrorModel2 { + + private _proxy: MainThreadDocumentsShape; + private _languageId: string; + private _isDirty: boolean; + private _textLines: vscode.TextLine[]; + private _document: vscode.TextDocument; + + constructor(proxy: MainThreadDocumentsShape, uri: URI, lines: string[], eol: string, + languageId: string, versionId: number, isDirty: boolean) { + + super(uri, lines, eol, versionId); + this._proxy = proxy; + this._languageId = languageId; + this._isDirty = isDirty; + this._textLines = []; + } + + dispose(): void { + this._textLines.length = 0; + this._isDirty = false; + super.dispose(); + } + + equalLines({ lines }: ITextSource): boolean { + const len = lines.length; + if (len !== this._lines.length) { + return false; + } + for (let i = 0; i < len; i++) { + if (lines[i] !== this._lines[i]) { + return false; + } + } + return true; + } + + get document(): vscode.TextDocument { + if (!this._document) { + const data = this; + this._document = { + get uri() { return data._uri; }, + get fileName() { return data._uri.fsPath; }, + get isUntitled() { return data._uri.scheme !== 'file'; }, + get languageId() { return data._languageId; }, + get version() { return data._versionId; }, + get isDirty() { return data._isDirty; }, + save() { return data._proxy.$trySaveDocument(data._uri); }, + getText(range?) { return range ? data._getTextInRange(range) : data.getText(); }, + get lineCount() { return data._lines.length; }, + lineAt(lineOrPos) { return data.lineAt(lineOrPos); }, + offsetAt(pos) { return data.offsetAt(pos); }, + positionAt(offset) { return data.positionAt(offset); }, + validateRange(ran) { return data.validateRange(ran); }, + validatePosition(pos) { return data.validatePosition(pos); }, + getWordRangeAtPosition(pos, regexp?) { return data.getWordRangeAtPosition(pos, regexp); } + }; + } + return this._document; + } + + _acceptLanguageId(newLanguageId: string): void { + this._languageId = newLanguageId; + } + + _acceptIsDirty(isDirty: boolean): void { + this._isDirty = isDirty; + } + + private _getTextInRange(_range: vscode.Range): string { + let range = this.validateRange(_range); + + if (range.isEmpty) { + return ''; + } + + if (range.isSingleLine) { + return this._lines[range.start.line].substring(range.start.character, range.end.character); + } + + let lineEnding = this._eol, + startLineIndex = range.start.line, + endLineIndex = range.end.line, + resultLines: string[] = []; + + resultLines.push(this._lines[startLineIndex].substring(range.start.character)); + for (let i = startLineIndex + 1; i < endLineIndex; i++) { + resultLines.push(this._lines[i]); + } + resultLines.push(this._lines[endLineIndex].substring(0, range.end.character)); + + return resultLines.join(lineEnding); + } + + lineAt(lineOrPosition: number | vscode.Position): vscode.TextLine { + + let line: number; + if (lineOrPosition instanceof Position) { + line = lineOrPosition.line; + } else if (typeof lineOrPosition === 'number') { + line = lineOrPosition; + } + + if (line < 0 || line >= this._lines.length) { + throw new Error('Illegal value for `line`'); + } + + let result = this._textLines[line]; + if (!result || result.lineNumber !== line || result.text !== this._lines[line]) { + + const text = this._lines[line]; + const firstNonWhitespaceCharacterIndex = /^(\s*)/.exec(text)[1].length; + const range = new Range(line, 0, line, text.length); + const rangeIncludingLineBreak = line < this._lines.length - 1 + ? new Range(line, 0, line + 1, 0) + : range; + + result = Object.freeze({ + lineNumber: line, + range, + rangeIncludingLineBreak, + text, + firstNonWhitespaceCharacterIndex, //TODO@api, rename to 'leadingWhitespaceLength' + isEmptyOrWhitespace: firstNonWhitespaceCharacterIndex === text.length + }); + + this._textLines[line] = result; + } + + return result; + } + + offsetAt(position: vscode.Position): number { + position = this.validatePosition(position); + this._ensureLineStarts(); + return this._lineStarts.getAccumulatedValue(position.line - 1) + position.character; + } + + positionAt(offset: number): vscode.Position { + offset = Math.floor(offset); + offset = Math.max(0, offset); + + this._ensureLineStarts(); + let out = this._lineStarts.getIndexOf(offset); + + let lineLength = this._lines[out.index].length; + + // Ensure we return a valid position + return new Position(out.index, Math.min(out.remainder, lineLength)); + } + + // ---- range math + + validateRange(range: vscode.Range): vscode.Range { + if (!(range instanceof Range)) { + throw new Error('Invalid argument'); + } + + let start = this.validatePosition(range.start); + let end = this.validatePosition(range.end); + + if (start === range.start && end === range.end) { + return range; + } + return new Range(start.line, start.character, end.line, end.character); + } + + validatePosition(position: vscode.Position): vscode.Position { + if (!(position instanceof Position)) { + throw new Error('Invalid argument'); + } + + let { line, character } = position; + let hasChanged = false; + + if (line < 0) { + line = 0; + character = 0; + hasChanged = true; + } + else if (line >= this._lines.length) { + line = this._lines.length - 1; + character = this._lines[line].length; + hasChanged = true; + } + else { + let maxCharacter = this._lines[line].length; + if (character < 0) { + character = 0; + hasChanged = true; + } + else if (character > maxCharacter) { + character = maxCharacter; + hasChanged = true; + } + } + + if (!hasChanged) { + return position; + } + return new Position(line, character); + } + + getWordRangeAtPosition(_position: vscode.Position, regexp?: RegExp): vscode.Range { + let position = this.validatePosition(_position); + if (!regexp || regExpLeadsToEndlessLoop(regexp)) { + regexp = getWordDefinitionFor(this._languageId); + } + let wordAtText = getWordAtText( + position.character + 1, + ensureValidWordDefinition(regexp), + this._lines[position.line], + 0 + ); + + if (wordAtText) { + return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1); + } + return undefined; + } +} diff --git a/src/vs/workbench/api/node/extHostDocuments.ts b/src/vs/workbench/api/node/extHostDocuments.ts index 3169769d16a3a63fc3fa23d1e54187f24afbd4f1..eaf14c3356fb35311f958e53f1d97b9587e7ee0c 100644 --- a/src/vs/workbench/api/node/extHostDocuments.ts +++ b/src/vs/workbench/api/node/extHostDocuments.ts @@ -5,82 +5,74 @@ 'use strict'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { MirrorModel2 } from 'vs/editor/common/model/mirrorModel2'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; import Event, { Emitter } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { Range, Position, Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/workbench/api/node/extHostTypes'; import * as TypeConverters from './extHostTypeConverters'; import { TPromise } from 'vs/base/common/winjs.base'; import * as vscode from 'vscode'; import { asWinJsPromise } from 'vs/base/common/async'; -import { getWordAtText, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; -import { MainContext, MainThreadDocumentsShape, ExtHostDocumentsShape, IModelAddedData } from './extHost.protocol'; -import { ITextSource, TextSource } from 'vs/editor/common/model/textSource'; - -const _modeId2WordDefinition = new Map(); - -function setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void { - _modeId2WordDefinition.set(modeId, wordDefinition); -} - -function getWordDefinitionFor(modeId: string): RegExp { - return _modeId2WordDefinition.get(modeId); -} +import { TextSource } from 'vs/editor/common/model/textSource'; +import { MainContext, MainThreadDocumentsShape, ExtHostDocumentsShape } from './extHost.protocol'; +import { ExtHostDocumentData, setWordDefinitionFor } from './extHostDocumentData'; +import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors'; export class ExtHostDocuments extends ExtHostDocumentsShape { private static _handlePool: number = 0; - private _onDidAddDocumentEventEmitter: Emitter; - public onDidAddDocument: Event; - - private _onDidRemoveDocumentEventEmitter: Emitter; - public onDidRemoveDocument: Event; + private _onDidAddDocument = new Emitter(); + private _onDidRemoveDocument = new Emitter(); + private _onDidChangeDocument = new Emitter(); + private _onDidSaveDocument = new Emitter(); - private _onDidChangeDocumentEventEmitter: Emitter; - public onDidChangeDocument: Event; + readonly onDidAddDocument: Event = this._onDidAddDocument.event; + readonly onDidRemoveDocument: Event = this._onDidRemoveDocument.event; + readonly onDidChangeDocument: Event = this._onDidChangeDocument.event; + readonly onDidSaveDocument: Event = this._onDidSaveDocument.event; - private _onDidSaveDocumentEventEmitter: Emitter; - public onDidSaveDocument: Event; - - private _documentData = new Map(); + private _toDispose: IDisposable[]; + private _proxy: MainThreadDocumentsShape; + private _documentsAndEditors: ExtHostDocumentsAndEditors; private _documentLoader = new Map>(); private _documentContentProviders = new Map(); - private _proxy: MainThreadDocumentsShape; - constructor(threadService: IThreadService) { + constructor(threadService: IThreadService, documentsAndEditors: ExtHostDocumentsAndEditors) { super(); this._proxy = threadService.get(MainContext.MainThreadDocuments); + this._documentsAndEditors = documentsAndEditors; - this._onDidAddDocumentEventEmitter = new Emitter(); - this.onDidAddDocument = this._onDidAddDocumentEventEmitter.event; - - this._onDidRemoveDocumentEventEmitter = new Emitter(); - this.onDidRemoveDocument = this._onDidRemoveDocumentEventEmitter.event; - - this._onDidChangeDocumentEventEmitter = new Emitter(); - this.onDidChangeDocument = this._onDidChangeDocumentEventEmitter.event; + this._toDispose = [ + this._documentsAndEditors.onDidRemoveDocuments(documents => { + for (const data of documents) { + this._onDidRemoveDocument.fire(data.document); + } + }), + this._documentsAndEditors.onDidAddDocuments(documents => { + for (const data of documents) { + this._onDidAddDocument.fire(data.document); + } + }) + ]; + } - this._onDidSaveDocumentEventEmitter = new Emitter(); - this.onDidSaveDocument = this._onDidSaveDocumentEventEmitter.event; + public dispose(): void { + dispose(this._toDispose); } public getAllDocumentData(): ExtHostDocumentData[] { - const result: ExtHostDocumentData[] = []; - this._documentData.forEach(data => result.push(data)); - return result; + return this._documentsAndEditors.allDocuments(); } public getDocumentData(resource: vscode.Uri): ExtHostDocumentData { if (!resource) { return undefined; } - const data = this._documentData.get(resource.toString()); + const data = this._documentsAndEditors.getDocument(resource.toString()); if (data) { return data; } @@ -89,7 +81,7 @@ export class ExtHostDocuments extends ExtHostDocumentsShape { public ensureDocumentData(uri: URI): TPromise { - let cached = this._documentData.get(uri.toString()); + let cached = this._documentsAndEditors.getDocument(uri.toString()); if (cached) { return TPromise.as(cached); } @@ -98,7 +90,7 @@ export class ExtHostDocuments extends ExtHostDocumentsShape { if (!promise) { promise = this._proxy.$tryOpenDocument(uri).then(() => { this._documentLoader.delete(uri.toString()); - return this._documentData.get(uri.toString()); + return this._documentsAndEditors.getDocument(uri.toString()); }, err => { this._documentLoader.delete(uri.toString()); return TPromise.wrapError(err); @@ -126,10 +118,10 @@ export class ExtHostDocuments extends ExtHostDocumentsShape { let subscription: IDisposable; if (typeof provider.onDidChange === 'function') { subscription = provider.onDidChange(uri => { - if (this._documentData.has(uri.toString())) { + if (this._documentsAndEditors.getDocument(uri.toString())) { this.$provideTextDocumentContent(handle, uri).then(value => { - const document = this._documentData.get(uri.toString()); + const document = this._documentsAndEditors.getDocument(uri.toString()); if (!document) { // disposed in the meantime return; @@ -158,7 +150,7 @@ export class ExtHostDocuments extends ExtHostDocumentsShape { }); } - $provideTextDocumentContent(handle: number, uri: URI): TPromise { + public $provideTextDocumentContent(handle: number, uri: URI): TPromise { const provider = this._documentContentProviders.get(handle); if (!provider) { return TPromise.wrapError(`unsupported uri-scheme: ${uri.scheme}`); @@ -166,57 +158,37 @@ export class ExtHostDocuments extends ExtHostDocumentsShape { return asWinJsPromise(token => provider.provideTextDocumentContent(uri, token)); } - public $acceptModelAdd(initData: IModelAddedData): void { - let data = new ExtHostDocumentData(this._proxy, initData.url, initData.lines, initData.EOL, initData.modeId, initData.versionId, initData.isDirty); - let key = data.document.uri.toString(); - if (this._documentData.has(key)) { - throw new Error('Document `' + key + '` already exists.'); - } - this._documentData.set(key, data); - this._onDidAddDocumentEventEmitter.fire(data.document); - } - public $acceptModelModeChanged(strURL: string, oldModeId: string, newModeId: string): void { - let data = this._documentData.get(strURL); + let data = this._documentsAndEditors.getDocument(strURL); // Treat a mode change as a remove + add - this._onDidRemoveDocumentEventEmitter.fire(data.document); + this._onDidRemoveDocument.fire(data.document); data._acceptLanguageId(newModeId); - this._onDidAddDocumentEventEmitter.fire(data.document); + this._onDidAddDocument.fire(data.document); } public $acceptModelSaved(strURL: string): void { - let data = this._documentData.get(strURL); + let data = this._documentsAndEditors.getDocument(strURL); data._acceptIsDirty(false); - this._onDidSaveDocumentEventEmitter.fire(data.document); + this._onDidSaveDocument.fire(data.document); } public $acceptModelDirty(strURL: string): void { - let document = this._documentData.get(strURL); + let document = this._documentsAndEditors.getDocument(strURL); document._acceptIsDirty(true); } public $acceptModelReverted(strURL: string): void { - let document = this._documentData.get(strURL); + let document = this._documentsAndEditors.getDocument(strURL); document._acceptIsDirty(false); } - public $acceptModelRemoved(strURL: string): void { - if (!this._documentData.has(strURL)) { - throw new Error('Document `' + strURL + '` does not exist.'); - } - let data = this._documentData.get(strURL); - this._documentData.delete(strURL); - this._onDidRemoveDocumentEventEmitter.fire(data.document); - data.dispose(); - } - public $acceptModelChanged(strURL: string, events: editorCommon.IModelContentChangedEvent2[], isDirty: boolean): void { - let data = this._documentData.get(strURL); + let data = this._documentsAndEditors.getDocument(strURL); data._acceptIsDirty(isDirty); data.onEvents(events); - this._onDidChangeDocumentEventEmitter.fire({ + this._onDidChangeDocument.fire({ document: data.document, contentChanges: events.map((e) => { return { @@ -228,229 +200,7 @@ export class ExtHostDocuments extends ExtHostDocumentsShape { }); } - setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void { + public setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void { setWordDefinitionFor(modeId, wordDefinition); } } - -export class ExtHostDocumentData extends MirrorModel2 { - - private _proxy: MainThreadDocumentsShape; - private _languageId: string; - private _isDirty: boolean; - private _textLines: vscode.TextLine[]; - private _document: vscode.TextDocument; - - constructor(proxy: MainThreadDocumentsShape, uri: URI, lines: string[], eol: string, - languageId: string, versionId: number, isDirty: boolean) { - - super(uri, lines, eol, versionId); - this._proxy = proxy; - this._languageId = languageId; - this._isDirty = isDirty; - this._textLines = []; - } - - dispose(): void { - this._textLines.length = 0; - this._isDirty = false; - super.dispose(); - } - - equalLines({lines}: ITextSource): boolean { - const len = lines.length; - if (len !== this._lines.length) { - return false; - } - for (let i = 0; i < len; i++) { - if (lines[i] !== this._lines[i]) { - return false; - } - } - return true; - } - - get document(): vscode.TextDocument { - if (!this._document) { - const data = this; - this._document = { - get uri() { return data._uri; }, - get fileName() { return data._uri.fsPath; }, - get isUntitled() { return data._uri.scheme !== 'file'; }, - get languageId() { return data._languageId; }, - get version() { return data._versionId; }, - get isDirty() { return data._isDirty; }, - save() { return data._proxy.$trySaveDocument(data._uri); }, - getText(range?) { return range ? data._getTextInRange(range) : data.getText(); }, - get lineCount() { return data._lines.length; }, - lineAt(lineOrPos) { return data.lineAt(lineOrPos); }, - offsetAt(pos) { return data.offsetAt(pos); }, - positionAt(offset) { return data.positionAt(offset); }, - validateRange(ran) { return data.validateRange(ran); }, - validatePosition(pos) { return data.validatePosition(pos); }, - getWordRangeAtPosition(pos, regexp?) { return data.getWordRangeAtPosition(pos, regexp); } - }; - } - return this._document; - } - - _acceptLanguageId(newLanguageId: string): void { - this._languageId = newLanguageId; - } - - _acceptIsDirty(isDirty: boolean): void { - this._isDirty = isDirty; - } - - private _getTextInRange(_range: vscode.Range): string { - let range = this.validateRange(_range); - - if (range.isEmpty) { - return ''; - } - - if (range.isSingleLine) { - return this._lines[range.start.line].substring(range.start.character, range.end.character); - } - - let lineEnding = this._eol, - startLineIndex = range.start.line, - endLineIndex = range.end.line, - resultLines: string[] = []; - - resultLines.push(this._lines[startLineIndex].substring(range.start.character)); - for (let i = startLineIndex + 1; i < endLineIndex; i++) { - resultLines.push(this._lines[i]); - } - resultLines.push(this._lines[endLineIndex].substring(0, range.end.character)); - - return resultLines.join(lineEnding); - } - - lineAt(lineOrPosition: number | vscode.Position): vscode.TextLine { - - let line: number; - if (lineOrPosition instanceof Position) { - line = lineOrPosition.line; - } else if (typeof lineOrPosition === 'number') { - line = lineOrPosition; - } - - if (line < 0 || line >= this._lines.length) { - throw new Error('Illegal value for `line`'); - } - - let result = this._textLines[line]; - if (!result || result.lineNumber !== line || result.text !== this._lines[line]) { - - const text = this._lines[line]; - const firstNonWhitespaceCharacterIndex = /^(\s*)/.exec(text)[1].length; - const range = new Range(line, 0, line, text.length); - const rangeIncludingLineBreak = line < this._lines.length - 1 - ? new Range(line, 0, line + 1, 0) - : range; - - result = Object.freeze({ - lineNumber: line, - range, - rangeIncludingLineBreak, - text, - firstNonWhitespaceCharacterIndex, //TODO@api, rename to 'leadingWhitespaceLength' - isEmptyOrWhitespace: firstNonWhitespaceCharacterIndex === text.length - }); - - this._textLines[line] = result; - } - - return result; - } - - offsetAt(position: vscode.Position): number { - position = this.validatePosition(position); - this._ensureLineStarts(); - return this._lineStarts.getAccumulatedValue(position.line - 1) + position.character; - } - - positionAt(offset: number): vscode.Position { - offset = Math.floor(offset); - offset = Math.max(0, offset); - - this._ensureLineStarts(); - let out = this._lineStarts.getIndexOf(offset); - - let lineLength = this._lines[out.index].length; - - // Ensure we return a valid position - return new Position(out.index, Math.min(out.remainder, lineLength)); - } - - // ---- range math - - validateRange(range: vscode.Range): vscode.Range { - if (!(range instanceof Range)) { - throw new Error('Invalid argument'); - } - - let start = this.validatePosition(range.start); - let end = this.validatePosition(range.end); - - if (start === range.start && end === range.end) { - return range; - } - return new Range(start.line, start.character, end.line, end.character); - } - - validatePosition(position: vscode.Position): vscode.Position { - if (!(position instanceof Position)) { - throw new Error('Invalid argument'); - } - - let {line, character} = position; - let hasChanged = false; - - if (line < 0) { - line = 0; - character = 0; - hasChanged = true; - } - else if (line >= this._lines.length) { - line = this._lines.length - 1; - character = this._lines[line].length; - hasChanged = true; - } - else { - let maxCharacter = this._lines[line].length; - if (character < 0) { - character = 0; - hasChanged = true; - } - else if (character > maxCharacter) { - character = maxCharacter; - hasChanged = true; - } - } - - if (!hasChanged) { - return position; - } - return new Position(line, character); - } - - getWordRangeAtPosition(_position: vscode.Position, regexp?: RegExp): vscode.Range { - let position = this.validatePosition(_position); - if (!regexp || regExpLeadsToEndlessLoop(regexp)) { - regexp = getWordDefinitionFor(this._languageId); - } - let wordAtText = getWordAtText( - position.character + 1, - ensureValidWordDefinition(regexp), - this._lines[position.line], - 0 - ); - - if (wordAtText) { - return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1); - } - return undefined; - } -} diff --git a/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee5180971b2782bd13047d510537d9317d86affe --- /dev/null +++ b/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Event, { Emitter } from 'vs/base/common/event'; +import { dispose } from 'vs/base/common/lifecycle'; +import { MainContext, ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta } from './extHost.protocol'; +import { ExtHostDocumentData } from './extHostDocumentData'; +import { ExtHostTextEditor } from './extHostTextEditor'; +import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; +import * as assert from 'assert'; +import * as typeConverters from './extHostTypeConverters'; + +export class ExtHostDocumentsAndEditors extends ExtHostDocumentsAndEditorsShape { + + private _activeEditorId: string; + private readonly _editors = new Map(); + private readonly _documents = new Map(); + + private readonly _onDidAddDocuments = new Emitter(); + private readonly _onDidRemoveDocuments = new Emitter(); + private readonly _onDidChangeVisibleTextEditors = new Emitter(); + private readonly _onDidChangeActiveTextEditor = new Emitter(); + + readonly onDidAddDocuments: Event = this._onDidAddDocuments.event; + readonly onDidRemoveDocuments: Event = this._onDidRemoveDocuments.event; + readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; + readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; + + constructor( + @IThreadService private _threadService: IThreadService + ) { + super(); + } + + $acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta): void { + + const removedDocuments: ExtHostDocumentData[] = []; + const addedDocuments: ExtHostDocumentData[] = []; + const removedEditors: ExtHostTextEditor[] = []; + + if (delta.removedDocuments) { + for (const id of delta.removedDocuments) { + const data = this._documents.get(id); + this._documents.delete(id); + removedDocuments.push(data); + } + } + + if (delta.addedDocuments) { + for (const data of delta.addedDocuments) { + assert.ok(!this._documents.has(data.url.toString()), `document '${data.url} already exists!'`); + + const documentData = new ExtHostDocumentData( + this._threadService.get(MainContext.MainThreadDocuments), + data.url, + data.lines, + data.EOL, + data.modeId, + data.versionId, + data.isDirty + ); + this._documents.set(data.url.toString(), documentData); + addedDocuments.push(documentData); + } + } + + if (delta.removedEditors) { + for (const id of delta.removedEditors) { + const editor = this._editors.get(id); + this._editors.delete(id); + removedEditors.push(editor); + } + } + + if (delta.addedEditors) { + for (const data of delta.addedEditors) { + assert.ok(this._documents.has(data.document.toString()), `document '${data.document}' does not exist`); + assert.ok(!this._editors.has(data.id), `editor '${data.id}' already exists!`); + + const documentData = this._documents.get(data.document.toString()); + const editor = new ExtHostTextEditor( + this._threadService.get(MainContext.MainThreadEditors), + data.id, + documentData, + data.selections.map(typeConverters.toSelection), + data.options, + typeConverters.toViewColumn(data.editorPosition) + ); + this._editors.set(data.id, editor); + } + } + + if (delta.newActiveEditor !== undefined) { + assert.ok(delta.newActiveEditor === null || this._editors.has(delta.newActiveEditor), `active editor '${delta.newActiveEditor}' does not exist`); + this._activeEditorId = delta.newActiveEditor; + } + + // now that the internal state is complete, fire events + if (delta.removedDocuments) { + this._onDidRemoveDocuments.fire(removedDocuments); + } + if (delta.addedDocuments) { + this._onDidAddDocuments.fire(addedDocuments); + } + + if (delta.removedEditors || delta.addedEditors) { + this._onDidChangeVisibleTextEditors.fire(this.allEditors()); + } + if (delta.newActiveEditor) { + this._onDidChangeActiveTextEditor.fire(this.activeEditor()); + } + + // now that the events are out, dispose removed documents and editors + dispose(removedDocuments); + dispose(removedEditors); + } + + getDocument(strUrl: string): ExtHostDocumentData { + return this._documents.get(strUrl); + } + + allDocuments(): ExtHostDocumentData[] { + const result: ExtHostDocumentData[] = []; + this._documents.forEach(data => result.push(data)); + return result; + } + + getEditor(id: string): ExtHostTextEditor { + return this._editors.get(id); + } + + activeEditor(): ExtHostTextEditor | undefined { + if (!this._activeEditorId) { + return undefined; + } else { + return this._editors.get(this._activeEditorId); + } + } + + allEditors(): ExtHostTextEditor[] { + const result: ExtHostTextEditor[] = []; + this._editors.forEach(data => result.push(data)); + return result; + } +} diff --git a/src/vs/workbench/api/node/extHostEditors.ts b/src/vs/workbench/api/node/extHostTextEditor.ts similarity index 67% rename from src/vs/workbench/api/node/extHostEditors.ts rename to src/vs/workbench/api/node/extHostTextEditor.ts index 2b948c2700d44bc69af5070eca8063b93a3eb253..b4e4488fa83d9b9ff4bd3a40bd9ed4d450394859 100644 --- a/src/vs/workbench/api/node/extHostEditors.ts +++ b/src/vs/workbench/api/node/extHostTextEditor.ts @@ -4,174 +4,19 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; + import { readonly, illegalArgument } from 'vs/base/common/errors'; -import { equals as arrayEquals } from 'vs/base/common/arrays'; import { IdGenerator } from 'vs/base/common/idGenerator'; -import Event, { Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; -import { ExtHostDocuments, ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocuments'; -import { Selection, Range, Position, EndOfLine, TextEditorRevealType, TextEditorSelectionChangeKind, TextEditorLineNumbersStyle, SnippetString } from './extHostTypes'; +import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocumentData'; +import { Selection, Range, Position, EndOfLine, TextEditorRevealType, TextEditorLineNumbersStyle, SnippetString } from './extHostTypes'; import { ISingleEditOperation, TextEditorCursorStyle, IRange } from 'vs/editor/common/editorCommon'; -import { IResolvedTextEditorConfiguration, ISelectionChangeEvent, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; +import { IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditor'; import * as TypeConverters from './extHostTypeConverters'; -import { MainContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextEditorAddData, ITextEditorPositionData } from './extHost.protocol'; +import { MainThreadEditorsShape } from './extHost.protocol'; import * as vscode from 'vscode'; -export class ExtHostEditors extends ExtHostEditorsShape { - - public onDidChangeTextEditorSelection: Event; - private _onDidChangeTextEditorSelection: Emitter; - - public onDidChangeTextEditorOptions: Event; - private _onDidChangeTextEditorOptions: Emitter; - - public onDidChangeTextEditorViewColumn: Event; - private _onDidChangeTextEditorViewColumn: Emitter; - - private _editors: Map; - private _proxy: MainThreadEditorsShape; - private _onDidChangeActiveTextEditor: Emitter; - private _onDidChangeVisibleTextEditors: Emitter; - private _extHostDocuments: ExtHostDocuments; - private _activeEditorId: string; - private _visibleEditorIds: string[]; - - constructor( - threadService: IThreadService, - extHostDocuments: ExtHostDocuments - ) { - super(); - 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._extHostDocuments = extHostDocuments; - this._proxy = threadService.get(MainContext.MainThreadEditors); - this._onDidChangeActiveTextEditor = new Emitter(); - this._onDidChangeVisibleTextEditors = new Emitter(); - this._editors = new Map(); - - this._visibleEditorIds = []; - } - - getActiveTextEditor(): vscode.TextEditor { - return this._editors.get(this._activeEditorId); - } - - getVisibleTextEditors(): vscode.TextEditor[] { - return this._visibleEditorIds.map(id => this._editors.get(id)); - } - - get onDidChangeActiveTextEditor(): Event { - return this._onDidChangeActiveTextEditor && this._onDidChangeActiveTextEditor.event; - } - - get onDidChangeVisibleTextEditors(): Event { - return this._onDidChangeVisibleTextEditors && this._onDidChangeVisibleTextEditors.event; - } - - showTextDocument(document: vscode.TextDocument, column: vscode.ViewColumn, preserveFocus: boolean): TPromise { - return this._proxy.$tryShowTextDocument(document.uri, TypeConverters.fromViewColumn(column), preserveFocus).then(id => { - let editor = this._editors.get(id); - if (editor) { - return editor; - } else { - throw new Error(`Failed to show text document ${document.uri.toString()}, should show in editor #${id}`); - } - }); - } - - createTextEditorDecorationType(options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { - return new TextEditorDecorationType(this._proxy, options); - } - - // --- called from main thread - - $acceptTextEditorAdd(data: ITextEditorAddData): void { - let document = this._extHostDocuments.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.set(data.id, newEditor); - } - - $acceptOptionsChanged(id: string, opts: IResolvedTextEditorConfiguration): void { - let editor = this._editors.get(id); - editor._acceptOptions(opts); - this._onDidChangeTextEditorOptions.fire({ - textEditor: editor, - options: opts - }); - } - - $acceptSelectionsChanged(id: string, event: ISelectionChangeEvent): void { - const kind = TextEditorSelectionChangeKind.fromValue(event.source); - const selections = event.selections.map(TypeConverters.toSelection); - const textEditor = this._editors.get(id); - textEditor._acceptSelections(selections); - this._onDidChangeTextEditorSelection.fire({ - textEditor, - selections, - kind - }); - } - - $acceptActiveEditorAndVisibleEditors(id: string, visibleIds: string[]): void { - let visibleChanged = false; - let activeChanged = false; - - if (!arrayEquals(this._visibleEditorIds, visibleIds)) { - this._visibleEditorIds = visibleIds; - visibleChanged = true; - } - - if (this._activeEditorId !== id) { - this._activeEditorId = id; - activeChanged = true; - } - - if (visibleChanged) { - this._onDidChangeVisibleTextEditors.fire(this.getVisibleTextEditors()); - } - if (activeChanged) { - this._onDidChangeActiveTextEditor.fire(this.getActiveTextEditor()); - } - } - - $acceptEditorPositionData(data: ITextEditorPositionData): void { - for (let id in data) { - let textEditor = this._editors.get(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.get(id); - editor.dispose(); - this._editors.delete(id); - } -} - -class TextEditorDecorationType implements vscode.TextEditorDecorationType { +export class TextEditorDecorationType implements vscode.TextEditorDecorationType { private static _Keys = new IdGenerator('TextEditorDecorationType'); @@ -467,7 +312,7 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { } -class ExtHostTextEditor implements vscode.TextEditor { +export class ExtHostTextEditor implements vscode.TextEditor { private _proxy: MainThreadEditorsShape; private _id: string; @@ -630,7 +475,7 @@ class ExtHostTextEditor implements vscode.TextEditor { ranges = this._selections.map(TypeConverters.fromRange); } else if (where instanceof Position) { - const {lineNumber, column} = TypeConverters.fromPosition(where); + const { lineNumber, column } = TypeConverters.fromPosition(where); ranges = [{ startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: column }]; } else if (where instanceof Range) { @@ -641,7 +486,7 @@ class ExtHostTextEditor implements vscode.TextEditor { if (posOrRange instanceof Range) { ranges.push(TypeConverters.fromRange(posOrRange)); } else { - const {lineNumber, column} = TypeConverters.fromPosition(posOrRange); + const { lineNumber, column } = TypeConverters.fromPosition(posOrRange); ranges.push({ startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: column }); } } diff --git a/src/vs/workbench/api/node/extHostTextEditors.ts b/src/vs/workbench/api/node/extHostTextEditors.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb4fd75dfd35dcf58efca2f91f8471a496466fd1 --- /dev/null +++ b/src/vs/workbench/api/node/extHostTextEditors.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TPromise } from 'vs/base/common/winjs.base'; +import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; +import { TextEditorSelectionChangeKind } from './extHostTypes'; +import { IResolvedTextEditorConfiguration, ISelectionChangeEvent } from 'vs/workbench/api/node/mainThreadEditor'; +import * as TypeConverters from './extHostTypeConverters'; +import { TextEditorDecorationType } from './extHostTextEditor'; +import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors'; +import { MainContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextEditorPositionData } from './extHost.protocol'; +import * as vscode from 'vscode'; + +export class ExtHostEditors extends ExtHostEditorsShape { + + private readonly _onDidChangeTextEditorSelection = new Emitter(); + private readonly _onDidChangeTextEditorOptions = new Emitter(); + private readonly _onDidChangeTextEditorViewColumn = new Emitter(); + private readonly _onDidChangeActiveTextEditor = new Emitter(); + private readonly _onDidChangeVisibleTextEditors = new Emitter(); + + readonly onDidChangeTextEditorSelection: Event = this._onDidChangeTextEditorSelection.event; + readonly onDidChangeTextEditorOptions: Event = this._onDidChangeTextEditorOptions.event; + readonly onDidChangeTextEditorViewColumn: Event = this._onDidChangeTextEditorViewColumn.event; + readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; + readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; + + + private _proxy: MainThreadEditorsShape; + private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; + + constructor( + threadService: IThreadService, + extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + ) { + super(); + this._proxy = threadService.get(MainContext.MainThreadEditors); + this._extHostDocumentsAndEditors = extHostDocumentsAndEditors; + + this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e)); + this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e)); + } + + getActiveTextEditor(): vscode.TextEditor { + return this._extHostDocumentsAndEditors.activeEditor(); + } + + getVisibleTextEditors(): vscode.TextEditor[] { + return this._extHostDocumentsAndEditors.allEditors(); + } + + showTextDocument(document: vscode.TextDocument, column: vscode.ViewColumn, preserveFocus: boolean): TPromise { + return this._proxy.$tryShowTextDocument(document.uri, TypeConverters.fromViewColumn(column), preserveFocus).then(id => { + let editor = this._extHostDocumentsAndEditors.getEditor(id); + if (editor) { + return editor; + } else { + throw new Error(`Failed to show text document ${document.uri.toString()}, should show in editor #${id}`); + } + }); + } + + createTextEditorDecorationType(options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { + return new TextEditorDecorationType(this._proxy, options); + } + + // --- called from main thread + + $acceptOptionsChanged(id: string, opts: IResolvedTextEditorConfiguration): void { + let editor = this._extHostDocumentsAndEditors.getEditor(id); + editor._acceptOptions(opts); + this._onDidChangeTextEditorOptions.fire({ + textEditor: editor, + options: opts + }); + } + + $acceptSelectionsChanged(id: string, event: ISelectionChangeEvent): void { + const kind = TextEditorSelectionChangeKind.fromValue(event.source); + const selections = event.selections.map(TypeConverters.toSelection); + const textEditor = this._extHostDocumentsAndEditors.getEditor(id); + textEditor._acceptSelections(selections); + this._onDidChangeTextEditorSelection.fire({ + textEditor, + selections, + kind + }); + } + + $acceptEditorPositionData(data: ITextEditorPositionData): void { + for (let id in data) { + let textEditor = this._extHostDocumentsAndEditors.getEditor(id); + let viewColumn = TypeConverters.toViewColumn(data[id]); + if (textEditor.viewColumn !== viewColumn) { + textEditor._acceptViewColumn(viewColumn); + this._onDidChangeTextEditorViewColumn.fire({ textEditor, viewColumn }); + } + } + } +} diff --git a/src/vs/workbench/api/node/mainThreadDocuments.ts b/src/vs/workbench/api/node/mainThreadDocuments.ts index aabb858100dfdd7add21676e80cc9c5649e10b06..e464aeb07202bb04fd3b1ae0c6e793b1ccbabb3f 100644 --- a/src/vs/workbench/api/node/mainThreadDocuments.ts +++ b/src/vs/workbench/api/node/mainThreadDocuments.ts @@ -20,8 +20,10 @@ import { ExtHostContext, MainThreadDocumentsShape, ExtHostDocumentsShape } from import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; import { ITextSource } from 'vs/editor/common/model/textSource'; +import { MainThreadDocumentsAndEditors } from './mainThreadDocumentsAndEditors'; export class MainThreadDocuments extends MainThreadDocumentsShape { + private _modelService: IModelService; private _modeService: IModeService; private _textModelResolverService: ITextModelResolverService; @@ -36,6 +38,7 @@ export class MainThreadDocuments extends MainThreadDocumentsShape { private _resourceContentProvider: { [handle: number]: IDisposable }; constructor( + documentsAndEditors: MainThreadDocumentsAndEditors, @IThreadService threadService: IThreadService, @IModelService modelService: IModelService, @IModeService modeService: IModeService, @@ -57,8 +60,8 @@ export class MainThreadDocuments extends MainThreadDocumentsShape { this._modelIsSynced = {}; this._toDispose = []; - modelService.onModelAdded(this._onModelAdded, this, this._toDispose); - modelService.onModelRemoved(this._onModelRemoved, this, this._toDispose); + this._toDispose.push(documentsAndEditors.onDocumentAdd(models => models.forEach(this._onModelAdded, this))); + this._toDispose.push(documentsAndEditors.onDocumentRemove(urls => urls.forEach(this._onModelRemoved, this))); modelService.onModelModeChanged(this._onModelModeChanged, this, this._toDispose); this._toDispose.push(textFileService.models.onModelSaved(e => { @@ -103,14 +106,6 @@ export class MainThreadDocuments extends MainThreadDocumentsShape { let modelUrl = model.uri; this._modelIsSynced[modelUrl.toString()] = true; this._modelToDisposeMap[modelUrl.toString()] = model.addBulkListener((events) => this._onModelEvents(modelUrl, events)); - this._proxy.$acceptModelAdd({ - url: model.uri, - versionId: model.getVersionId(), - lines: model.getLinesContent(), - EOL: model.getEOL(), - modeId: model.getLanguageIdentifier().language, - isDirty: this._textFileService.isDirty(modelUrl) - }); } private _onModelModeChanged(event: { model: editorCommon.IModel; oldModeId: string; }): void { @@ -122,15 +117,14 @@ export class MainThreadDocuments extends MainThreadDocumentsShape { this._proxy.$acceptModelModeChanged(model.uri.toString(), oldModeId, model.getLanguageIdentifier().language); } - private _onModelRemoved(model: editorCommon.IModel): void { - let modelUrl = model.uri; - if (!this._modelIsSynced[modelUrl.toString()]) { + private _onModelRemoved(modelUrl: string): void { + + if (!this._modelIsSynced[modelUrl]) { return; } - delete this._modelIsSynced[modelUrl.toString()]; - this._modelToDisposeMap[modelUrl.toString()].dispose(); - delete this._modelToDisposeMap[modelUrl.toString()]; - this._proxy.$acceptModelRemoved(modelUrl.toString()); + delete this._modelIsSynced[modelUrl]; + this._modelToDisposeMap[modelUrl].dispose(); + delete this._modelToDisposeMap[modelUrl]; } private _onModelEvents(modelUrl: URI, events: EmitterEvent[]): void { @@ -246,6 +240,7 @@ export class MainThreadDocuments extends MainThreadDocumentsShape { if (!model) { return; } + const raw: ITextSource = { lines: value.lines, length: value.length, diff --git a/src/vs/workbench/api/node/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/node/mainThreadDocumentsAndEditors.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d710b5eceb36c3d936ebf114205ad3a1326c06a --- /dev/null +++ b/src/vs/workbench/api/node/mainThreadDocumentsAndEditors.ts @@ -0,0 +1,351 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IModelService } from 'vs/editor/common/services/modelService'; +import { IModel, ICommonCodeEditor, isCommonCodeEditor, isCommonDiffEditor } from 'vs/editor/common/editorCommon'; +import { compare } from 'vs/base/common/strings'; +import { delta } from 'vs/base/common/arrays'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; +import Event, { Emitter, any } from 'vs/base/common/event'; +import { ExtHostContext, ExtHostDocumentsAndEditorsShape, IModelAddedData, ITextEditorAddData, IDocumentsAndEditorsDelta } from './extHost.protocol'; +import { MainThreadTextEditor } from 'vs/workbench/api/node/mainThreadEditor'; +import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { Position as EditorPosition, IEditor } from 'vs/platform/editor/common/editor'; + +namespace cmp { + export function compareModels(a: IModel, b: IModel): number { + return compare(a.uri.toString(), b.uri.toString()); + } + export function compareEditors(a: EditorAndModel, b: EditorAndModel): number { + let ret = compare(a.editor.getId(), b.editor.getId()); + if (ret === 0) { + ret = compare(a.document.uri.toString(), b.document.uri.toString()); + } + return ret; + } +} + +class EditorAndModel { + + readonly id: string; + + constructor( + readonly editor: ICommonCodeEditor, + readonly document: IModel, + ) { + this.id = `${editor.getId()},${document.uri.toString()}`; + } +} + +class DocumentAndEditorStateDelta { + + readonly isEmpty: boolean; + + constructor( + readonly removedDocuments: IModel[], + readonly addedDocuments: IModel[], + readonly removedEditors: EditorAndModel[], + readonly addedEditors: EditorAndModel[], + readonly oldActiveEditor: string, + readonly newActiveEditor: string, + ) { + this.isEmpty = this.removedDocuments.length === 0 + && this.addedDocuments.length === 0 + && this.removedEditors.length === 0 + && this.addedEditors.length === 0 + && oldActiveEditor === newActiveEditor; + } + + toString(): string { + let ret = 'DocumentAndEditorStateDelta\n'; + ret += `\tRemoved Documents: [${this.removedDocuments.map(d => d.uri.toString(true)).join(', ')}]\n`; + ret += `\tAdded Documents: [${this.addedDocuments.map(d => d.uri.toString(true)).join(', ')}]\n`; + ret += `\tRemoved Editors: [${this.removedEditors.map(e => e.id).join(', ')}]\n`; + ret += `\tAdded Editors: [${this.addedEditors.map(e => e.id).join(', ')}]\n`; + ret += `\tNew Active Editor: ${this.newActiveEditor}\n`; + return ret; + } +} + +class DocumentAndEditorState { + + static compute(before: DocumentAndEditorState, after: DocumentAndEditorState): DocumentAndEditorStateDelta { + if (!before) { + return new DocumentAndEditorStateDelta([], after.documents, [], after.editors, undefined, after.activeEditor); + } + const documentDelta = delta(before.documents, after.documents, cmp.compareModels); + const editorDelta = delta(before.editors, after.editors, cmp.compareEditors); + const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined; + const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined; + + return new DocumentAndEditorStateDelta( + documentDelta.removed, documentDelta.added, + editorDelta.removed, editorDelta.added, + oldActiveEditor, newActiveEditor + ); + } + + constructor( + readonly documents: IModel[], + readonly editors: EditorAndModel[], + readonly activeEditor: string, + ) { + this.documents = documents.sort(cmp.compareModels); + this.editors = editors.sort(cmp.compareEditors); + } +} + +class MainThreadDocumentAndEditorStateComputer { + + private _toDispose: IDisposable[] = []; + private _toDisposeOnEditorRemove = new Map(); + private _onDidChangeState = new Emitter(); + private _currentState: DocumentAndEditorState; + + readonly onDidChangeState: Event = this._onDidChangeState.event; + + constructor( + @IModelService private _modelService: IModelService, + @ICodeEditorService private _codeEditorService: ICodeEditorService, + @IWorkbenchEditorService private _workbenchEditorService: IWorkbenchEditorService + ) { + this._modelService.onModelAdded(this._updateState, this, this._toDispose); + this._modelService.onModelRemoved(this._updateState, this, this._toDispose); + this._codeEditorService.onCodeEditorAdd(this._onDidAddEditor, this, this._toDispose); + this._codeEditorService.onCodeEditorRemove(this._onDidRemoveEditor, this, this._toDispose); + // this._updateState(); + } + + dispose(): void { + this._toDispose = dispose(this._toDispose); + } + + private _onDidAddEditor(e: ICommonCodeEditor): void { + const listener = any( + e.onDidChangeModel, + e.onDidFocusEditor, + e.onDidBlurEditor + )(this._updateState, this); + this._toDisposeOnEditorRemove.set(e.getId(), listener); + this._updateState(); + } + + private _onDidRemoveEditor(e: ICommonCodeEditor): void { + const sub = this._toDisposeOnEditorRemove.get(e.getId()); + if (sub) { + this._toDisposeOnEditorRemove.delete(e.getId()); + sub.dispose(); + this._updateState(); + } + } + + private _updateState(): void { + + // models: ignore too large models + const models = this._modelService.getModels(); + for (let i = 0; i < models.length; i++) { + if (models[i].isTooLargeForHavingARichMode()) { + models.splice(i, 1); + i--; + } + } + + // editor: only take those that have a not too large model + const editors: EditorAndModel[] = []; + let activeEditor: string = null; + + for (const editor of this._codeEditorService.listCodeEditors()) { + const model = editor.getModel(); + if (model && !model.isTooLargeForHavingARichMode() + && !model.isDisposed() // model disposed + && Boolean(this._modelService.getModel(model.uri)) // model disposing, the flag didn't flip yet but the model service already removed it + ) { + const apiEditor = new EditorAndModel(editor, model); + editors.push(apiEditor); + if (editor.isFocused()) { + activeEditor = apiEditor.id; + } + } + } + + // active editor: if none of the previous editors had focus we try + // to match the action workbench editor with one of editor we have + // just computed + if (!activeEditor) { + const workbenchEditor = this._workbenchEditorService.getActiveEditor(); + if (workbenchEditor) { + const workbenchEditorControl = workbenchEditor.getControl(); + let candidate: ICommonCodeEditor; + if (isCommonCodeEditor(workbenchEditorControl)) { + candidate = workbenchEditorControl; + } else if (isCommonDiffEditor(workbenchEditorControl)) { + candidate = workbenchEditorControl.getModifiedEditor(); + } + if (candidate) { + for (const { editor, id } of editors) { + if (candidate === editor) { + activeEditor = id; + break; + } + } + } + } + } + + // compute new state and compare against old + const newState = new DocumentAndEditorState(models, editors, activeEditor); + const delta = DocumentAndEditorState.compute(this._currentState, newState); + if (!delta.isEmpty) { + this._currentState = newState; + this._onDidChangeState.fire(delta); + } + } +} + +export class MainThreadDocumentsAndEditors { + + private _toDispose: IDisposable[]; + private _proxy: ExtHostDocumentsAndEditorsShape; + private _stateComputer: MainThreadDocumentAndEditorStateComputer; + private _editors = <{ [id: string]: MainThreadTextEditor }>Object.create(null); + + private _onTextEditorAdd = new Emitter(); + private _onTextEditorRemove = new Emitter(); + private _onDocumentAdd = new Emitter(); + private _onDocumentRemove = new Emitter(); + + readonly onTextEditorAdd: Event = this._onTextEditorAdd.event; + readonly onTextEditorRemove: Event = this._onTextEditorRemove.event; + readonly onDocumentAdd: Event = this._onDocumentAdd.event; + readonly onDocumentRemove: Event = this._onDocumentRemove.event; + + constructor( + @IModelService private _modelService: IModelService, + @ITextFileService private _textFileService: ITextFileService, + @IWorkbenchEditorService private _workbenchEditorService: IWorkbenchEditorService, + @IThreadService threadService: IThreadService, + @ICodeEditorService codeEditorService: ICodeEditorService, + ) { + this._proxy = threadService.get(ExtHostContext.ExtHostDocumentsAndEditors); + this._stateComputer = new MainThreadDocumentAndEditorStateComputer(_modelService, codeEditorService, _workbenchEditorService); + this._toDispose = [ + this._stateComputer, + this._stateComputer.onDidChangeState(this._onDelta, this) + ]; + } + + dispose(): void { + this._toDispose = dispose(this._toDispose); + } + + private _onDelta(delta: DocumentAndEditorStateDelta): void { + + let removedDocuments: string[]; + let removedEditors: string[] = []; + let addedEditors: MainThreadTextEditor[] = []; + + // removed models + removedDocuments = delta.removedDocuments.map(m => m.uri.toString()); + + // added editors + for (const apiEditor of delta.addedEditors) { + const mainThreadEditor = new MainThreadTextEditor(apiEditor.id, apiEditor.document, + apiEditor.editor, { onGainedFocus() { }, onLostFocus() { } }, this._modelService); + + this._editors[apiEditor.id] = mainThreadEditor; + addedEditors.push(mainThreadEditor); + } + + // removed editors + for (const { id } of delta.removedEditors) { + const mainThreadEditor = this._editors[id]; + if (mainThreadEditor) { + mainThreadEditor.dispose(); + delete this._editors[id]; + removedEditors.push(id); + } + } + + let extHostDelta: IDocumentsAndEditorsDelta = Object.create(null); + let empty = true; + if (delta.newActiveEditor !== undefined) { + empty = false; + extHostDelta.newActiveEditor = delta.newActiveEditor; + } + if (removedDocuments.length > 0) { + empty = false; + extHostDelta.removedDocuments = removedDocuments; + } + if (removedEditors.length > 0) { + empty = false; + extHostDelta.removedEditors = removedEditors; + } + if (delta.addedDocuments.length > 0) { + empty = false; + extHostDelta.addedDocuments = delta.addedDocuments.map(m => this._toModelAddData(m)); + } + if (delta.addedEditors.length > 0) { + empty = false; + extHostDelta.addedEditors = addedEditors.map(e => this._toTextEditorAddData(e)); + } + + if (!empty) { + // first update ext host + this._proxy.$acceptDocumentsAndEditorsDelta(extHostDelta); + // second update dependent state listener + this._onDocumentRemove.fire(removedDocuments); + this._onDocumentAdd.fire(delta.addedDocuments); + this._onTextEditorRemove.fire(removedEditors); + this._onTextEditorAdd.fire(addedEditors); + } + } + + private _toModelAddData(model: IModel): IModelAddedData { + return { + url: model.uri, + versionId: model.getVersionId(), + lines: model.getLinesContent(), + EOL: model.getEOL(), + modeId: model.getLanguageIdentifier().language, + isDirty: this._textFileService.isDirty(model.uri) + }; + } + + private _toTextEditorAddData(textEditor: MainThreadTextEditor): ITextEditorAddData { + return { + id: textEditor.getId(), + document: textEditor.getModel().uri, + options: textEditor.getConfiguration(), + selections: textEditor.getSelections(), + editorPosition: this._findEditorPosition(textEditor) + }; + } + + private _findEditorPosition(editor: MainThreadTextEditor): EditorPosition { + for (let workbenchEditor of this._workbenchEditorService.getVisibleEditors()) { + if (editor.matches(workbenchEditor)) { + return workbenchEditor.position; + } + } + return undefined; + } + + findTextEditorIdFor(editor: IEditor): string { + for (let id in this._editors) { + if (this._editors[id].matches(editor)) { + return id; + } + } + return undefined; + } + + getEditor(id: string): MainThreadTextEditor { + return this._editors[id]; + } +} diff --git a/src/vs/workbench/api/node/mainThreadEditorsTracker.ts b/src/vs/workbench/api/node/mainThreadEditor.ts similarity index 55% rename from src/vs/workbench/api/node/mainThreadEditorsTracker.ts rename to src/vs/workbench/api/node/mainThreadEditor.ts index cd4fef572a66fb4be0d3fbf5fe8d3b270eed8834..7d1548572bf80f6b47eb3936a51f993d9d6ff882 100644 --- a/src/vs/workbench/api/node/mainThreadEditorsTracker.ts +++ b/src/vs/workbench/api/node/mainThreadEditor.ts @@ -7,11 +7,8 @@ import EditorCommon = require('vs/editor/common/editorCommon'); import Event, { Emitter } from 'vs/base/common/event'; import { IEditor } from 'vs/platform/editor/common/editor'; -import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { IdGenerator } from 'vs/base/common/idGenerator'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { SnippetController } from 'vs/editor/contrib/snippet/common/snippetController'; @@ -131,6 +128,10 @@ export class MainThreadTextEditor { return this._model; } + public getCodeEditor(): EditorCommon.ICommonCodeEditor { + return this._codeEditor; + } + public hasCodeEditor(codeEditor: EditorCommon.ICommonCodeEditor): boolean { return (this._codeEditor === codeEditor); } @@ -413,324 +414,3 @@ export class MainThreadTextEditor { return true; } } - -/** - * Keeps track of what goes on in the main thread and maps models => text editors - */ -export class MainThreadEditorsTracker { - - private static _Ids = new IdGenerator(''); - - private _toDispose: IDisposable[]; - private _codeEditorService: ICodeEditorService; - private _modelService: IModelService; - private _updateMapping: RunOnceScheduler; - private _editorModelChangeListeners: { [editorId: string]: IDisposable; }; - - private _model2TextEditors: { - [modelUri: string]: MainThreadTextEditor[]; - }; - private _focusedTextEditorId: string; - private _visibleTextEditorIds: string[]; - private _onTextEditorAdd: Emitter; - private _onTextEditorRemove: Emitter; - private _onDidChangeFocusedTextEditor: Emitter; - private _onDidUpdateTextEditors: Emitter; - - private _focusTracker: IFocusTracker; - - constructor( - editorService: ICodeEditorService, - modelService: IModelService - ) { - this._codeEditorService = editorService; - this._modelService = modelService; - this._toDispose = []; - this._focusedTextEditorId = null; - this._visibleTextEditorIds = []; - this._editorModelChangeListeners = Object.create(null); - this._model2TextEditors = Object.create(null); - this._onTextEditorAdd = new Emitter(); - this._onTextEditorRemove = new Emitter(); - this._onDidUpdateTextEditors = new Emitter(); - this._onDidChangeFocusedTextEditor = new Emitter(); - this._focusTracker = { - onGainedFocus: () => this._updateFocusedTextEditor(), - onLostFocus: () => this._updateFocusedTextEditor() - }; - - this._modelService.onModelAdded(this._onModelAdded, this, this._toDispose); - this._modelService.onModelRemoved(this._onModelRemoved, this, this._toDispose); - - this._codeEditorService.onCodeEditorAdd(this._onCodeEditorAdd, this, this._toDispose); - this._codeEditorService.onCodeEditorRemove(this._onCodeEditorRemove, this, this._toDispose); - - this._updateMapping = new RunOnceScheduler(() => this._doUpdateMapping(), 0); - this._toDispose.push(this._updateMapping); - } - - public dispose(): void { - this._toDispose = dispose(this._toDispose); - } - - private _onModelAdded(model: EditorCommon.IModel): void { - this._updateMapping.schedule(); - } - - private _onModelRemoved(model: EditorCommon.IModel): void { - this._updateMapping.schedule(); - } - - private _onCodeEditorAdd(codeEditor: EditorCommon.ICommonCodeEditor): void { - this._editorModelChangeListeners[codeEditor.getId()] = codeEditor.onDidChangeModel(_ => this._updateMapping.schedule()); - this._updateMapping.schedule(); - } - - private _onCodeEditorRemove(codeEditor: EditorCommon.ICommonCodeEditor): void { - this._editorModelChangeListeners[codeEditor.getId()].dispose(); - delete this._editorModelChangeListeners[codeEditor.getId()]; - this._updateMapping.schedule(); - } - - private _doUpdateMapping(): void { - let allModels = this._modelService.getModels(); - // Same filter as in extHostDocuments - allModels = allModels.filter((model) => !model.isTooLargeForHavingARichMode()); - let allModelsMap: { [modelUri: string]: EditorCommon.IModel; } = Object.create(null); - allModels.forEach((model) => { - allModelsMap[model.uri.toString()] = model; - }); - - // Remove text editors for models that no longer exist - Object.keys(this._model2TextEditors).forEach((modelUri) => { - if (allModelsMap[modelUri]) { - // model still exists, will be updated below - return; - } - - let textEditorsToRemove = this._model2TextEditors[modelUri]; - delete this._model2TextEditors[modelUri]; - - for (let i = 0; i < textEditorsToRemove.length; i++) { - this._onTextEditorRemove.fire(textEditorsToRemove[i]); - textEditorsToRemove[i].dispose(); - } - }); - - // Handle all visible models - let visibleModels = this._getVisibleModels(); - Object.keys(visibleModels).forEach((modelUri) => { - let model = visibleModels[modelUri].model; - let codeEditors = visibleModels[modelUri].codeEditors; - - if (!this._model2TextEditors[modelUri]) { - this._model2TextEditors[modelUri] = []; - } - let existingTextEditors = this._model2TextEditors[modelUri]; - - // Remove text editors if more exist - while (existingTextEditors.length > codeEditors.length) { - let removedTextEditor = existingTextEditors.pop(); - this._onTextEditorRemove.fire(removedTextEditor); - removedTextEditor.dispose(); - } - - // Adjust remaining text editors - for (let i = 0; i < existingTextEditors.length; i++) { - existingTextEditors[i].setCodeEditor(codeEditors[i]); - } - - // Create new editors as needed - for (let i = existingTextEditors.length; i < codeEditors.length; i++) { - let newTextEditor = new MainThreadTextEditor(MainThreadEditorsTracker._Ids.nextId(), model, codeEditors[i], this._focusTracker, this._modelService); - existingTextEditors.push(newTextEditor); - this._onTextEditorAdd.fire(newTextEditor); - } - }); - - // Handle all not visible models - allModels.forEach((model) => { - let modelUri = model.uri.toString(); - - if (visibleModels[modelUri]) { - // model is visible, already handled above - return; - } - - if (!this._model2TextEditors[modelUri]) { - this._model2TextEditors[modelUri] = []; - } - let existingTextEditors = this._model2TextEditors[modelUri]; - - // Remove extra text editors - while (existingTextEditors.length > 1) { - let removedTextEditor = existingTextEditors.pop(); - this._onTextEditorRemove.fire(removedTextEditor); - removedTextEditor.dispose(); - } - - // Create new editor if needed or adjust it - if (existingTextEditors.length === 0) { - let newTextEditor = new MainThreadTextEditor(MainThreadEditorsTracker._Ids.nextId(), model, null, this._focusTracker, this._modelService); - existingTextEditors.push(newTextEditor); - this._onTextEditorAdd.fire(newTextEditor); - } else { - existingTextEditors[0].setCodeEditor(null); - } - }); - - this._printState(); - - this._visibleTextEditorIds = this._findVisibleTextEditorIds(); - - this._updateFocusedTextEditor(); - - // this is a sync event - this._onDidUpdateTextEditors.fire(undefined); - } - - private _updateFocusedTextEditor(): void { - this._setFocusedTextEditorId(this._findFocusedTextEditorId()); - } - - private _findFocusedTextEditorId(): string { - let modelUris = Object.keys(this._model2TextEditors); - for (let i = 0, len = modelUris.length; i < len; i++) { - let editors = this._model2TextEditors[modelUris[i]]; - for (let j = 0, lenJ = editors.length; j < lenJ; j++) { - if (editors[j].isFocused()) { - return editors[j].getId(); - } - } - } - - return null; - } - - private _findVisibleTextEditorIds(): string[] { - let result: string[] = []; - let modelUris = Object.keys(this._model2TextEditors); - for (let i = 0, len = modelUris.length; i < len; i++) { - let editors = this._model2TextEditors[modelUris[i]]; - for (let j = 0, lenJ = editors.length; j < lenJ; j++) { - if (editors[j].isVisible()) { - result.push(editors[j].getId()); - } - } - } - result.sort(); - return result; - } - - private _setFocusedTextEditorId(focusedTextEditorId: string): void { - if (this._focusedTextEditorId === focusedTextEditorId) { - // no change - return; - } - - this._focusedTextEditorId = focusedTextEditorId; - this._printState(); - this._onDidChangeFocusedTextEditor.fire(this._focusedTextEditorId); - } - - - private _printState(): void { - // console.log('----------------------'); - // Object.keys(this._model2TextEditors).forEach((modelUri) => { - // let editors = this._model2TextEditors[modelUri]; - - // console.log(editors.map((e) => { - // return e.getId() + " (" + (e.getId() === this._focusedTextEditorId ? 'FOCUSED, ': '') + modelUri + ")"; - // }).join('\n')); - // }); - } - - private _getVisibleModels(): IVisibleModels { - let r: IVisibleModels = {}; - - let allCodeEditors = this._codeEditorService.listCodeEditors(); - - // Maintain a certain sorting such that the mapping doesn't change too much all the time - allCodeEditors.sort((a, b) => strcmp(a.getId(), b.getId())); - - allCodeEditors.forEach((codeEditor) => { - let model = codeEditor.getModel(); - if (!model || model.isTooLargeForHavingARichMode() || !this._modelService.getModel(model.uri)) { - return; - } - - let modelUri = model.uri.toString(); - r[modelUri] = r[modelUri] || { - model: model, - codeEditors: [] - }; - r[modelUri].codeEditors.push(codeEditor); - }); - - return r; - } - - public getFocusedTextEditorId(): string { - return this._focusedTextEditorId; - } - - public getVisibleTextEditorIds(): string[] { - return this._visibleTextEditorIds; - } - - public get onTextEditorAdd(): Event { - return this._onTextEditorAdd.event; - } - - public get onTextEditorRemove(): Event { - return this._onTextEditorRemove.event; - } - - public get onDidUpdateTextEditors(): Event { - return this._onDidUpdateTextEditors.event; - } - - public get onChangedFocusedTextEditor(): Event { - return this._onDidChangeFocusedTextEditor.event; - } - - public findTextEditorIdFor(codeEditor: EditorCommon.ICommonCodeEditor): string { - let modelUris = Object.keys(this._model2TextEditors); - for (let i = 0, len = modelUris.length; i < len; i++) { - let editors = this._model2TextEditors[modelUris[i]]; - for (let j = 0, lenJ = editors.length; j < lenJ; j++) { - if (editors[j].hasCodeEditor(codeEditor)) { - return editors[j].getId(); - } - } - } - - return null; - } - - public registerTextEditorDecorationType(key: string, options: EditorCommon.IDecorationRenderOptions): void { - this._codeEditorService.registerDecorationType(key, options); - } - - public removeTextEditorDecorationType(key: string): void { - this._codeEditorService.removeDecorationType(key); - } -} - -interface IVisibleModels { - [modelUri: string]: { - model: EditorCommon.IModel; - codeEditors: EditorCommon.ICommonCodeEditor[]; - }; -} - -function strcmp(a: string, b: string): number { - if (a < b) { - return -1; - } - if (a > b) { - return 1; - } - return 0; -} - diff --git a/src/vs/workbench/api/node/mainThreadEditors.ts b/src/vs/workbench/api/node/mainThreadEditors.ts index 789d305644748cb61751ce7c83d9ddc8774df914..9c3833207ddf613b66109332ba13ffec9c744e5c 100644 --- a/src/vs/workbench/api/node/mainThreadEditors.ts +++ b/src/vs/workbench/api/node/mainThreadEditors.ts @@ -8,58 +8,47 @@ import URI from 'vs/base/common/uri'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; -import { ISingleEditOperation, ISelection, IRange, IEditor, EditorType, ICommonCodeEditor, ICommonDiffEditor, IDecorationRenderOptions, IDecorationOptions } from 'vs/editor/common/editorCommon'; +import { ISingleEditOperation, ISelection, IRange, IDecorationRenderOptions, IDecorationOptions } from 'vs/editor/common/editorCommon'; import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { Position as EditorPosition } from 'vs/platform/editor/common/editor'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { MainThreadEditorsTracker, TextEditorRevealType, MainThreadTextEditor, IApplyEditsOptions, IUndoStopOptions, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; +import { TextEditorRevealType, MainThreadTextEditor, IApplyEditsOptions, IUndoStopOptions, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditor'; +import { MainThreadDocumentsAndEditors } from './mainThreadDocumentsAndEditors'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { equals as arrayEquals } from 'vs/base/common/arrays'; import { equals as objectEquals } from 'vs/base/common/objects'; import { ExtHostContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextEditorPositionData } from './extHost.protocol'; export class MainThreadEditors extends MainThreadEditorsShape { private _proxy: ExtHostEditorsShape; + private _documentsAndEditors: MainThreadDocumentsAndEditors; 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( + documentsAndEditors: MainThreadDocumentsAndEditors, + @ICodeEditorService private _codeEditorService: ICodeEditorService, @IThreadService threadService: IThreadService, @IWorkbenchEditorService workbenchEditorService: IWorkbenchEditorService, @IEditorGroupService editorGroupService: IEditorGroupService, - @ITelemetryService telemetryService: ITelemetryService, - @ICodeEditorService editorService: ICodeEditorService, - @IModelService modelService: IModelService + @ITelemetryService telemetryService: ITelemetryService ) { super(); this._proxy = threadService.get(ExtHostContext.ExtHostEditors); + this._documentsAndEditors = documentsAndEditors; 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(documentsAndEditors.onTextEditorAdd(editors => editors.forEach(this._onTextEditorAdd, this))); + this._toDispose.push(documentsAndEditors.onTextEditorRemove(editors => editors.forEach(this._onTextEditorRemove, this))); - 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(editorGroupService.onEditorsChanged(() => this._updateActiveAndVisibleTextEditors())); this._toDispose.push(editorGroupService.onEditorsMoved(() => this._updateActiveAndVisibleTextEditors())); } @@ -81,37 +70,17 @@ export class MainThreadEditors extends MainThreadEditorsShape { toDispose.push(textEditor.onSelectionChanged((event) => { this._proxy.$acceptSelectionsChanged(id, event); })); - this._proxy.$acceptTextEditorAdd({ - id: id, - document: textEditor.getModel().uri, - 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(); + private _onTextEditorRemove(id: string): void { dispose(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)) { @@ -120,55 +89,12 @@ export class MainThreadEditors extends MainThreadEditorsShape { } } - 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; - } - } - return undefined; - } - 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; - } + const id = this._documentsAndEditors.findTextEditorIdFor(workbenchEditor); + if (id) { + result[id] = workbenchEditor.position; } } return result; @@ -187,43 +113,7 @@ export class MainThreadEditors extends MainThreadEditorsShape { if (!editor) { return undefined; } - - const findEditor = (): string => { - // 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; - } - } - return undefined; - }; - - const syncEditorId = findEditor(); - if (syncEditorId) { - return TPromise.as(syncEditorId); - } - - return new TPromise(resolve => { - // 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); - resolve(undefined); - } - subscription = this._editorTracker.onDidUpdateTextEditors(() => { - contd(); - }); - handle = setTimeout(() => { - contd(); - }, 1000); - - }).then(findEditor); + return this._documentsAndEditors.findTextEditorIdFor(editor); }); } @@ -231,7 +121,7 @@ export class MainThreadEditors extends MainThreadEditorsShape { // check how often this is used this._telemetryService.publicLog('api.deprecated', { function: 'TextEditor.show' }); - let mainThreadEditor = this._textEditorsMap[id]; + let mainThreadEditor = this._documentsAndEditors.getEditor(id); if (mainThreadEditor) { let model = mainThreadEditor.getModel(); return this._workbenchEditorService.openEditor({ @@ -246,7 +136,7 @@ export class MainThreadEditors extends MainThreadEditorsShape { // check how often this is used this._telemetryService.publicLog('api.deprecated', { function: 'TextEditor.hide' }); - let mainThreadEditor = this._textEditorsMap[id]; + let mainThreadEditor = this._documentsAndEditors.getEditor(id); if (mainThreadEditor) { let editors = this._workbenchEditorService.getVisibleEditors(); for (let editor of editors) { @@ -259,56 +149,57 @@ export class MainThreadEditors extends MainThreadEditorsShape { } $trySetSelections(id: string, selections: ISelection[]): TPromise { - if (!this._textEditorsMap[id]) { + if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError('TextEditor disposed'); } - this._textEditorsMap[id].setSelections(selections); + this._documentsAndEditors.getEditor(id).setSelections(selections); return TPromise.as(null); } $trySetDecorations(id: string, key: string, ranges: IDecorationOptions[]): TPromise { - if (!this._textEditorsMap[id]) { + if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError('TextEditor disposed'); } - this._textEditorsMap[id].setDecorations(key, ranges); + this._documentsAndEditors.getEditor(id).setDecorations(key, ranges); return TPromise.as(null); } $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise { - if (!this._textEditorsMap[id]) { + if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError('TextEditor disposed'); } - this._textEditorsMap[id].revealRange(range, revealType); + this._documentsAndEditors.getEditor(id).revealRange(range, revealType); return undefined; } $trySetOptions(id: string, options: ITextEditorConfigurationUpdate): TPromise { - if (!this._textEditorsMap[id]) { + if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError('TextEditor disposed'); } - this._textEditorsMap[id].setConfiguration(options); + this._documentsAndEditors.getEditor(id).setConfiguration(options); return TPromise.as(null); } $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): TPromise { - if (!this._textEditorsMap[id]) { + if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError('TextEditor disposed'); } - return TPromise.as(this._textEditorsMap[id].applyEdits(modelVersionId, edits, opts)); + return TPromise.as(this._documentsAndEditors.getEditor(id).applyEdits(modelVersionId, edits, opts)); } $tryInsertSnippet(id: string, template: string, ranges: IRange[], opts: IUndoStopOptions): TPromise { - if (!this._textEditorsMap[id]) { + if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError('TextEditor disposed'); } - return TPromise.as(this._textEditorsMap[id].insertSnippet(template, ranges, opts)); + return TPromise.as(this._documentsAndEditors.getEditor(id).insertSnippet(template, ranges, opts)); } $registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void { - this._editorTracker.registerTextEditorDecorationType(key, options); + this._codeEditorService.registerDecorationType(key, options); } $removeTextEditorDecorationType(key: string): void { - this._editorTracker.removeTextEditorDecorationType(key); + this._codeEditorService.removeDecorationType(key); + } } diff --git a/src/vs/workbench/test/node/api/extHostApiCommands.test.ts b/src/vs/workbench/test/node/api/extHostApiCommands.test.ts index 59333a5e5e59d5e074770a6dc49994aacc8dec9d..78be2e46df96fc53a9ed45f090ed3ac0c345b9c3 100644 --- a/src/vs/workbench/test/node/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/node/api/extHostApiCommands.test.ts @@ -27,6 +27,7 @@ import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; import { MainThreadCommands } from 'vs/workbench/api/node/mainThreadCommands'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { MainContext, ExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; import * as vscode from 'vscode'; @@ -68,7 +69,7 @@ suite('ExtHostLanguageFeatureCommands', function () { instantiationService.stub(ICommandService, { _serviceBrand: undefined, executeCommand(id, args): any { - let {handler} = CommandsRegistry.getCommands()[id]; + let { handler } = CommandsRegistry.getCommands()[id]; return TPromise.as(instantiationService.invokeFunction(handler, args)); } }); @@ -88,16 +89,19 @@ suite('ExtHostLanguageFeatureCommands', function () { getCreationOptions(): any { throw new Error(); } }); - const extHostDocuments = new ExtHostDocuments(threadService); - threadService.set(ExtHostContext.ExtHostDocuments, extHostDocuments); - extHostDocuments.$acceptModelAdd({ - isDirty: false, - versionId: model.getVersionId(), - modeId: model.getLanguageIdentifier().language, - url: model.uri, - lines: model.getValue().split(model.getEOL()), - EOL: model.getEOL(), + const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(threadService); + extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ + addedDocuments: [{ + isDirty: false, + versionId: model.getVersionId(), + modeId: model.getLanguageIdentifier().language, + url: model.uri, + lines: model.getValue().split(model.getEOL()), + EOL: model.getEOL(), + }] }); + const extHostDocuments = new ExtHostDocuments(threadService, extHostDocumentsAndEditors); + threadService.set(ExtHostContext.ExtHostDocuments, extHostDocuments); const heapService = new ExtHostHeapService(); diff --git a/src/vs/workbench/test/node/api/extHostDocuments.test.ts b/src/vs/workbench/test/node/api/extHostDocumentData.test.ts similarity index 98% rename from src/vs/workbench/test/node/api/extHostDocuments.test.ts rename to src/vs/workbench/test/node/api/extHostDocumentData.test.ts index 06ca8634be7da6a05c76c3afb7b6aff04c7bd22e..4ddad3fdace5abb4ad26e07916b06c3669dfb408 100644 --- a/src/vs/workbench/test/node/api/extHostDocuments.test.ts +++ b/src/vs/workbench/test/node/api/extHostDocumentData.test.ts @@ -7,13 +7,13 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; -import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocuments'; +import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocumentData'; import { Position } from 'vs/workbench/api/node/extHostTypes'; import { Range as CodeEditorRange } from 'vs/editor/common/core/range'; import * as EditorCommon from 'vs/editor/common/editorCommon'; -suite('ExtHostDocument', () => { +suite('ExtHostDocumentData', () => { let data: ExtHostDocumentData; @@ -87,7 +87,7 @@ suite('ExtHostDocument', () => { test('line, issue #5704', function () { let line = data.document.lineAt(0); - let {range, rangeIncludingLineBreak} = line; + let { range, rangeIncludingLineBreak } = line; assert.equal(range.end.line, 0); assert.equal(range.end.character, 16); assert.equal(rangeIncludingLineBreak.end.line, 1); @@ -243,7 +243,7 @@ enum AssertDocumentLineMappingDirection { PositionToOffset } -suite('ExtHostDocument updates line mapping', () => { +suite('ExtHostDocumentData updates line mapping', () => { function positionToStr(position: { line: number; character: number; }): string { return '(' + position.line + ',' + position.character + ')'; diff --git a/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts index 97eb4431fef97dd74676954b5d6f05d496378dfc..81aef369d8414d3ec46b44ad6680c17b11dc914b 100644 --- a/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts @@ -8,6 +8,7 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { TextDocumentSaveReason, TextEdit, Position } from 'vs/workbench/api/node/extHostTypes'; import { MainThreadWorkspaceShape } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; @@ -23,16 +24,18 @@ suite('ExtHostDocumentSaveParticipant', () => { let documents: ExtHostDocuments; setup(() => { - - documents = new ExtHostDocuments(OneGetThreadService(null)); - documents.$acceptModelAdd({ - isDirty: false, - modeId: 'foo', - url: resource, - versionId: 1, - lines: ['foo'], - EOL: '\n', - }); + const documentsAndEditors = new ExtHostDocumentsAndEditors(OneGetThreadService(null)); + documentsAndEditors.$acceptDocumentsAndEditorsDelta({ + addedDocuments: [{ + isDirty: false, + modeId: 'foo', + url: resource, + versionId: 1, + lines: ['foo'], + EOL: '\n', + }] + }); + documents = new ExtHostDocuments(OneGetThreadService(null), documentsAndEditors); }); test('no listeners, no problem', () => { @@ -305,7 +308,7 @@ suite('ExtHostDocumentSaveParticipant', () => { const participant = new ExtHostDocumentSaveParticipant(documents, new class extends MainThreadWorkspaceShape { $applyWorkspaceEdit(_edits: IResourceEdit[]) { - for (const {resource, newText, range} of _edits) { + for (const { resource, newText, range } of _edits) { documents.$acceptModelChanged(resource.toString(), [{ range, text: newText, diff --git a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts index f7cb6557aabb69c772e7baa971fa5488eac006a3..6f0040b2457940ffdce693e14d43ba83da0c8f5d 100644 --- a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts @@ -24,6 +24,7 @@ import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; import { MainThreadCommands } from 'vs/workbench/api/node/mainThreadCommands'; import { IHeapService } from 'vs/workbench/api/node/mainThreadHeapService'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/common/quickOpen'; import { DocumentSymbolProviderRegistry, DocumentHighlightKind } from 'vs/editor/common/modes'; import { getCodeLensData } from 'vs/editor/contrib/codelens/common/codelens'; @@ -80,16 +81,19 @@ suite('ExtHostLanguageFeatures', function () { originalErrorHandler = errorHandler.getUnexpectedErrorHandler(); setUnexpectedErrorHandler(() => { }); - const extHostDocuments = new ExtHostDocuments(threadService); - threadService.set(ExtHostContext.ExtHostDocuments, extHostDocuments); - extHostDocuments.$acceptModelAdd({ - isDirty: false, - versionId: model.getVersionId(), - modeId: model.getLanguageIdentifier().language, - url: model.uri, - lines: model.getValue().split(model.getEOL()), - EOL: model.getEOL(), + const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(threadService); + extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ + addedDocuments: [{ + isDirty: false, + versionId: model.getVersionId(), + modeId: model.getLanguageIdentifier().language, + url: model.uri, + lines: model.getValue().split(model.getEOL()), + EOL: model.getEOL(), + }] }); + const extHostDocuments = new ExtHostDocuments(threadService, extHostDocumentsAndEditors); + threadService.set(ExtHostContext.ExtHostDocuments, extHostDocuments); const heapService = new ExtHostHeapService(); diff --git a/src/vs/workbench/test/node/api/extHostEditors.test.ts b/src/vs/workbench/test/node/api/extHostTextEditor.test.ts similarity index 99% rename from src/vs/workbench/test/node/api/extHostEditors.test.ts rename to src/vs/workbench/test/node/api/extHostTextEditor.test.ts index 05490a666f95fa5d5d3992b84c2009c5fe3b1dc8..8e2148999b589ccb26cf9c95ba006fbbb0d47804 100644 --- a/src/vs/workbench/test/node/api/extHostEditors.test.ts +++ b/src/vs/workbench/test/node/api/extHostTextEditor.test.ts @@ -8,9 +8,9 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; import { TextEditorLineNumbersStyle } from 'vs/workbench/api/node/extHostTypes'; import { TextEditorCursorStyle } from 'vs/editor/common/editorCommon'; -import { IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; +import { IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditor'; import { MainThreadEditorsShape } from 'vs/workbench/api/node/extHost.protocol'; -import { ExtHostTextEditorOptions } from 'vs/workbench/api/node/extHostEditors'; +import { ExtHostTextEditorOptions } from 'vs/workbench/api/node/extHostTextEditor'; suite('ExtHostTextEditorOptions', () => { diff --git a/src/vs/workbench/test/node/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/node/api/mainThreadDocumentsAndEditors.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e73484f08e9b7f7e7ad68b1acd5ce178f3180bb --- /dev/null +++ b/src/vs/workbench/test/node/api/mainThreadDocumentsAndEditors.test.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/node/mainThreadDocumentsAndEditors'; +import { OneGetThreadService } from './testThreadService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { MockCodeEditorService } from 'vs/editor/test/common/mocks/mockCodeEditorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta } from 'vs/workbench/api/node/extHost.protocol'; +import { mockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; + +suite('MainThreadDocumentsAndEditors', () => { + + let modelService: ModelServiceImpl; + let codeEditorService: MockCodeEditorService; + let textFileService: ITextFileService; + let workbenchEditorService: IWorkbenchEditorService; + let documentAndEditor: MainThreadDocumentsAndEditors; + let deltas: IDocumentsAndEditorsDelta[] = []; + + setup(() => { + deltas.length = 0; + modelService = new ModelServiceImpl(null, new TestConfigurationService()); + codeEditorService = new MockCodeEditorService(); + textFileService = { isDirty() { return false; } }; + workbenchEditorService = { + getVisibleEditors() { return []; }, + getActiveEditor() { return undefined; } + }; + + documentAndEditor = new MainThreadDocumentsAndEditors( + modelService, textFileService, workbenchEditorService, + OneGetThreadService(new class extends ExtHostDocumentsAndEditorsShape { + $acceptDocumentsAndEditorsDelta(delta) { deltas.push(delta); } + }), codeEditorService + ); + }); + + + test('Model#add', () => { + + modelService.createModel('farboo', null, null); + + assert.equal(deltas.length, 1); + const [delta] = deltas; + + assert.equal(delta.addedDocuments.length, 1); + assert.equal(delta.removedDocuments, undefined); + assert.equal(delta.addedEditors, undefined); + assert.equal(delta.removedEditors, undefined); + assert.equal(delta.newActiveEditor, null); + }); + + test('ignore huge model', () => { + + const data = new Array(2 + (5 * 1024 * 1024)).join('!'); + const model = modelService.createModel(data, null, null); + assert.ok(model.isTooLargeForHavingARichMode()); + + assert.equal(deltas.length, 1); + const [delta] = deltas; + assert.equal(delta.newActiveEditor, null); + assert.equal(delta.addedDocuments, undefined); + assert.equal(delta.removedDocuments, undefined); + assert.equal(delta.addedEditors, undefined); + assert.equal(delta.removedEditors, undefined); + }); + + test('ignore huge model from editor', () => { + + const data = new Array(2 + (5 * 1024 * 1024)).join('!'); + const model = modelService.createModel(data, null, null); + const editor = mockCodeEditor(null, { model }); + + assert.equal(deltas.length, 1); + deltas.length = 0; + codeEditorService.addCodeEditor(editor); + assert.equal(deltas.length, 0); + }); + + test('ignore editor w/o model', () => { + const editor = mockCodeEditor([], {}); + editor.setModel(null); + codeEditorService.addCodeEditor(editor); + assert.equal(deltas.length, 1); + const [delta] = deltas; + assert.equal(delta.newActiveEditor, null); + assert.equal(delta.addedDocuments, undefined); + assert.equal(delta.removedDocuments, undefined); + assert.equal(delta.addedEditors, undefined); + assert.equal(delta.removedEditors, undefined); + }); + + test('editor with model', () => { + const model = modelService.createModel('farboo', null, null); + codeEditorService.addCodeEditor(mockCodeEditor(null, { model })); + + assert.equal(deltas.length, 2); + const [first, second] = deltas; + assert.equal(first.addedDocuments.length, 1); + assert.equal(first.newActiveEditor, null); + assert.equal(first.removedDocuments, undefined); + assert.equal(first.addedEditors, undefined); + assert.equal(first.removedEditors, undefined); + + assert.equal(second.addedEditors.length, 1); + assert.equal(second.addedDocuments, undefined); + assert.equal(second.removedDocuments, undefined); + assert.equal(second.removedEditors, undefined); + assert.equal(typeof second.newActiveEditor, 'string'); + }); + + test('editor with dispos-ed/-ing model', () => { + modelService.createModel('foobar', null, null); + const model = modelService.createModel('farboo', null, null); + const editor = mockCodeEditor(null, { model }); + codeEditorService.addCodeEditor(editor); + + // ignore things until now + deltas.length = 0; + + modelService.destroyModel(model.uri); + assert.equal(deltas.length, 1); + const [first] = deltas; + assert.equal(first.newActiveEditor, null); + assert.equal(first.removedEditors.length, 1); + assert.equal(first.removedDocuments.length, 1); + assert.equal(first.addedDocuments, undefined); + assert.equal(first.addedEditors, undefined); + }); +});