diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index c2117e0e2256b997f9bc95ba697b786af7dc52ae..2c33be00d33fbb22504abff89005e42012602f96 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -460,6 +460,11 @@ export interface CompletionItem { * A command that should be run upon acceptance of this item. */ command?: Command; + + /** + * @internal + */ + [key: string]: any; } export interface CompletionList { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 1c5df46f1c02633eac957a2244622b742a5242c5..3a2af95b37138e42e430879c2089ed8fed3f8f1e 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -10,8 +10,8 @@ import * as modes from 'vs/editor/common/modes'; import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; -import { Range as EditorRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, CodeInsetDto, LinkDto, CallHierarchyDto } from '../common/extHost.protocol'; +import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, CodeInsetDto, LinkDto, CallHierarchyDto, SuggestDataDto } from '../common/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -22,6 +22,7 @@ import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { IHeapService } from 'vs/workbench/services/heap/common/heap'; +import { mixin } from 'vs/base/common/objects'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -359,8 +360,29 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- suggest + private static _inflateSuggestDto(defaultRange: IRange, data: SuggestDataDto): modes.CompletionItem { + return { + label: data.a, + kind: data.b, + detail: data.c, + documentation: data.d, + sortText: data.e, + filterText: data.f, + preselect: data.g, + insertText: data.h || data.a, + insertTextRules: data.i, + range: data.j || defaultRange, + commitCharacters: data.k, + additionalTextEdits: data.l, + command: data.m, + // not-standard + _id: data.x, + _pid: data.y + }; + } + $registerSuggestSupport(handle: number, selector: ISerializedDocumentFilter[], triggerCharacters: string[], supportsResolveDetails: boolean): void { - this._registrations[handle] = modes.CompletionProviderRegistry.register(selector, { + const provider: modes.CompletionItemProvider = { triggerCharacters, provideCompletionItems: (model: ITextModel, position: EditorPosition, context: modes.CompletionContext, token: CancellationToken): Promise => { return this._proxy.$provideCompletionItems(handle, model.uri, position, context, token).then(result => { @@ -368,20 +390,25 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return result; } return { - suggestions: result.suggestions, - incomplete: result.incomplete, - dispose: () => { - if (typeof result._id === 'number') { - this._proxy.$releaseCompletionItems(handle, result._id); - } - } + suggestions: result.b.map(d => MainThreadLanguageFeatures._inflateSuggestDto(result.a, d)), + incomplete: result.c, + dispose: () => this._proxy.$releaseCompletionItems(handle, result.x) }; }); - }, - resolveCompletionItem: supportsResolveDetails - ? (model, position, suggestion, token) => this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion, token) - : undefined - }); + } + }; + if (supportsResolveDetails) { + provider.resolveCompletionItem = (model, position, suggestion, token) => { + return this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion._id, suggestion._pid, token).then(result => { + if (!result) { + return suggestion; + } + let newSuggestion = MainThreadLanguageFeatures._inflateSuggestDto(suggestion.range, result); + return mixin(suggestion, newSuggestion, true); + }); + }; + } + this._registrations[handle] = modes.CompletionProviderRegistry.register(selector, provider); } // --- parameter hints diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2590c31ace20f1f326bde133c834d5d9a20d72f7..f7b3e1dfedb8d579fc060d3d6e57bdfab4e70e76 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -854,14 +854,30 @@ export class IdObject { } } -export interface SuggestionDto extends modes.CompletionItem { - _id: number; - _parentId: number; -} - -export interface SuggestResultDto extends IdObject { - suggestions: SuggestionDto[]; - incomplete?: boolean; +export interface SuggestDataDto { + a/* label */: string; + b/* kind */: modes.CompletionItemKind; + c/* detail */?: string; + d/* documentation */?: string | IMarkdownString; + e/* sortText */?: string; + f/* filterText */?: string; + g/* preselect */?: boolean; + h/* insertText */?: string; + i/* insertTextRules */?: modes.CompletionItemInsertTextRule; + j/* range */?: IRange; + k/* commitCharacters */?: string[]; + l/* additionalTextEdits */?: ISingleEditOperation[]; + m/* command */?: modes.Command; + // not-standard + x: number; + y: number; +} + +export interface SuggestResultDto { + x: number; + a: IRange; + b: SuggestDataDto[]; + c?: boolean; } export interface LocationDto { @@ -982,7 +998,7 @@ export interface ExtHostLanguageFeaturesShape { $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string, token: CancellationToken): Promise; $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise; - $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.CompletionItem, token: CancellationToken): Promise; + $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, id: number, pid: number, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise; $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 328f1b2b02c743f37833ac4230b2e910c8a39c8b..7fcaa73f015a8331fb51205be9e3a1b61ea5c5f0 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -15,7 +15,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; import { asPromise } from 'vs/base/common/async'; -import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, SuggestionDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, MainThreadWebviewsShape, CodeInsetDto } from '../common/extHost.protocol'; +import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, MainThreadWebviewsShape, CodeInsetDto, SuggestDataDto } from '../common/extHost.protocol'; import { regExpLeadsToEndlessLoop, regExpFlags } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; @@ -637,15 +637,18 @@ class SuggestAdapter { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); - return asPromise( - () => this._provider.provideCompletionItems(doc, pos, token, typeConvert.CompletionContext.to(context)) - ).then(value => { + return asPromise(() => this._provider.provideCompletionItems(doc, pos, token, typeConvert.CompletionContext.to(context))).then(value => { const _id = this._idPool++; + // the default text edit range + const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)) + .with({ end: pos }); + const result: SuggestResultDto = { - _id, - suggestions: [], + x: _id, + b: [], + a: typeConvert.Range.from(wordRangeBeforePos), }; let list: CompletionList; @@ -658,54 +661,45 @@ class SuggestAdapter { } else { list = value; - result.incomplete = list.isIncomplete; + result.c = list.isIncomplete; } - // the default text edit range - const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)) - .with({ end: pos }); - for (let i = 0; i < list.items.length; i++) { - const suggestion = this._convertCompletionItem(list.items[i], pos, wordRangeBeforePos, i, _id); + const suggestion = this._convertCompletionItem2(list.items[i], pos, i, _id); // check for bad completion item // for the converter did warn if (suggestion) { - result.suggestions.push(suggestion); + result.b.push(suggestion); } } - this._cache.set(_id, list.items); + + if (SuggestAdapter.supportsResolving(this._provider)) { + this._cache.set(_id, list.items); + } return result; }); } - resolveCompletionItem(resource: URI, position: IPosition, suggestion: modes.CompletionItem, token: CancellationToken): Promise { + resolveCompletionItem(_resource: URI, position: IPosition, id: number, pid: number, token: CancellationToken): Promise { if (typeof this._provider.resolveCompletionItem !== 'function') { - return Promise.resolve(suggestion); + return Promise.resolve(undefined); } - const { _parentId, _id } = (suggestion); - const item = this._cache.has(_parentId) ? this._cache.get(_parentId)![_id] : undefined; + const item = this._cache.has(pid) ? this._cache.get(pid)![id] : undefined; if (!item) { - return Promise.resolve(suggestion); + return Promise.resolve(undefined); } return asPromise(() => this._provider.resolveCompletionItem!(item, token)).then(resolvedItem => { if (!resolvedItem) { - return suggestion; + return undefined; } - const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); - const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)).with({ end: pos }); - const newSuggestion = this._convertCompletionItem(resolvedItem, pos, wordRangeBeforePos, _id, _parentId); - if (newSuggestion) { - mixin(suggestion, newSuggestion, true); - } - - return suggestion; + return this._convertCompletionItem2(resolvedItem, pos, id, pid); }); } @@ -713,60 +707,52 @@ class SuggestAdapter { this._cache.delete(id); } - private _convertCompletionItem(item: vscode.CompletionItem, position: vscode.Position, defaultRange: vscode.Range, _id: number, _parentId: number): SuggestionDto | undefined { + private _convertCompletionItem2(item: vscode.CompletionItem, position: vscode.Position, id: number, pid: number): SuggestDataDto | undefined { if (typeof item.label !== 'string' || item.label.length === 0) { console.warn('INVALID text edit -> must have at least a label'); return undefined; } - const result: SuggestionDto = { - // - _id, - _parentId, + const result: SuggestDataDto = { // - label: item.label, - kind: typeConvert.CompletionItemKind.from(item.kind), - detail: item.detail, - documentation: typeof item.documentation === 'undefined' ? undefined : typeConvert.MarkdownString.fromStrict(item.documentation), - filterText: item.filterText, - sortText: item.sortText, - preselect: item.preselect, + x: id, + y: pid, // - range: undefined!, // populated below - insertText: undefined!, // populated below - insertTextRules: item.keepWhitespace ? modes.CompletionItemInsertTextRule.KeepWhitespace : 0, - additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(typeConvert.TextEdit.from), - command: this._commands.toInternal(item.command), - commitCharacters: item.commitCharacters + a: item.label, + b: typeConvert.CompletionItemKind.from(item.kind), + c: item.detail, + d: typeof item.documentation === 'undefined' ? undefined : typeConvert.MarkdownString.fromStrict(item.documentation), + e: item.sortText, + f: item.filterText, + g: item.preselect, + i: item.keepWhitespace ? modes.CompletionItemInsertTextRule.KeepWhitespace : 0, + k: item.commitCharacters, + l: item.additionalTextEdits && item.additionalTextEdits.map(typeConvert.TextEdit.from), + m: this._commands.toInternal(item.command), }; // 'insertText'-logic if (item.textEdit) { - result.insertText = item.textEdit.newText; + result.h = item.textEdit.newText; } else if (typeof item.insertText === 'string') { - result.insertText = item.insertText; + result.h = item.insertText; } else if (item.insertText instanceof SnippetString) { - result.insertText = item.insertText.value; - result.insertTextRules! |= modes.CompletionItemInsertTextRule.InsertAsSnippet; - - } else { - result.insertText = item.label; + result.h = item.insertText.value; + result.i! |= modes.CompletionItemInsertTextRule.InsertAsSnippet; } // 'overwrite[Before|After]'-logic - let range: vscode.Range; + let range: vscode.Range | undefined; if (item.textEdit) { range = item.textEdit.range; } else if (item.range) { range = item.range; - } else { - range = defaultRange; } - result.range = typeConvert.Range.from(range); + result.j = typeConvert.Range.from(range); - if (!range.isSingleLine || range.start.line !== position.line) { + if (range && (!range.isSingleLine || range.start.line !== position.line)) { console.warn('INVALID text edit -> must be single line and on the same line'); return undefined; } @@ -1387,8 +1373,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined); } - $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.CompletionItem, token: CancellationToken): Promise { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(URI.revive(resource), position, suggestion, token), suggestion); + $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, id: number, pid: number, token: CancellationToken): Promise { + return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(URI.revive(resource), position, id, pid, token), undefined); } $releaseCompletionItems(handle: number, id: number): void {