From 6c1558699442337a54d97d9509ae818d57665699 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 23 Jul 2024 14:59:04 +0200 Subject: [PATCH 1/5] Add telemtry for how long it takes to parse files with tree-sitter (#213565) * Make space for tree sitter * Add the tree sitter wasm file * Very naive tree-sitter syntax highlighting for html, with a layer breaker * Update tree when content changes * WIP for making abstract tokens class * Handle theme changes * Replace entire text model value with parse callback * Perf improvements * Add tree-sitter-typescript * Add typescript + better initial parsing * Refactor into tree parsing service and fix flaw in parse callback * Remove things that aren't the parser service * Add yielding * Remove changes that aren't required for PR * Remove more file changes * Reduce yield to 50 ms * Fix incremental parsing * Try update node-abi * Revert "Try update node-abi" This reverts commit df28801e31e3d672a0754eb9edc55a7208d7cde8. * Update text buffer chunk api * fix build * Remove tree-sitter dependency * Adopt new, as yet unpublished, `@vscode/tree-sitter-wasm` package * Use published `@vscode/tree-sitter-wasm` package * Break `TreeSitterTree` and `TreeSitterParserService` into better pieces and: - document the order of editor changes - use service injection where `TextModel` is constructed * Fix tests * Remove unneeded import * Fix missing tree-sitter-wasm in web and remote * Make package.jsons match * Add @vscode/tree-sitter-wasm to web loader config * Try using importAMDNodeModule * PR feedback * Add race condition test for changing language while loading language * Use same timeout * Queue content changes * Remove override dispose * Move queue into TreeSitterTree --------- Co-authored-by: Peng Lyu --- .eslintrc.json | 3 +- build/.webignore | 1 + package.json | 1 + remote/package.json | 1 + remote/web/package.json | 1 + remote/web/yarn.lock | 5 + remote/yarn.lock | 5 + src/bootstrap-window.js | 1 + .../treeSitter/treeSitterParserService.ts | 347 ++++++++++++++++++ src/vs/editor/common/languages.ts | 30 +- src/vs/editor/common/model/textModel.ts | 8 +- .../common/model/tokenizationTextModelPart.ts | 4 +- src/vs/editor/common/services/modelService.ts | 14 +- .../services/treeSitterParserService.ts | 16 + src/vs/editor/common/textModelEvents.ts | 3 + src/vs/editor/common/tokenizationRegistry.ts | 22 +- .../browser/peek/referencesWidget.ts | 8 +- .../browser/documentSemanticTokens.test.ts | 8 +- .../browser/services/testTreeSitterService.ts | 27 ++ .../services/treeSitterParserService.test.ts | 144 ++++++++ src/vs/monaco.d.ts | 3 + .../mainThreadDocumentsAndEditors.test.ts | 15 +- .../test/browser/mainThreadEditors.test.ts | 24 +- .../bulkEdit/browser/preview/bulkEditTree.ts | 10 +- .../services/model/common/modelService.ts | 8 +- ...eSitterTokenizationFeature.contribution.ts | 25 ++ src/vs/workbench/workbench.common.main.ts | 1 + test/unit/browser/renderer.html | 1 + yarn.lock | 5 + 29 files changed, 670 insertions(+), 71 deletions(-) create mode 100644 src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts create mode 100644 src/vs/editor/common/services/treeSitterParserService.ts create mode 100644 src/vs/editor/test/browser/services/testTreeSitterService.ts create mode 100644 src/vs/editor/test/browser/services/treeSitterParserService.test.ts create mode 100644 src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts diff --git a/.eslintrc.json b/.eslintrc.json index c39a66311e4..d7f317c85b9 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 15935edce8a..860ab59616b 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 cc8cbfb8fb7..4c00a91cce8 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 48b849c5928..460258a2bf2 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 7d7ca2fa12a..06c3256114e 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 d0dae067240..3800e9c0196 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 38f916a284e..7cff8526869 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 59ddc3fdfbf..ccc437c304f 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 00000000000..7b1b17c6133 --- /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 72fd97338ec..ad1c39e3fa5 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 97b5a483fc3..04f76ee800b 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 40c6c921afc..30972f1a33b 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 2bbda14a026..00019d6f961 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 00000000000..e3e911efc49 --- /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 58c720ac87c..7d63afec8e8 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 d9fb1bba82f..15ad1b85159 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 0941671756d..6503b7df220 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 86d3bf5d62d..9eb8d2f0006 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 00000000000..f449962d6cd --- /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 00000000000..be1b58185ad --- /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 24cb17d259c..be2c930e9eb 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 f0d0538ce22..17a9049d064 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 1ca165c1b5f..d83a5ebcf42 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 e45a95008c3..45a97a0c67a 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 Date: Tue, 23 Jul 2024 10:02:53 -0500 Subject: [PATCH 2/5] Add customizations.codespaces.disableAutomaticConfiguration to the devcontainer schema (#223083) --- .../schemas/devContainer.codespaces.schema.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json b/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json index 681ca6105cf..3f8400a7bd4 100644 --- a/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json @@ -180,6 +180,11 @@ "items": { "type": "string" } + }, + "disableAutomaticConfiguration": { + "type": "boolean", + "description": "Disables the setup that is automatically run in a codespace if no `postCreateCommand` is specified.", + "default": false } } } From d4bb523d43261a3ec2c4c9ca01052faeb0a874e7 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 23 Jul 2024 12:19:43 -0700 Subject: [PATCH 3/5] Fix a few more type assertions (#223138) For #211878 --- src/vs/base/common/types.ts | 10 +++++++ .../electron-main/utilityProcess.ts | 11 +++---- .../workbench/api/common/extHost.api.impl.ts | 8 ++--- .../api/common/extHostTypeConverters.ts | 30 +++++++++---------- .../test/browser/workbenchTestServices.ts | 9 ++---- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 1acab57b758..6025a90c711 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -190,6 +190,16 @@ export function validateConstraint(arg: unknown, constraint: TypeConstraint | un } } +/** + * Helper type assertion that safely upcasts a type to a supertype. + * + * This can be used to make sure the argument correctly conforms to the subtype while still being able to pass it + * to contexts that expects the supertype. + */ +export function upcast(x: Sub): Base { + return x; +} + type AddFirstParameterToFunction = T extends (...args: any[]) => TargetFunctionsReturnType ? // Function: add param to function (firstArg: FirstParameter, ...args: Parameters) => ReturnType : diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 3b817404c49..3b1cee5f90c 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -18,6 +18,7 @@ import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { deepClone } from 'vs/base/common/objects'; import { isWindows } from 'vs/base/common/platform'; import { isUNCAccessRestrictionsDisabled, getUNCHostAllowlist } from 'vs/base/node/unc'; +import { upcast } from 'vs/base/common/types'; export interface IUtilityProcessConfiguration { @@ -249,7 +250,10 @@ export class UtilityProcess extends Disposable { this.log('creating new...', Severity.Info); // Fork utility process - this.process = utilityProcess.fork(modulePath, args, { + this.process = utilityProcess.fork(modulePath, args, upcast({ serviceName, env, execArgv, @@ -257,10 +261,7 @@ export class UtilityProcess extends Disposable { forceAllocationsToV8Sandbox, respondToAuthRequestsFromMainProcess, stdio - } as ForkOptions & { - forceAllocationsToV8Sandbox?: Boolean; - respondToAuthRequestsFromMainProcess?: boolean; - }); + })); // Register to events this.registerListeners(this.process, this.configuration, serviceName); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 910f5251ab7..60c35821086 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -7,7 +7,7 @@ import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable } from 'vs/base/common/lifecycle'; import { Schemas, matchesScheme } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; @@ -1206,17 +1206,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, registerFileSearchProviderNew: (scheme: string, provider: vscode.FileSearchProviderNew) => { checkProposedApiEnabled(extension, 'fileSearchProviderNew'); - return { dispose: () => { } }; + return { dispose: () => { } }; }, registerTextSearchProviderNew: (scheme: string, provider: vscode.TextSearchProviderNew) => { checkProposedApiEnabled(extension, 'textSearchProviderNew'); - return { dispose: () => { } }; + return { dispose: () => { } }; }, registerAITextSearchProviderNew: (scheme: string, provider: vscode.AITextSearchProviderNew) => { // there are some dependencies on textSearchProvider, so we need to check for both checkProposedApiEnabled(extension, 'aiTextSearchProviderNew'); checkProposedApiEnabled(extension, 'textSearchProviderNew'); - return { dispose: () => { } }; + return { dispose: () => { } }; }, registerRemoteAuthorityResolver: (authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver) => { checkProposedApiEnabled(extension, 'resolvers'); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 92989936141..6a81c0db675 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -787,7 +787,7 @@ export namespace SymbolTag { export namespace WorkspaceSymbol { export function from(info: vscode.SymbolInformation): search.IWorkspaceSymbol { - return { + return { name: info.name, kind: SymbolKind.from(info.kind), tags: info.tags && info.tags.map(SymbolTag.from), @@ -966,7 +966,7 @@ export namespace Hover { export namespace EvaluatableExpression { export function from(expression: vscode.EvaluatableExpression): languages.EvaluatableExpression { - return { + return { range: Range.from(expression.range), expression: expression.expression }; @@ -980,24 +980,24 @@ export namespace EvaluatableExpression { export namespace InlineValue { export function from(inlineValue: vscode.InlineValue): languages.InlineValue { if (inlineValue instanceof types.InlineValueText) { - return { + return { type: 'text', range: Range.from(inlineValue.range), text: inlineValue.text - }; + } satisfies languages.InlineValueText; } else if (inlineValue instanceof types.InlineValueVariableLookup) { - return { + return { type: 'variable', range: Range.from(inlineValue.range), variableName: inlineValue.variableName, caseSensitiveLookup: inlineValue.caseSensitiveLookup - }; + } satisfies languages.InlineValueVariableLookup; } else if (inlineValue instanceof types.InlineValueEvaluatableExpression) { - return { + return { type: 'expression', range: Range.from(inlineValue.range), expression: inlineValue.expression - }; + } satisfies languages.InlineValueExpression; } else { throw new Error(`Unknown 'InlineValue' type`); } @@ -1006,28 +1006,28 @@ export namespace InlineValue { export function to(inlineValue: languages.InlineValue): vscode.InlineValue { switch (inlineValue.type) { case 'text': - return { + return { range: Range.to(inlineValue.range), text: inlineValue.text - }; + } satisfies vscode.InlineValueText; case 'variable': - return { + return { range: Range.to(inlineValue.range), variableName: inlineValue.variableName, caseSensitiveLookup: inlineValue.caseSensitiveLookup - }; + } satisfies vscode.InlineValueVariableLookup; case 'expression': - return { + return { range: Range.to(inlineValue.range), expression: inlineValue.expression - }; + } satisfies vscode.InlineValueEvaluatableExpression; } } } export namespace InlineValueContext { export function from(inlineValueContext: vscode.InlineValueContext): extHostProtocol.IInlineValueContextDto { - return { + return { frameId: inlineValueContext.frameId, stoppedLocation: Range.from(inlineValueContext.stoppedLocation) }; diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 65d93898567..32bb3da3a3c 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -123,7 +123,7 @@ import { IEnterWorkspaceResult, IRecent, IRecentlyOpened, IWorkspaceFolderCreati import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { IExtensionTerminalProfile, IShellLaunchConfig, ITerminalBackend, ITerminalLogService, ITerminalProfile, TerminalIcon, TerminalLocation, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { ICreateTerminalOptions, IDeserializedTerminalEditorInput, ITerminalConfigurationService, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined, upcast } from 'vs/base/common/types'; import { IRegisterContributedProfileArgs, IShellLaunchConfigResolveOptions, ITerminalProfileProvider, ITerminalProfileResolverService, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import { EditorResolverService } from 'vs/workbench/services/editor/browser/editorResolverService'; import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; @@ -211,12 +211,7 @@ export class TestTextFileEditor extends TextFileEditor { } setSelection(selection: Selection | undefined, reason: EditorPaneSelectionChangeReason): void { - if (selection) { - const options: ITextEditorOptions = { selection }; - this._options = options; - } else { - this._options = undefined; - } + this._options = selection ? upcast({ selection }) : undefined; this._onDidChangeSelection.fire({ reason }); } From ea0d7033cfe986280343abfdbea80f402b6391e4 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 23 Jul 2024 15:12:34 -0700 Subject: [PATCH 4/5] Properly gate type acquisition on web (#223403) For #221299 Make sure we don't register the file system at all in these cases --- .../src/extension.browser.ts | 16 ++-------- .../src/filesystems/ata.ts | 30 +++++++++++++++++++ .../src/tsServer/serverProcess.browser.ts | 8 ++--- 3 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 extensions/typescript-language-features/src/filesystems/ata.ts diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 5fe6b8651ec..5f575253f3d 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -11,8 +11,7 @@ import { registerBaseCommands } from './commands/index'; import { TypeScriptServiceConfiguration } from './configuration/configuration'; import { BrowserServiceConfigurationProvider } from './configuration/configuration.browser'; import { ExperimentationTelemetryReporter, IExperimentationTelemetryReporter } from './experimentTelemetryReporter'; -import { AutoInstallerFs } from './filesystems/autoInstallerFs'; -import { MemFs } from './filesystems/memFs'; +import { registerAtaSupport } from './filesystems/ata'; import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost'; import { Logger } from './logging/logger'; import RemoteRepositories from './remoteRepositories.browser'; @@ -25,7 +24,7 @@ import { ITypeScriptVersionProvider, TypeScriptVersion, TypeScriptVersionSource import { ActiveJsTsEditorTracker } from './ui/activeJsTsEditorTracker'; import { Disposable } from './utils/dispose'; import { getPackageInfo } from './utils/packageInfo'; -import { isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform'; +import { isWebAndHasSharedArrayBuffers } from './utils/platform'; class StaticVersionProvider implements ITypeScriptVersionProvider { @@ -102,16 +101,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { await startPreloadWorkspaceContentsIfNeeded(context, logger); })); - if (supportsReadableByteStreams()) { - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), { - isCaseSensitive: true, - isReadonly: false - })); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), { - isCaseSensitive: true, - isReadonly: false - })); - } + context.subscriptions.push(registerAtaSupport()); return getExtensionApi(onCompletionAccepted.event, pluginManager); } diff --git a/extensions/typescript-language-features/src/filesystems/ata.ts b/extensions/typescript-language-features/src/filesystems/ata.ts new file mode 100644 index 00000000000..731ad303529 --- /dev/null +++ b/extensions/typescript-language-features/src/filesystems/ata.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { conditionalRegistration, requireGlobalConfiguration } from '../languageFeatures/util/dependentRegistration'; +import { supportsReadableByteStreams } from '../utils/platform'; +import { AutoInstallerFs } from './autoInstallerFs'; +import { MemFs } from './memFs'; + +export function registerAtaSupport(): vscode.Disposable { + if (!supportsReadableByteStreams()) { + return vscode.Disposable.from(); + } + + return conditionalRegistration([ + requireGlobalConfiguration('typescript', 'tsserver.web.typeAcquisition.enabled'), + ], () => { + return vscode.Disposable.from( + vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), { + isCaseSensitive: true, + isReadonly: false + }), + vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), { + isCaseSensitive: true, + isReadonly: false + })); + }); +} diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts index 71daf1fb0b6..5adf1866112 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -40,7 +40,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { version: TypeScriptVersion, args: readonly string[], kind: TsServerProcessKind, - _configuration: TypeScriptServiceConfiguration, + configuration: TypeScriptServiceConfiguration, _versionManager: TypeScriptVersionManager, _nodeVersionManager: NodeVersionManager, tsServerLog: TsServerLog | undefined, @@ -50,10 +50,10 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { ...args, // Explicitly give TS Server its path so it can load local resources '--executingFilePath', tsServerPath, + // Enable/disable web type acquisition + (configuration.webTypeAcquisitionEnabled && supportsReadableByteStreams() ? '--experimentalTypeAcquisition' : '--disableAutomaticTypingAcquisition'), ]; - if (_configuration.webTypeAcquisitionEnabled && supportsReadableByteStreams()) { - launchArgs.push('--experimentalTypeAcquisition'); - } + return new WorkerServerProcess(kind, tsServerPath, this._extensionUri, launchArgs, tsServerLog, this._logger); } } From ead2adc1c3902695d606ef7e65f983a9f7ec3b29 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 23 Jul 2024 15:42:22 -0700 Subject: [PATCH 5/5] fix: enable Attach Selection to Chat for untitled files (#223413) --- .../chat/browser/actions/chatContextActions.ts | 14 +++++++------- .../contrib/chat/browser/chatVariables.ts | 3 +-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 30d401ab408..bf136eb5dc6 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -7,15 +7,18 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas } from 'vs/base/common/network'; -import { IRange } from 'vs/editor/common/core/range'; +import { compare } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { IRange } from 'vs/editor/common/core/range'; +import { EditorType } from 'vs/editor/common/editorCommon'; import { Command } from 'vs/editor/common/languages'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess'; import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -27,13 +30,10 @@ import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_QUICK_CHAT } f import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorType } from 'vs/editor/common/editorCommon'; -import { compare } from 'vs/base/common/strings'; -import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; export function registerChatContextActions() { registerAction2(AttachContextAction); @@ -107,7 +107,7 @@ class AttachFileAction extends Action2 { const textEditorService = accessor.get(IEditorService); const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { variablesService.attachContext('file', activeUri, ChatAgentLocation.Panel); } } @@ -132,7 +132,7 @@ class AttachSelectionAction extends Action2 { const activeEditor = textEditorService.activeTextEditorControl; const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { const selection = activeEditor?.getSelection(); if (selection) { variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 87eceece34f..24b5d888c6c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -164,8 +164,7 @@ export class ChatVariablesService implements IChatVariablesService { return; } - await showChatView(this.viewsService); - const widget = this.chatWidgetService.lastFocusedWidget; + const widget = await showChatView(this.viewsService); if (!widget || !widget.viewModel) { return; }