diff --git a/.eslintrc.json b/.eslintrc.json index c39a66311e4fc..d7f317c85b9f0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -755,7 +755,8 @@ "vs/base/~", "vs/base/parts/*/~", "vs/platform/*/~", - "vs/editor/~" + "vs/editor/~", + "@vscode/tree-sitter-wasm" // node module allowed even in /common/ ] }, { diff --git a/build/.webignore b/build/.webignore index 15935edce8a61..860ab59616b2b 100644 --- a/build/.webignore +++ b/build/.webignore @@ -38,6 +38,7 @@ vscode-textmate/webpack.config.js # This makes sure the model is included in the package !@vscode/vscode-languagedetection/model/** +!@vscode/tree-sitter-wasm/wasm/** # Ensure only the required telemetry pieces are loaded in web to reduce bundle size @microsoft/1ds-core-js/** diff --git a/package.json b/package.json index e26ad20fb06a5..d1713efd7eb71 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", "@vscode/sudo-prompt": "9.3.1", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", diff --git a/remote/package.json b/remote/package.json index 48b849c5928e0..460258a2bf22e 100644 --- a/remote/package.json +++ b/remote/package.json @@ -11,6 +11,7 @@ "@vscode/proxy-agent": "^0.22.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", diff --git a/remote/web/package.json b/remote/web/package.json index 7d7ca2fa12aae..06c3256114e99 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -6,6 +6,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "0.2.0-beta.34", "@xterm/addon-image": "0.9.0-beta.51", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index d0dae067240b4..3800e9c019615 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -43,6 +43,11 @@ resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== +"@vscode/tree-sitter-wasm@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.1.tgz#ffb2e295a416698f4c77cbffeca3b28567d6754b" + integrity sha512-m0GKnQ3BxWnVd+20KLGwr1+Qvt/RiiaJmKAqHNU35pNydDtduUzyBm7ETz/T0vOVKoeIAaiYsJOA1aKWs7Y1tA== + "@vscode/vscode-languagedetection@1.0.21": version "1.0.21" resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" diff --git a/remote/yarn.lock b/remote/yarn.lock index 38f916a284ed1..7cff852686962 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -98,6 +98,11 @@ mkdirp "^1.0.4" node-addon-api "7.1.0" +"@vscode/tree-sitter-wasm@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.1.tgz#ffb2e295a416698f4c77cbffeca3b28567d6754b" + integrity sha512-m0GKnQ3BxWnVd+20KLGwr1+Qvt/RiiaJmKAqHNU35pNydDtduUzyBm7ETz/T0vOVKoeIAaiYsJOA1aKWs7Y1tA== + "@vscode/vscode-languagedetection@1.0.21": version "1.0.21" resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 59ddc3fdfbf38..ccc437c304f40 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -197,6 +197,7 @@ const isESM = false; // using a fallback such as node.js require which does not exist in sandbox const baseNodeModulesPath = isDev ? '../node_modules' : '../node_modules.asar'; loaderConfig.paths = { + '@vscode/tree-sitter-wasm': `${baseNodeModulesPath}/@vscode/tree-sitter-wasm/wasm/tree-sitter.js`, 'vscode-textmate': `${baseNodeModulesPath}/vscode-textmate/release/main.js`, 'vscode-oniguruma': `${baseNodeModulesPath}/vscode-oniguruma/release/main.js`, 'vsda': `${baseNodeModulesPath}/vsda/index.js`, diff --git a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts new file mode 100644 index 0000000000000..7b1b17c613357 --- /dev/null +++ b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts @@ -0,0 +1,347 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TreeSitterTokenizationRegistry } from 'vs/editor/common/languages'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { AppResourcePath, FileAccess, nodeModulesPath } from 'vs/base/common/network'; +import { ITreeSitterParserService } from 'vs/editor/common/services/treeSitterParserService'; +import { IModelService } from 'vs/editor/common/services/model'; +import { Disposable, DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ITextModel } from 'vs/editor/common/model'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { setTimeout0 } from 'vs/base/common/platform'; +import { importAMDNodeModule } from 'vs/amdX'; +import { Event } from 'vs/base/common/event'; +import { cancelOnDispose } from 'vs/base/common/cancellation'; + +const EDITOR_EXPERIMENTAL_PREFER_TREESITTER = 'editor.experimental.preferTreeSitter'; +const moduleLocationTreeSitter: AppResourcePath = `${nodeModulesPath}/@vscode/tree-sitter-wasm/wasm`; +const moduleLocationTreeSitterWasm: AppResourcePath = `${moduleLocationTreeSitter}/tree-sitter.wasm`; + +export class TextModelTreeSitter extends Disposable { + private _treeSitterTree: TreeSitterTree | undefined; + + // Not currently used since we just get telemetry, but later this will be needed. + get tree() { return this._treeSitterTree; } + + constructor(readonly model: ITextModel, + private readonly _treeSitterParser: TreeSitterLanguages, + private readonly _treeSitterImporter: TreeSitterImporter, + private readonly _logService: ILogService, + private readonly _telemetryService: ITelemetryService + ) { + super(); + this._register(Event.runAndSubscribe(this.model.onDidChangeLanguage, (e => this._onDidChangeLanguage(e ? e.newLanguage : this.model.getLanguageId())))); + } + + private readonly _languageSessionDisposables = this._register(new DisposableStore()); + /** + * Be very careful when making changes to this method as it is easy to introduce race conditions. + */ + private async _onDidChangeLanguage(languageId: string) { + this._languageSessionDisposables.clear(); + this._treeSitterTree = undefined; + + const token = cancelOnDispose(this._languageSessionDisposables); + const language = await this._treeSitterParser.getLanguage(languageId); + if (!language || token.isCancellationRequested) { + return; + } + + const Parser = await this._treeSitterImporter.getParserClass(); + if (token.isCancellationRequested) { + return; + } + + const treeSitterTree = this._languageSessionDisposables.add(new TreeSitterTree(new Parser(), language, this._logService, this._telemetryService)); + this._languageSessionDisposables.add(this.model.onDidChangeContent(e => this._onDidChangeContent(treeSitterTree, e))); + await this._onDidChangeContent(treeSitterTree); + if (token.isCancellationRequested) { + return; + } + + this._treeSitterTree = treeSitterTree; + } + + private async _onDidChangeContent(treeSitterTree: TreeSitterTree, e?: IModelContentChangedEvent) { + return treeSitterTree.onDidChangeContent(this.model, e); + } +} + +export class TreeSitterTree implements IDisposable { + private _tree: Parser.Tree | undefined; + private _isDisposed: boolean = false; + constructor(public readonly parser: Parser, + public /** exposed for tests **/ readonly language: Parser.Language, + private readonly _logService: ILogService, + private readonly _telemetryService: ITelemetryService) { + this.parser.setTimeoutMicros(50 * 1000); // 50 ms + this.parser.setLanguage(language); + } + dispose(): void { + this._isDisposed = true; + this._tree?.delete(); + this.parser?.delete(); + } + get tree() { return this._tree; } + set tree(newTree: Parser.Tree | undefined) { + this._tree?.delete(); + this._tree = newTree; + } + get isDisposed() { return this._isDisposed; } + + private _onDidChangeContentQueue: Promise = Promise.resolve(); + public async onDidChangeContent(model: ITextModel, e?: IModelContentChangedEvent) { + this._onDidChangeContentQueue = this._onDidChangeContentQueue.then(() => { + if (this.isDisposed) { + // No need to continue the queue if we are disposed + return; + } + return this._onDidChangeContent(model, e); + }).catch((e) => { + this._logService.error('Error parsing tree-sitter tree', e); + }); + return this._onDidChangeContentQueue; + } + + private async _onDidChangeContent(model: ITextModel, e?: IModelContentChangedEvent) { + if (e) { + for (const change of e.changes) { + const newEndOffset = change.rangeOffset + change.text.length; + const newEndPosition = model.getPositionAt(newEndOffset); + + this.tree?.edit({ + startIndex: change.rangeOffset, + oldEndIndex: change.rangeOffset + change.rangeLength, + newEndIndex: change.rangeOffset + change.text.length, + startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, + oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, + newEndPosition: { row: newEndPosition.lineNumber - 1, column: newEndPosition.column - 1 } + }); + } + } + + this.tree = await this.parse(model); + } + + private parse(model: ITextModel): Promise { + let telemetryTag: string; + if (this.tree) { + telemetryTag = 'incrementalParse'; + } else { + telemetryTag = 'fullParse'; + } + return this._parseAndYield(model, telemetryTag); + } + + private async _parseAndYield(model: ITextModel, telemetryTag: string): Promise { + const language = model.getLanguageId(); + let tree: Parser.Tree | undefined; + let time: number = 0; + let passes: number = 0; + do { + const timer = performance.now(); + try { + tree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this.tree); + } catch (e) { + // parsing can fail when the timeout is reached, will resume upon next loop + } finally { + time += performance.now() - timer; + passes++; + } + + // Even if the model changes and edits are applied, the tree parsing will continue correctly after the await. + await new Promise(resolve => setTimeout0(resolve)); + + if (model.isDisposed() || this.isDisposed) { + return; + } + } while (!tree); + this.sendParseTimeTelemetry(telemetryTag, language, time, passes); + return tree; + } + + private _parseCallback(textModel: ITextModel, index: number): string | null { + return textModel.getTextBuffer().getNearestChunk(index); + } + + private sendParseTimeTelemetry(eventName: string, languageId: string, time: number, passes: number): void { + this._logService.debug(`Tree parsing (${eventName}) took ${time} ms and ${passes} passes.`); + type ParseTimeClassification = { + owner: 'alros'; + comment: 'Used to understand how long it takes to parse a tree-sitter tree'; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The programming language ID.' }; + time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ms it took to parse' }; + passes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of passes it took to parse' }; + }; + this._telemetryService.publicLog2<{ languageId: string; time: number; passes: number }, ParseTimeClassification>(`treeSitter.${eventName}`, { languageId, time, passes }); + } +} + +export class TreeSitterLanguages extends Disposable { + private _languages: Map = new Map(); + + constructor(private readonly _treeSitterImporter: TreeSitterImporter, + private readonly _fileService: IFileService + ) { + super(); + } + + public async getLanguage(languageId: string): Promise { + let language = this._languages.get(languageId); + if (!language) { + language = await this._fetchLanguage(languageId); + if (!language) { + return undefined; + } + this._languages.set(languageId, language); + } + return language; + } + + private async _fetchLanguage(languageId: string): Promise { + const grammarName = TreeSitterTokenizationRegistry.get(languageId); + const languageLocation = this._getLanguageLocation(languageId); + if (!grammarName || !languageLocation) { + return undefined; + } + const wasmPath: AppResourcePath = `${languageLocation}/${grammarName.name}.wasm`; + const languageFile = await (this._fileService.readFile(FileAccess.asFileUri(wasmPath))); + const Parser = await this._treeSitterImporter.getParserClass(); + return Parser.Language.load(languageFile.value.buffer); + } + + private _getLanguageLocation(languageId: string): AppResourcePath | undefined { + const grammarName = TreeSitterTokenizationRegistry.get(languageId); + if (!grammarName) { + return undefined; + } + return moduleLocationTreeSitter; + } +} + +export class TreeSitterImporter { + private _treeSitterImport: typeof import('@vscode/tree-sitter-wasm') | undefined; + private async _getTreeSitterImport() { + if (!this._treeSitterImport) { + this._treeSitterImport = await importAMDNodeModule('@vscode/tree-sitter-wasm', 'wasm/tree-sitter.js'); + } + return this._treeSitterImport; + } + + private _parserClass: typeof Parser | undefined; + public async getParserClass() { + if (!this._parserClass) { + this._parserClass = (await this._getTreeSitterImport()).Parser; + } + return this._parserClass; + } +} + +export class TreeSitterTextModelService extends Disposable implements ITreeSitterParserService { + readonly _serviceBrand: undefined; + private _init!: Promise; + private _textModelTreeSitters: DisposableMap = this._register(new DisposableMap()); + private _registeredLanguages: DisposableMap = this._register(new DisposableMap()); + private readonly _treeSitterImporter: TreeSitterImporter = new TreeSitterImporter(); + private readonly _treeSitterParser: TreeSitterLanguages; + + constructor(@IModelService private readonly _modelService: IModelService, + @IFileService fileService: IFileService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + this._treeSitterParser = this._register(new TreeSitterLanguages(this._treeSitterImporter, fileService)); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(EDITOR_EXPERIMENTAL_PREFER_TREESITTER)) { + this._supportedLanguagesChanged(); + } + })); + this._supportedLanguagesChanged(); + } + + private async _doInitParser() { + const Parser = await this._treeSitterImporter.getParserClass(); + await Parser.init({ + locateFile(_file: string, _folder: string) { + return FileAccess.asBrowserUri(moduleLocationTreeSitterWasm).toString(true); + } + }); + return true; + } + + private _hasInit: boolean = false; + private async _initParser(hasLanguages: boolean): Promise { + if (this._hasInit) { + return this._init; + } + + if (hasLanguages) { + this._hasInit = true; + this._init = this._doInitParser(); + + // New init, we need to deal with all the existing text models and set up listeners + this._init.then(() => this._registerModelServiceListeners()); + } else { + this._init = Promise.resolve(false); + } + return this._init; + } + + private async _supportedLanguagesChanged() { + const setting = this._getSetting(); + + let hasLanguages = true; + if (setting.length === 0) { + hasLanguages = false; + } + + if (await this._initParser(hasLanguages)) { + // Eventually, this should actually use an extension point to add tree sitter grammars, but for now they are hard coded in core + if (setting.includes('typescript')) { + this._addGrammar('typescript', 'tree-sitter-typescript'); + } else { + this._removeGrammar('typescript'); + } + } + } + + private _getSetting(): string[] { + return this._configurationService.getValue(EDITOR_EXPERIMENTAL_PREFER_TREESITTER) || []; + } + + private async _registerModelServiceListeners() { + this._register(this._modelService.onModelAdded(model => { + this._createTextModelTreeSitter(model); + })); + this._register(this._modelService.onModelRemoved(model => { + this._textModelTreeSitters.deleteAndDispose(model); + })); + this._modelService.getModels().forEach(model => this._createTextModelTreeSitter(model)); + } + + private _createTextModelTreeSitter(model: ITextModel) { + const textModelTreeSitter = new TextModelTreeSitter(model, this._treeSitterParser, this._treeSitterImporter, this._logService, this._telemetryService); + this._textModelTreeSitters.set(model, textModelTreeSitter); + } + + private _addGrammar(languageId: string, grammarName: string) { + if (!TreeSitterTokenizationRegistry.get(languageId)) { + this._registeredLanguages.set(languageId, TreeSitterTokenizationRegistry.register(languageId, { name: grammarName })); + } + } + + private _removeGrammar(languageId: string) { + if (this._registeredLanguages.has(languageId)) { + this._registeredLanguages.deleteAndDispose('typescript'); + } + } +} diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 72fd97338eca3..ad1c39e3fa5ff 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -82,6 +82,14 @@ export class EncodedTokenizationResult { } } +/** + * An intermediate interface for scaffolding the new tree sitter tokenization support. Not final. + * @internal + */ +export interface ITreeSitterTokenizationSupport { + name: string; +} + /** * @internal */ @@ -2106,14 +2114,14 @@ export interface ITokenizationSupportChangedEvent { /** * @internal */ -export interface ILazyTokenizationSupport { - get tokenizationSupport(): Promise; +export interface ILazyTokenizationSupport { + get tokenizationSupport(): Promise; } /** * @internal */ -export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSupport { +export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSupport { private _tokenizationSupport: Promise | null = null; constructor(private readonly createSupport: () => Promise) { @@ -2140,7 +2148,7 @@ export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSu /** * @internal */ -export interface ITokenizationRegistry { +export interface ITokenizationRegistry { /** * An event triggered when: @@ -2158,24 +2166,24 @@ export interface ITokenizationRegistry { /** * Register a tokenization support. */ - register(languageId: string, support: ITokenizationSupport): IDisposable; + register(languageId: string, support: TSupport): IDisposable; /** * Register a tokenization support factory. */ - registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable; + registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable; /** * Get or create the tokenization support for a language. * Returns `null` if not found. */ - getOrCreate(languageId: string): Promise; + getOrCreate(languageId: string): Promise; /** * Get the tokenization support for a language. * Returns `null` if not found. */ - get(languageId: string): ITokenizationSupport | null; + get(languageId: string): TSupport | null; /** * Returns false if a factory is still pending. @@ -2195,8 +2203,12 @@ export interface ITokenizationRegistry { /** * @internal */ -export const TokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); +export const TokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); +/** + * @internal + */ +export const TreeSitterTokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); /** * @internal diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 97b5a483fc350..04f76ee800bd8 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -43,6 +43,7 @@ import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPai import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; @@ -299,6 +300,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @ILanguageService private readonly _languageService: ILanguageService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -327,13 +329,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._bracketPairs = this._register(new BracketPairsTextModelPart(this, this._languageConfigurationService)); this._guidesTextModelPart = this._register(new GuidesTextModelPart(this, this._languageConfigurationService)); this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this)); - this._tokenizationTextModelPart = new TokenizationTextModelPart( - this._languageService, - this._languageConfigurationService, + this._tokenizationTextModelPart = this.instantiationService.createInstance(TokenizationTextModelPart, this, this._bracketPairs, languageId, - this._attachedViews, + this._attachedViews ); const bufferLineCount = this._buffer.getLineCount(); diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 40c6c921afc34..30972f1a33bde 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -47,12 +47,12 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz private readonly grammarTokens = this._register(new GrammarTokens(this._languageService.languageIdCodec, this._textModel, () => this._languageId, this._attachedViews)); constructor( - private readonly _languageService: ILanguageService, - private readonly _languageConfigurationService: ILanguageConfigurationService, private readonly _textModel: TextModel, private readonly _bracketPairsTextModelPart: BracketPairsTextModelPart, private _languageId: string, private readonly _attachedViews: AttachedViews, + @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(); diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index 2bbda14a026a3..00019d6f96197 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -14,7 +14,7 @@ import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; import { IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageSelection } from 'vs/editor/common/languages/language'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -23,7 +23,7 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { equals } from 'vs/base/common/objects'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -107,8 +107,7 @@ export class ModelService extends Disposable implements IModelService { @IConfigurationService private readonly _configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, - @ILanguageService private readonly _languageService: ILanguageService, - @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._modelCreationOptionsByLanguageAndResource = Object.create(null); @@ -314,14 +313,11 @@ export class ModelService extends Disposable implements IModelService { private _createModelData(value: string | ITextBufferFactory, languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { // create & save the model const options = this.getCreationOptions(languageIdOrSelection, resource, isForSimpleWidget); - const model: TextModel = new TextModel( + const model: TextModel = this._instantiationService.createInstance(TextModel, value, languageIdOrSelection, options, - resource, - this._undoRedoService, - this._languageService, - this._languageConfigurationService, + resource ); if (resource && this._disposedModels.has(MODEL_ID(resource))) { const disposedModelData = this._removeDisposedModel(resource)!; diff --git a/src/vs/editor/common/services/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitterParserService.ts new file mode 100644 index 0000000000000..e3e911efc497f --- /dev/null +++ b/src/vs/editor/common/services/treeSitterParserService.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ITreeSitterParserService = createDecorator('treeSitterParserService'); + +/** + * Currently this service just logs telemetry about how long it takes to parse files. + * Actual API will come later as we add features like syntax highlighting. + */ +export interface ITreeSitterParserService { + readonly _serviceBrand: undefined; +} diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 58c720ac87c09..7d63afec8e883 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -55,6 +55,9 @@ export interface IModelContentChange { * An event describing a change in the text of a model. */ export interface IModelContentChangedEvent { + /** + * The changes are ordered from the end of the document to the beginning, so they should be safe to apply in sequence. + */ readonly changes: IModelContentChange[]; /** * The (new) end-of-line character. diff --git a/src/vs/editor/common/tokenizationRegistry.ts b/src/vs/editor/common/tokenizationRegistry.ts index d9fb1bba82f2b..15ad1b8515974 100644 --- a/src/vs/editor/common/tokenizationRegistry.ts +++ b/src/vs/editor/common/tokenizationRegistry.ts @@ -6,13 +6,13 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent, ILazyTokenizationSupport } from 'vs/editor/common/languages'; +import { ITokenizationRegistry, ITokenizationSupportChangedEvent, ILazyTokenizationSupport } from 'vs/editor/common/languages'; import { ColorId } from 'vs/editor/common/encodedTokenAttributes'; -export class TokenizationRegistry implements ITokenizationRegistry { +export class TokenizationRegistry implements ITokenizationRegistry { - private readonly _tokenizationSupports = new Map(); - private readonly _factories = new Map(); + private readonly _tokenizationSupports = new Map(); + private readonly _factories = new Map>(); private readonly _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; @@ -30,7 +30,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public register(languageId: string, support: ITokenizationSupport): IDisposable { + public register(languageId: string, support: TSupport): IDisposable { this._tokenizationSupports.set(languageId, support); this.handleChange([languageId]); return toDisposable(() => { @@ -42,11 +42,11 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public get(languageId: string): ITokenizationSupport | null { + public get(languageId: string): TSupport | null { return this._tokenizationSupports.get(languageId) || null; } - public registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable { + public registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable { this._factories.get(languageId)?.dispose(); const myData = new TokenizationSupportFactoryData(this, languageId, factory); this._factories.set(languageId, myData); @@ -60,7 +60,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public async getOrCreate(languageId: string): Promise { + public async getOrCreate(languageId: string): Promise { // check first if the support is already set const tokenizationSupport = this.get(languageId); if (tokenizationSupport) { @@ -112,7 +112,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { } } -class TokenizationSupportFactoryData extends Disposable { +class TokenizationSupportFactoryData extends Disposable { private _isDisposed: boolean = false; private _resolvePromise: Promise | null = null; @@ -123,9 +123,9 @@ class TokenizationSupportFactoryData extends Disposable { } constructor( - private readonly _registry: TokenizationRegistry, + private readonly _registry: TokenizationRegistry, private readonly _languageId: string, - private readonly _factory: ILazyTokenizationSupport, + private readonly _factory: ILazyTokenizationSupport, ) { super(); } diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index 0941671756d7e..6503b7df22079 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -23,9 +23,7 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/languages'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, IdentityProvider, OneReferenceRenderer, StringRepresentationProvider, TreeElement } from 'vs/editor/contrib/gotoSymbol/browser/peek/referencesTree'; import * as peekView from 'vs/editor/contrib/peekView/browser/peekView'; @@ -35,7 +33,6 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkbenchAsyncDataTreeOptions, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { FileReferences, OneReference, ReferencesModel } from '../referencesModel'; class DecorationsManager implements IDisposable { @@ -224,10 +221,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { @IInstantiationService private readonly _instantiationService: IInstantiationService, @peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService, @ILabelService private readonly _uriLabel: ILabelService, - @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ILanguageService private readonly _languageService: ILanguageService, - @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, supportOnTitleClick: true }, _instantiationService); @@ -315,7 +309,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { }; this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, {}, this.editor); dom.hide(this._previewContainer); - this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null, this._undoRedoService, this._languageService, this._languageConfigurationService); + this._previewNotAvailableMessage = this._instantiationService.createInstance(TextModel, nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null); // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts index 86d3bf5d62dca..9eb8d2f000694 100644 --- a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts @@ -14,6 +14,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { Range } from 'vs/editor/common/core/range'; import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ITextModel } from 'vs/editor/common/model'; import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -29,6 +30,7 @@ import { TestTextResourcePropertiesService } from 'vs/editor/test/common/service import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NullLogService } from 'vs/platform/log/common/log'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { ColorScheme } from 'vs/platform/theme/common/theme'; @@ -50,12 +52,14 @@ suite('ModelSemanticColoring', () => { languageFeaturesService = new LanguageFeaturesService(); languageService = disposables.add(new LanguageService(false)); const semanticTokensStylingService = disposables.add(new SemanticTokensStylingService(themeService, logService, languageService)); + const instantiationService = new TestInstantiationService(); + instantiationService.set(ILanguageService, languageService); + instantiationService.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); modelService = disposables.add(new ModelService( configService, new TestTextResourcePropertiesService(configService), new UndoRedoService(new TestDialogService(), new TestNotificationService()), - languageService, - new TestLanguageConfigurationService(), + instantiationService )); const envService = new class extends mock() { override isBuilt: boolean = true; diff --git a/src/vs/editor/test/browser/services/testTreeSitterService.ts b/src/vs/editor/test/browser/services/testTreeSitterService.ts new file mode 100644 index 0000000000000..f449962d6cd06 --- /dev/null +++ b/src/vs/editor/test/browser/services/testTreeSitterService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AppResourcePath } from 'vs/base/common/network'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { ITextModel } from 'vs/editor/common/model'; +import { ITreeSitterParserService } from 'vs/editor/common/services/treeSitterParserService'; + +export class TestTreeSitterParserService implements ITreeSitterParserService { + getLanguage(model: ITextModel): Parser.Language | undefined { + throw new Error('Method not implemented.'); + } + getLanguageLocation(languageId: string): AppResourcePath { + throw new Error('Method not implemented.'); + } + readonly _serviceBrand: undefined; + + public initTreeSitter(): Promise { + return Promise.resolve(); + } + + public getTree(_model: ITextModel): Parser.Tree | undefined { + return undefined; + } +} diff --git a/src/vs/editor/test/browser/services/treeSitterParserService.test.ts b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts new file mode 100644 index 0000000000000..be1b58185ad25 --- /dev/null +++ b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TextModelTreeSitter, TreeSitterImporter, TreeSitterLanguages } from 'vs/editor/browser/services/treeSitter/treeSitterParserService'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { timeout } from 'vs/base/common/async'; +import { ConsoleMainLogger, ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { LogService } from 'vs/platform/log/common/logService'; +import { mock } from 'vs/base/test/common/mock'; + +class MockParser implements Parser { + static async init(): Promise { } + delete(): void { } + parse(input: string | Parser.Input, oldTree?: Parser.Tree, options?: Parser.Options): Parser.Tree { + return new MockTree(); + } + getIncludedRanges(): Parser.Range[] { + return []; + } + getTimeoutMicros(): number { return 0; } + setTimeoutMicros(timeout: number): void { } + reset(): void { } + getLanguage(): Parser.Language { return {} as any; } + setLanguage(): void { } + getLogger(): Parser.Logger { + throw new Error('Method not implemented.'); + } + setLogger(logFunc?: Parser.Logger | false | null): void { + throw new Error('Method not implemented.'); + } +} + +class MockTreeSitterImporter extends TreeSitterImporter { + public override async getParserClass(): Promise { + return MockParser as any; + } +} + +class MockTree implements Parser.Tree { + editorLanguage: string = ''; + editorContents: string = ''; + rootNode: Parser.SyntaxNode = {} as any; + rootNodeWithOffset(offsetBytes: number, offsetExtent: Parser.Point): Parser.SyntaxNode { + throw new Error('Method not implemented.'); + } + copy(): Parser.Tree { + throw new Error('Method not implemented.'); + } + delete(): void { } + edit(edit: Parser.Edit): Parser.Tree { + return this; + } + walk(): Parser.TreeCursor { + throw new Error('Method not implemented.'); + } + getChangedRanges(other: Parser.Tree): Parser.Range[] { + throw new Error('Method not implemented.'); + } + getIncludedRanges(): Parser.Range[] { + throw new Error('Method not implemented.'); + } + getEditedRange(other: Parser.Tree): Parser.Range { + throw new Error('Method not implemented.'); + } + getLanguage(): Parser.Language { + throw new Error('Method not implemented.'); + } +} + +class MockLanguage implements Parser.Language { + version: number = 0; + fieldCount: number = 0; + stateCount: number = 0; + nodeTypeCount: number = 0; + fieldNameForId(fieldId: number): string | null { + throw new Error('Method not implemented.'); + } + fieldIdForName(fieldName: string): number | null { + throw new Error('Method not implemented.'); + } + idForNodeType(type: string, named: boolean): number { + throw new Error('Method not implemented.'); + } + nodeTypeForId(typeId: number): string | null { + throw new Error('Method not implemented.'); + } + nodeTypeIsNamed(typeId: number): boolean { + throw new Error('Method not implemented.'); + } + nodeTypeIsVisible(typeId: number): boolean { + throw new Error('Method not implemented.'); + } + nextState(stateId: number, typeId: number): number { + throw new Error('Method not implemented.'); + } + query(source: string): Parser.Query { + throw new Error('Method not implemented.'); + } + lookaheadIterator(stateId: number): Parser.LookaheadIterable | null { + throw new Error('Method not implemented.'); + } + languageId: string = ''; +} + +suite('TreeSitterParserService', function () { + const treeSitterImporter: TreeSitterImporter = new MockTreeSitterImporter(); + let logService: ILogService; + let telemetryService: ITelemetryService; + setup(function () { + logService = new LogService(new ConsoleMainLogger()); + telemetryService = new class extends mock() { + override async publicLog2() { + // + } + }; + }); + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('TextModelTreeSitter race condition: first language is slow to load', async function () { + class MockTreeSitterParser extends TreeSitterLanguages { + public override async getLanguage(languageId: string): Promise { + if (languageId === 'javascript') { + await timeout(200); + } + const language = new MockLanguage(); + language.languageId = languageId; + return language; + } + } + + const treeSitterParser: TreeSitterLanguages = store.add(new MockTreeSitterParser(treeSitterImporter, {} as any)); + const textModel = store.add(createTextModel('console.log("Hello, world!");', 'javascript')); + const textModelTreeSitter = store.add(new TextModelTreeSitter(textModel, treeSitterParser, treeSitterImporter, logService, telemetryService)); + textModel.setLanguage('typescript'); + await timeout(300); + assert.strictEqual((textModelTreeSitter.tree?.language as MockLanguage).languageId, 'typescript'); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 584faab47accf..eaceca0bd2997 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2919,6 +2919,9 @@ declare namespace monaco.editor { * An event describing a change in the text of a model. */ export interface IModelContentChangedEvent { + /** + * The changes are ordered from the end of the document to the beginning, so they should be safe to apply in sequence. + */ readonly changes: IModelContentChange[]; /** * The (new) end-of-line character. diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index f0d0538ce226f..17a9049d0643b 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -27,11 +27,15 @@ import { TestTextResourcePropertiesService, TestWorkingCopyFileService } from 'v import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { LanguageService } from 'vs/editor/common/services/languageService'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; suite('MainThreadDocumentsAndEditors', () => { @@ -61,12 +65,15 @@ suite('MainThreadDocumentsAndEditors', () => { const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); const themeService = new TestThemeService(); + const instantiationService = new TestInstantiationService(); + instantiationService.set(ILanguageService, disposables.add(new LanguageService())); + instantiationService.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); + instantiationService.set(IUndoRedoService, undoRedoService); modelService = new ModelService( configService, new TestTextResourcePropertiesService(configService), undoRedoService, - disposables.add(new LanguageService()), - new TestLanguageConfigurationService(), + instantiationService ); codeEditorService = new TestCodeEditorService(themeService); textFileService = new class extends mock() { diff --git a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts index 1ca165c1b5fcb..d83a5ebcf42b4 100644 --- a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts @@ -16,12 +16,10 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { LanguageService } from 'vs/editor/common/services/languageService'; import { IModelService } from 'vs/editor/common/services/model'; import { ModelService } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -57,6 +55,10 @@ import { ICopyOperation, ICreateFileOperation, ICreateOperation, IDeleteOperatio import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestFileService, TestLifecycleService, TestWorkingCopyService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestContextService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('MainThreadEditors', () => { @@ -80,19 +82,11 @@ suite('MainThreadEditors', () => { createdResources.clear(); deletedResources.clear(); - const configService = new TestConfigurationService(); const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); const themeService = new TestThemeService(); - modelService = new ModelService( - configService, - new TestTextResourcePropertiesService(configService), - undoRedoService, - disposables.add(new LanguageService()), - new TestLanguageConfigurationService(), - ); const services = new ServiceCollection(); services.set(IBulkEditService, new SyncDescriptor(BulkEditService)); @@ -178,8 +172,18 @@ suite('MainThreadEditors', () => { } }); + services.set(ILanguageService, disposables.add(new LanguageService())); + services.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); + const instaService = new InstantiationService(services); + modelService = new ModelService( + configService, + new TestTextResourcePropertiesService(configService), + undoRedoService, + instaService + ); + bulkEdits = instaService.createInstance(MainThreadBulkEdits, SingleProxyRPCProtocol(null)); }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index e45a95008c3ff..45a97a0c67a22 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -25,13 +25,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { AriaRole } from 'vs/base/browser/ui/aria/aria'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; // --- VIEW MODEL @@ -202,9 +200,7 @@ export class BulkEditDataSource implements IAsyncDataSource