SnippetString and improved CompletionItem.insertText, #3210

This commit is contained in:
Johannes Rieken
2016-11-16 15:20:32 +01:00
parent 2f06e39fdb
commit c76b721d81
7 changed files with 219 additions and 144 deletions

View File

@@ -12,7 +12,7 @@ import { XHRRequest } from 'request-light';
import {
CompletionItem, CompletionItemProvider, CompletionList, TextDocument, Position, Hover, HoverProvider,
CancellationToken, Range, TextEdit, MarkedString, DocumentSelector, languages, Disposable
CancellationToken, Range, MarkedString, DocumentSelector, languages, Disposable
} from 'vscode';
export interface ISuggestionsCollector {
@@ -109,7 +109,7 @@ export class JSONCompletionItemProvider implements CompletionItemProvider {
add: (suggestion: CompletionItem) => {
if (!proposed[suggestion.label]) {
proposed[suggestion.label] = true;
suggestion.textEdit = TextEdit.replace(overwriteRange, suggestion.insertText);
suggestion.range = overwriteRange;
items.push(suggestion);
}
},
@@ -160,4 +160,4 @@ export class JSONCompletionItemProvider implements CompletionItemProvider {
}
return nextToken === SyntaxKind.CloseBraceToken || nextToken === SyntaxKind.EOF;
}
}
}

46
src/vs/vscode.d.ts vendored
View File

@@ -2023,6 +2023,25 @@ declare module 'vscode' {
entries(): [Uri, TextEdit[]][];
}
/**
* A snippet string is a template which allows to insert text
* and to control the editor cursor when insertion happens.
*
* A snippet can define tab stops and placeholders with `$1`, `$2`
* and `${3:foo}`. `$0` defines the final tab stop, it defaults to
* the end of the snippet. Placeholders with equal identifiers are linked,
* that is typing in one will update others too.
*/
export class SnippetString {
/**
* The snippet string.
*/
value: string;
constructor(value: string);
}
/**
* The rename provider interface defines the contract between extensions and
* the [rename](https://code.visualstudio.com/docs/editor/editingevolved#_rename-symbol)-feature.
@@ -2310,19 +2329,32 @@ declare module 'vscode' {
filterText: string;
/**
* A string that should be inserted in a document when selecting
* A string or snippet that should be inserted in a document when selecting
* this completion. When `falsy` the [label](#CompletionItem.label)
* is used.
*/
insertText: string;
insertText: string | SnippetString;
/**
* An [edit](#TextEdit) which is applied to a document when selecting
* this completion. When an edit is provided the value of
* [insertText](#CompletionItem.insertText) is ignored.
* A range of text that should be replaced by this completion item.
*
* The [range](#Range) of the edit must be single-line and on the same
* line completions were [requested](#CompletionItemProvider.provideCompletionItems) at.
* Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the
* current position.
*
* *Note:* The range must be a [single line](#Range.isSingleLine) and it must
* [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems).
*/
range: Range;
/**
* @deprecated **Deprecated** in favor of `CompletionItem.insertText` and `CompletionItem.range`.
*
* ~~An [edit](#TextEdit) which is applied to a document when selecting
* this completion. When an edit is provided the value of
* [insertText](#CompletionItem.insertText) is ignored.~~
*
* ~~The [range](#Range) of the edit must be single-line and on the same
* line completions were [requested](#CompletionItemProvider.provideCompletionItems) at.~~
*/
textEdit: TextEdit;

View File

@@ -368,41 +368,42 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ
window,
workspace,
// types
Uri: URI,
Location: extHostTypes.Location,
Diagnostic: extHostTypes.Diagnostic,
DiagnosticSeverity: extHostTypes.DiagnosticSeverity,
EventEmitter: Emitter,
Disposable: extHostTypes.Disposable,
TextEdit: extHostTypes.TextEdit,
WorkspaceEdit: extHostTypes.WorkspaceEdit,
Position: extHostTypes.Position,
Range: extHostTypes.Range,
Selection: extHostTypes.Selection,
CancellationTokenSource: CancellationTokenSource,
Hover: extHostTypes.Hover,
SymbolKind: extHostTypes.SymbolKind,
SymbolInformation: extHostTypes.SymbolInformation,
DocumentHighlightKind: extHostTypes.DocumentHighlightKind,
DocumentHighlight: extHostTypes.DocumentHighlight,
CodeLens: extHostTypes.CodeLens,
ParameterInformation: extHostTypes.ParameterInformation,
SignatureInformation: extHostTypes.SignatureInformation,
SignatureHelp: extHostTypes.SignatureHelp,
CompletionItem: extHostTypes.CompletionItem,
CompletionItemKind: extHostTypes.CompletionItemKind,
CompletionList: extHostTypes.CompletionList,
Diagnostic: extHostTypes.Diagnostic,
DiagnosticSeverity: extHostTypes.DiagnosticSeverity,
Disposable: extHostTypes.Disposable,
DocumentHighlight: extHostTypes.DocumentHighlight,
DocumentHighlightKind: extHostTypes.DocumentHighlightKind,
DocumentLink: extHostTypes.DocumentLink,
ViewColumn: extHostTypes.ViewColumn,
StatusBarAlignment: extHostTypes.StatusBarAlignment,
IndentAction: languageConfiguration.IndentAction,
OverviewRulerLane: EditorCommon.OverviewRulerLane,
TextEditorRevealType: extHostTypes.TextEditorRevealType,
EndOfLine: extHostTypes.EndOfLine,
EventEmitter: Emitter,
Hover: extHostTypes.Hover,
IndentAction: languageConfiguration.IndentAction,
Location: extHostTypes.Location,
OverviewRulerLane: EditorCommon.OverviewRulerLane,
ParameterInformation: extHostTypes.ParameterInformation,
Position: extHostTypes.Position,
Range: extHostTypes.Range,
Selection: extHostTypes.Selection,
SignatureHelp: extHostTypes.SignatureHelp,
SignatureInformation: extHostTypes.SignatureInformation,
SnippetString: extHostTypes.SnippetString,
StatusBarAlignment: extHostTypes.StatusBarAlignment,
SymbolInformation: extHostTypes.SymbolInformation,
SymbolKind: extHostTypes.SymbolKind,
TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason,
TextEdit: extHostTypes.TextEdit,
TextEditorCursorStyle: EditorCommon.TextEditorCursorStyle,
TextEditorLineNumbersStyle: extHostTypes.TextEditorLineNumbersStyle,
TextEditorRevealType: extHostTypes.TextEditorRevealType,
TextEditorSelectionChangeKind: extHostTypes.TextEditorSelectionChangeKind,
TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason,
Uri: URI,
ViewColumn: extHostTypes.ViewColumn,
WorkspaceEdit: extHostTypes.WorkspaceEdit,
};
};
}

View File

@@ -6,10 +6,11 @@
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { mixin } from 'vs/base/common/objects';
import { IThreadService } from 'vs/workbench/services/thread/common/threadService';
import * as vscode from 'vscode';
import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters';
import { Range, Disposable, CompletionList, CompletionItem } from 'vs/workbench/api/node/extHostTypes';
import { Range, Disposable, CompletionList, CompletionItem, SnippetString } from 'vs/workbench/api/node/extHostTypes';
import { IPosition, IRange, ISingleEditOperation } from 'vs/editor/common/editorCommon';
import * as modes from 'vs/editor/common/modes';
import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService';
@@ -21,7 +22,6 @@ import { asWinJsPromise } from 'vs/base/common/async';
import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier } from './extHost.protocol';
import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import * as semver from 'semver';
// --- adapter
@@ -415,7 +415,6 @@ class SuggestAdapter {
private _heapService: ExtHostHeapService;
private _provider: vscode.CompletionItemProvider;
private _extension: IExtensionDescription;
private _extensionIsBefore180: boolean;
constructor(documents: ExtHostDocuments, commands: CommandsConverter, heapService: ExtHostHeapService, provider: vscode.CompletionItemProvider, extension?: IExtensionDescription) {
this._documents = documents;
@@ -423,18 +422,6 @@ class SuggestAdapter {
this._heapService = heapService;
this._provider = provider;
this._extension = extension;
this._extensionIsBefore180 = SuggestAdapter._isBefore180(extension);
}
private static _isBefore180(extension: IExtensionDescription): boolean {
if (extension && extension.engines) {
let versionOrRange = extension.engines.vscode;
if (semver.valid(versionOrRange)) {
return semver.lt(versionOrRange, '1.8.0');
} else if (semver.validRange(versionOrRange)) {
return semver.gtr('1.8.0', versionOrRange);
}
}
}
provideCompletionItems(resource: URI, position: IPosition): TPromise<modes.ISuggestResult> {
@@ -448,10 +435,6 @@ class SuggestAdapter {
suggestions: [],
};
// the default text edit range
const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) || new Range(pos, pos))
.with({ end: pos });
let list: CompletionList;
if (!value) {
// undefined and null are valid results
@@ -465,39 +448,21 @@ class SuggestAdapter {
result.incomplete = list.isIncomplete;
}
for (let i = 0; i < list.items.length; i++) {
// the default text edit range
const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) || new Range(pos, pos))
.with({ end: pos });
const item = list.items[i];
const suggestion = TypeConverters.Suggest.from(item);
suggestion.command = this._commands.toInternal(item.command);
ObjectIdentifier.mixin(suggestion, this._heapService.keep(item));
for (const item of list.items) {
if (item.textEdit) {
const suggestion = this._convertCompletionItem(item, pos, wordRangeBeforePos);
const editRange = item.textEdit.range;
// invalid text edit
if (!editRange.isSingleLine || editRange.start.line !== pos.line) {
console.warn('INVALID text edit, must be single line and on the same line');
continue;
}
// insert the text of the edit and create a dedicated
// suggestion-container with overwrite[Before|After]
suggestion.insertText = item.textEdit.newText;
suggestion.overwriteBefore = pos.character - editRange.start.character;
suggestion.overwriteAfter = editRange.end.character - pos.character;
} else {
// default text edit
suggestion.overwriteBefore = pos.character - wordRangeBeforePos.start.character;
suggestion.overwriteAfter = 0;
// bad completion item
if (!suggestion) {
// converter did warn
continue;
}
suggestion._extensionId = this._extension && this._extension.id;
suggestion.snippetType = 'internal';
// store suggestion
ObjectIdentifier.mixin(suggestion, this._heapService.keep(item));
result.suggestions.push(suggestion);
}
@@ -516,13 +481,82 @@ class SuggestAdapter {
if (!item) {
return TPromise.as(suggestion);
}
return asWinJsPromise(token => this._provider.resolveCompletionItem(item, token)).then(resolvedItem => {
resolvedItem = resolvedItem || item;
const suggestion = TypeConverters.Suggest.from(resolvedItem);
suggestion.command = this._commands.toInternal(resolvedItem.command);
if (!resolvedItem) {
return suggestion;
}
const doc = this._documents.getDocumentData(resource).document;
const pos = TypeConverters.toPosition(position);
const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) || new Range(pos, pos)).with({ end: pos });
const newSuggestion = this._convertCompletionItem(resolvedItem, pos, wordRangeBeforePos);
if (newSuggestion) {
mixin(suggestion, newSuggestion, true);
}
return suggestion;
});
}
private _convertCompletionItem(item: vscode.CompletionItem, position: vscode.Position, defaultRange: vscode.Range): modes.ISuggestion {
if (!item.label) {
console.warn('INVALID text edit -> must have at least a label');
return;
}
const result: modes.ISuggestion = {
//
label: item.label,
type: TypeConverters.CompletionItemKind.from(item.kind),
detail: item.detail,
documentation: item.documentation,
filterText: item.filterText,
sortText: item.sortText,
//
insertText: undefined,
additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(TypeConverters.TextEdit.from),
command: this._commands.toInternal(item.command)
};
// 'insertText'-logic
if (item.textEdit) {
result.insertText = item.textEdit.newText;
result.snippetType = 'internal';
} else if (typeof item.insertText === 'string') {
result.insertText = item.insertText;
result.snippetType = 'internal';
} else if (item.insertText instanceof SnippetString) {
result.insertText = item.insertText.value;
result.snippetType = 'textmate';
} else {
result.insertText = item.label;
result.snippetType = 'internal';
}
// 'overwrite[Before|After]'-logic
let range: vscode.Range;
if (item.textEdit) {
range = item.textEdit.range;
} else if (item.range) {
range = item.range;
} else {
range = defaultRange;
}
result.overwriteBefore = position.character - range.start.character;
result.overwriteAfter = range.end.character - position.character;
if (!range.isSingleLine || range.start.line !== position.line) {
console.warn('INVALID text edit -> must be single line and on the same line');
return;
}
return result;
}
}
class SignatureHelpAdapter {

View File

@@ -270,23 +270,9 @@ export const CompletionItemKind = {
}
};
export const Suggest = {
export namespace Suggest {
from(item: vscode.CompletionItem): modes.ISuggestion {
const suggestion: modes.ISuggestion = {
label: item.label || '<missing label>',
insertText: item.insertText || item.label,
type: CompletionItemKind.from(item.kind),
detail: item.detail,
documentation: item.documentation,
sortText: item.sortText,
filterText: item.filterText,
additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(TextEdit.from)
};
return suggestion;
},
to(position: types.Position, suggestion: modes.ISuggestion): types.CompletionItem {
export function to(position: types.Position, suggestion: modes.ISuggestion): types.CompletionItem {
const result = new types.CompletionItem(suggestion.label);
result.insertText = suggestion.insertText;
result.kind = CompletionItemKind.to(suggestion.type);
@@ -295,14 +281,25 @@ export const Suggest = {
result.sortText = suggestion.sortText;
result.filterText = suggestion.filterText;
// 'overwrite[Before|After]'-logic
let overwriteBefore = (typeof suggestion.overwriteBefore === 'number') ? suggestion.overwriteBefore : 0;
let startPosition = new types.Position(position.line, Math.max(0, position.character - overwriteBefore));
let endPosition = position;
if (typeof suggestion.overwriteAfter === 'number') {
endPosition = new types.Position(position.line, position.character + suggestion.overwriteAfter);
}
result.range = new types.Range(startPosition, endPosition);
// 'inserText'-logic
if (suggestion.snippetType === 'textmate') {
result.insertText = new types.SnippetString(suggestion.insertText);
} else {
result.insertText = suggestion.insertText;
result.textEdit = new types.TextEdit(result.range, result.insertText);
}
// TODO additionalEdits, command
result.textEdit = types.TextEdit.replace(new types.Range(startPosition, endPosition), suggestion.insertText);
return result;
}
};

View File

@@ -522,6 +522,15 @@ export class WorkspaceEdit {
}
}
export class SnippetString {
value: string;
constructor(value: string) {
this.value = value;
}
}
export enum DiagnosticSeverity {
Hint = 3,
Information = 2,
@@ -774,7 +783,8 @@ export class CompletionItem {
documentation: string;
sortText: string;
filterText: string;
insertText: string;
insertText: string | SnippetString;
range: Range;
textEdit: TextEdit;
additionalTextEdits: TextEdit[];
command: vscode.Command;
@@ -876,4 +886,4 @@ export class DocumentLink {
this.range = range;
this.target = target;
}
}
}

View File

@@ -288,7 +288,7 @@ suite('ExtHostLanguageFeatureCommands', function () {
// --- suggest
test('Suggest, back and forth', function (done) {
test('Suggest, back and forth', function () {
disposables.push(extHost.registerCompletionItemProvider(defaultSelector, <vscode.CompletionItemProvider>{
provideCompletionItems(doc, pos): any {
let a = new types.CompletionItem('item1');
@@ -296,53 +296,54 @@ suite('ExtHostLanguageFeatureCommands', function () {
b.textEdit = types.TextEdit.replace(new types.Range(0, 4, 0, 8), 'foo'); // overwite after
let c = new types.CompletionItem('item3');
c.textEdit = types.TextEdit.replace(new types.Range(0, 1, 0, 6), 'foobar'); // overwite before & after
// snippet string!
let d = new types.CompletionItem('item4');
d.textEdit = types.TextEdit.replace(new types.Range(0, 1, 0, 4), ''); // overwite before
d.range = new types.Range(0, 1, 0, 4);// overwite before
d.insertText = new types.SnippetString('foo$0bar');
return [a, b, c, d];
}
}, []));
threadService.sync().then(() => {
commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', model.uri, new types.Position(0, 4)).then(list => {
try {
assert.ok(list instanceof types.CompletionList);
let values = list.items;
assert.ok(Array.isArray(values));
assert.equal(values.length, 4);
let [first, second, third, forth] = values;
assert.equal(first.label, 'item1');
assert.equal(first.textEdit.newText, 'item1');
assert.equal(first.textEdit.range.start.line, 0);
assert.equal(first.textEdit.range.start.character, 0);
assert.equal(first.textEdit.range.end.line, 0);
assert.equal(first.textEdit.range.end.character, 4);
return threadService.sync().then(() => {
return commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', model.uri, new types.Position(0, 4)).then(list => {
assert.equal(second.label, 'item2');
assert.equal(second.textEdit.newText, 'foo');
assert.equal(second.textEdit.range.start.line, 0);
assert.equal(second.textEdit.range.start.character, 4);
assert.equal(second.textEdit.range.end.line, 0);
assert.equal(second.textEdit.range.end.character, 8);
assert.ok(list instanceof types.CompletionList);
let values = list.items;
assert.ok(Array.isArray(values));
assert.equal(values.length, 4);
let [first, second, third, forth] = values;
assert.equal(first.label, 'item1');
assert.equal(first.textEdit.newText, 'item1');
assert.equal(first.textEdit.range.start.line, 0);
assert.equal(first.textEdit.range.start.character, 0);
assert.equal(first.textEdit.range.end.line, 0);
assert.equal(first.textEdit.range.end.character, 4);
assert.equal(third.label, 'item3');
assert.equal(third.textEdit.newText, 'foobar');
assert.equal(third.textEdit.range.start.line, 0);
assert.equal(third.textEdit.range.start.character, 1);
assert.equal(third.textEdit.range.end.line, 0);
assert.equal(third.textEdit.range.end.character, 6);
assert.equal(second.label, 'item2');
assert.equal(second.textEdit.newText, 'foo');
assert.equal(second.textEdit.range.start.line, 0);
assert.equal(second.textEdit.range.start.character, 4);
assert.equal(second.textEdit.range.end.line, 0);
assert.equal(second.textEdit.range.end.character, 8);
assert.equal(forth.label, 'item4');
assert.equal(forth.textEdit.newText, '');
assert.equal(forth.textEdit.range.start.line, 0);
assert.equal(forth.textEdit.range.start.character, 1);
assert.equal(forth.textEdit.range.end.line, 0);
assert.equal(forth.textEdit.range.end.character, 4);
done();
} catch (e) {
done(e);
}
}, done);
}, done);
assert.equal(third.label, 'item3');
assert.equal(third.textEdit.newText, 'foobar');
assert.equal(third.textEdit.range.start.line, 0);
assert.equal(third.textEdit.range.start.character, 1);
assert.equal(third.textEdit.range.end.line, 0);
assert.equal(third.textEdit.range.end.character, 6);
assert.equal(forth.label, 'item4');
assert.equal(forth.textEdit, undefined);
assert.equal(forth.range.start.line, 0);
assert.equal(forth.range.start.character, 1);
assert.equal(forth.range.end.line, 0);
assert.equal(forth.range.end.character, 4);
assert.ok(forth.insertText instanceof types.SnippetString);
assert.equal((<types.SnippetString>forth.insertText).value, 'foo$0bar');
});
});
});
test('Suggest, return CompletionList !array', function (done) {