implement API proposal

This commit is contained in:
Johannes Rieken 2019-10-22 16:48:12 +02:00
parent f1583f11ab
commit 9f474febfe
13 changed files with 111 additions and 73 deletions

View file

@ -453,7 +453,7 @@ export interface CompletionItem {
* *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: IRange;
range: IRange | { insert: IRange, replace: IRange };
/**
* An optional set of characters that when pressed while this completion is active will accept it first and
* then type that character. *Note* that all commit characters should have `length=1` and that superfluous

View file

@ -160,7 +160,7 @@ export class CompletionModel {
// 'word' is that remainder of the current line that we
// filter and score against. In theory each suggestion uses a
// different word, but in practice not - that's why we cache
const overwriteBefore = item.position.column - item.completion.range.startColumn;
const overwriteBefore = item.position.column - item.editStart.column;
const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);
if (word.length !== wordLen) {
word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);

View file

@ -31,6 +31,11 @@ export class CompletionItem {
readonly resolve: (token: CancellationToken) => Promise<void>;
//
readonly editStart: IPosition;
readonly editInsertEnd: IPosition;
readonly editReplaceEnd: IPosition;
// perf
readonly labelLow: string;
readonly sortTextLow?: string;
@ -54,6 +59,17 @@ export class CompletionItem {
this.sortTextLow = completion.sortText && completion.sortText.toLowerCase();
this.filterTextLow = completion.filterText && completion.filterText.toLowerCase();
// normalize ranges
if (Range.isIRange(completion.range)) {
this.editStart = new Position(completion.range.startLineNumber, completion.range.startColumn);
this.editInsertEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
this.editReplaceEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
} else {
this.editStart = new Position(completion.range.insert.startLineNumber, completion.range.insert.startColumn);
this.editInsertEnd = new Position(completion.range.insert.endLineNumber, completion.range.insert.endColumn);
this.editReplaceEnd = new Position(completion.range.replace.endLineNumber, completion.range.replace.endColumn);
}
// create the suggestion resolver
const { resolveCompletionItem } = provider;
if (typeof resolveCompletionItem !== 'function') {
@ -122,8 +138,12 @@ export function provideSuggestionItems(
token: CancellationToken = CancellationToken.None
): Promise<CompletionItem[]> {
const wordUntil = model.getWordUntilPosition(position);
const defaultRange = new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);
const word = model.getWordAtPosition(position);
const defaultReplaceRange = word ? new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) : Range.fromPositions(position);
const defaultInsertRange = defaultReplaceRange.setEndPosition(position.lineNumber, position.column);
// const wordUntil = model.getWordUntilPosition(position);
// const defaultRange = new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);
position = position.clone();
@ -159,7 +179,7 @@ export function provideSuggestionItems(
// fill in default range when missing
if (!suggestion.range) {
suggestion.range = defaultRange;
suggestion.range = { insert: defaultInsertRange, replace: defaultReplaceRange };
}
// fill in default sortText when missing
if (!suggestion.sortText) {

View file

@ -138,7 +138,7 @@ export class SuggestController implements IEditorContribution {
this._toDispose.add(widget.onDidFocus(({ item }) => {
const position = this._editor.getPosition()!;
const startColumn = item.completion.range.startColumn;
const startColumn = item.editStart.column;
const endColumn = position.column;
let value = true;
if (
@ -241,7 +241,8 @@ export class SuggestController implements IEditorContribution {
const model = this._editor.getModel();
const modelVersionNow = model.getAlternativeVersionId();
const { completion: suggestion, position } = event.item;
const { item } = event;
const { completion: suggestion, position } = item;
const columnDelta = this._editor.getPosition().column - position.column;
// pushing undo stops *before* additional text edits and
@ -255,33 +256,20 @@ export class SuggestController implements IEditorContribution {
}
// keep item in memory
this._memoryService.memorize(model, this._editor.getPosition(), event.item);
this._memoryService.memorize(model, this._editor.getPosition(), item);
let { insertText } = suggestion;
if (!(suggestion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) {
insertText = SnippetParser.escape(insertText);
}
let overwriteBefore = position.column - suggestion.range.startColumn;
let overwriteAfter = suggestion.range.endColumn - position.column;
let suffixDelta = this._lineSuffix.value ? this._lineSuffix.value.delta(this._editor.getPosition()) : 0;
let word = model.getWordAtPosition(this._editor.getPosition());
const overwriteConfig = flags & InsertFlags.AlternativeOverwriteConfig
? !this._editor.getOption(EditorOption.suggest).overwriteOnAccept
: this._editor.getOption(EditorOption.suggest).overwriteOnAccept;
if (!overwriteConfig) {
if (overwriteAfter > 0 && word && suggestion.range.endColumn === word.endColumn) {
// don't overwrite anything right of the cursor, overrule extension even when the
// completion only replaces a word...
overwriteAfter = 0;
}
} else {
if (overwriteAfter === 0 && word) {
// compute fallback overwrite length
overwriteAfter = word.endColumn - this._editor.getPosition().column;
}
}
const overwriteBefore = position.column - item.editStart.column;
const overwriteAfter = (overwriteConfig ? item.editReplaceEnd.column : item.editInsertEnd.column) - position.column;
const suffixDelta = this._lineSuffix.value ? this._lineSuffix.value.delta(this._editor.getPosition()) : 0;
SnippetController2.get(this._editor).insert(insertText, {
overwriteBefore: overwriteBefore + columnDelta,
@ -367,7 +355,7 @@ export class SuggestController implements IEditorContribution {
return true;
}
const position = this._editor.getPosition()!;
const startColumn = item.completion.range.startColumn;
const startColumn = item.editStart.column;
const endColumn = position.column;
if (endColumn - startColumn !== item.completion.insertText.length) {
// unequal lengths -> makes edit

5
src/vs/monaco.d.ts vendored
View file

@ -4765,7 +4765,10 @@ declare namespace monaco.languages {
* *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: IRange;
range: IRange | {
insert: IRange;
replace: IRange;
};
/**
* An optional set of characters that when pressed while this completion is active will accept it first and
* then type that character. *Note* that all commit characters should have `length=1` and that superfluous

12
src/vs/vscode.d.ts vendored
View file

@ -3393,15 +3393,17 @@ declare module 'vscode' {
insertText?: string | SnippetString;
/**
* A range of text that should be replaced by this completion item.
* A range or a insert and replace range selecting the text that should be replaced by this completion item.
*
* Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the
* current position.
* When omitted, the range of the [current word](#TextDocument.getWordRangeAtPosition) is used as replace-range
* and as insert-range the start of the [current word](#TextDocument.getWordRangeAtPosition) to the
* current position is used.
*
* *Note:* The range must be a [single line](#Range.isSingleLine) and it must
* *Note 1:* A range must be a [single line](#Range.isSingleLine) and it must
* [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems).
* *Note 2:* A insert range must be a prefix of a replace range, that means it must be contained and starting at the same position.
*/
range?: Range;
range?: Range | { insert: Range; replace: Range; };
/**
* An optional set of characters that when pressed while this completion is active will accept it first and

View file

@ -326,7 +326,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
// --- suggest
private static _inflateSuggestDto(defaultRange: IRange, data: ISuggestDataDto): modes.CompletionItem {
private static _inflateSuggestDto(defaultRange: IRange | { insert: IRange, replace: IRange }, data: ISuggestDataDto): modes.CompletionItem {
return {
label: data[ISuggestDataDtoField.label],
kind: data[ISuggestDataDtoField.kind],
@ -337,8 +338,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
filterText: data[ISuggestDataDtoField.filterText],
preselect: data[ISuggestDataDtoField.preselect],
insertText: typeof data.h === 'undefined' ? data[ISuggestDataDtoField.label] : data.h,
insertTextRules: data[ISuggestDataDtoField.insertTextRules],
range: data[ISuggestDataDtoField.range] || defaultRange,
insertTextRules: data[ISuggestDataDtoField.insertTextRules],
commitCharacters: data[ISuggestDataDtoField.commitCharacters],
additionalTextEdits: data[ISuggestDataDtoField.additionalTextEdits],
command: data[ISuggestDataDtoField.command],
@ -370,6 +371,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
if (!result) {
return suggestion;
}
let newSuggestion = MainThreadLanguageFeatures._inflateSuggestDto(suggestion.range, result);
return mixin(suggestion, newSuggestion, true);
});

View file

@ -970,7 +970,7 @@ export interface ISuggestDataDto {
[ISuggestDataDtoField.preselect]?: boolean;
[ISuggestDataDtoField.insertText]?: string;
[ISuggestDataDtoField.insertTextRules]?: modes.CompletionItemInsertTextRule;
[ISuggestDataDtoField.range]?: IRange;
[ISuggestDataDtoField.range]?: IRange | { insert: IRange, replace: IRange };
[ISuggestDataDtoField.commitCharacters]?: string[];
[ISuggestDataDtoField.additionalTextEdits]?: ISingleEditOperation[];
[ISuggestDataDtoField.command]?: modes.Command;
@ -981,7 +981,7 @@ export interface ISuggestDataDto {
export interface ISuggestResultDto {
x?: number;
a: IRange;
a: { insert: IRange, replace: IRange };
b: ISuggestDataDto[];
c?: boolean;
}

View file

@ -658,13 +658,13 @@ class SuggestAdapter {
this._disposables.set(pid, disposables);
// the default text edit range
const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos))
.with({ end: pos });
const replaceRange = doc.getWordRangeAtPosition(pos) || new Range(pos, pos);
const insertRange = replaceRange.with({ end: pos });
const result: extHostProtocol.ISuggestResultDto = {
x: pid,
b: [],
a: typeConvert.Range.from(wordRangeBeforePos),
a: { replace: typeConvert.Range.from(replaceRange), insert: typeConvert.Range.from(insertRange) },
c: list.isIncomplete || undefined
};
@ -751,21 +751,44 @@ class SuggestAdapter {
}
// 'overwrite[Before|After]'-logic
let range: vscode.Range | undefined;
let range: vscode.Range | { insert: vscode.Range, replace: vscode.Range } | undefined;
if (item.textEdit) {
range = item.textEdit.range;
} else if (item.range) {
range = item.range;
}
result[extHostProtocol.ISuggestDataDtoField.range] = typeConvert.Range.from(range);
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;
if (range) {
if (Range.isRange(range)) {
if (!SuggestAdapter._isValidRangeForCompletion(range, position)) {
console.trace('INVALID range -> must be single line and on the same line');
return undefined;
}
result[extHostProtocol.ISuggestDataDtoField.range] = typeConvert.Range.from(range);
} else {
if (
!SuggestAdapter._isValidRangeForCompletion(range.insert, position)
|| !SuggestAdapter._isValidRangeForCompletion(range.replace, position)
|| !range.insert.start.isEqual(range.replace.start)
|| !range.replace.contains(range.insert)
) {
console.trace('INVALID range -> must be single line, on the same line, insert range must be a prefix of replace range');
return undefined;
}
result[extHostProtocol.ISuggestDataDtoField.range] = {
insert: typeConvert.Range.from(range.insert),
replace: typeConvert.Range.from(range.replace)
};
}
}
return result;
}
private static _isValidRangeForCompletion(range: vscode.Range, position: vscode.Position): boolean {
return range.isSingleLine || range.start.line === position.line;
}
}
class SignatureHelpAdapter {

View file

@ -15,7 +15,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress';
import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
import { IPosition } from 'vs/editor/common/core/position';
import { IRange } from 'vs/editor/common/core/range';
import * as editorRange from 'vs/editor/common/core/range';
import { ISelection } from 'vs/editor/common/core/selection';
import * as htmlContent from 'vs/base/common/htmlContent';
import * as languageSelector from 'vs/editor/common/modes/languageSelector';
@ -68,9 +68,9 @@ export namespace Selection {
export namespace Range {
export function from(range: undefined): undefined;
export function from(range: RangeLike): IRange;
export function from(range: RangeLike | undefined): IRange | undefined;
export function from(range: RangeLike | undefined): IRange | undefined {
export function from(range: RangeLike): editorRange.IRange;
export function from(range: RangeLike | undefined): editorRange.IRange | undefined;
export function from(range: RangeLike | undefined): editorRange.IRange | undefined {
if (!range) {
return undefined;
}
@ -84,9 +84,9 @@ export namespace Range {
}
export function to(range: undefined): types.Range;
export function to(range: IRange): types.Range;
export function to(range: IRange | undefined): types.Range | undefined;
export function to(range: IRange | undefined): types.Range | undefined {
export function to(range: editorRange.IRange): types.Range;
export function to(range: editorRange.IRange | undefined): types.Range | undefined;
export function to(range: editorRange.IRange | undefined): types.Range | undefined {
if (!range) {
return undefined;
}
@ -821,14 +821,14 @@ export namespace CompletionItem {
result.filterText = suggestion.filterText;
result.preselect = suggestion.preselect;
result.commitCharacters = suggestion.commitCharacters;
result.range = Range.to(suggestion.range);
result.range = editorRange.Range.isIRange(suggestion.range) ? Range.to(suggestion.range) : { insert: Range.to(suggestion.range.insert), replace: Range.to(suggestion.range.replace) };
result.keepWhitespace = typeof suggestion.insertTextRules === 'undefined' ? false : Boolean(suggestion.insertTextRules & modes.CompletionItemInsertTextRule.KeepWhitespace);
// 'inserText'-logic
if (typeof suggestion.insertTextRules !== 'undefined' && suggestion.insertTextRules & modes.CompletionItemInsertTextRule.InsertAsSnippet) {
result.insertText = new types.SnippetString(suggestion.insertText);
} else {
result.insertText = suggestion.insertText;
result.textEdit = new types.TextEdit(result.range, result.insertText);
result.textEdit = result.range instanceof types.Range ? new types.TextEdit(result.range, result.insertText) : undefined;
}
// TODO additionalEdits, command

View file

@ -1343,7 +1343,7 @@ export class CompletionItem implements vscode.CompletionItem {
preselect?: boolean;
insertText?: string | SnippetString;
keepWhitespace?: boolean;
range?: Range;
range?: Range | { insert: Range; replace: Range; };
commitCharacters?: string[];
textEdit?: TextEdit;
additionalTextEdits?: TextEdit[];

View file

@ -84,7 +84,7 @@ suite('SnippetsService', function () {
assert.equal(result.incomplete, undefined);
assert.equal(result.suggestions.length, 1);
assert.equal(result.suggestions[0].label, 'bar');
assert.equal(result.suggestions[0].range.startColumn, 1);
assert.equal((result.suggestions[0].range as any).startColumn, 1);
assert.equal(result.suggestions[0].insertText, 'barCodeSnippet');
});
});
@ -117,10 +117,10 @@ suite('SnippetsService', function () {
assert.equal(result.suggestions.length, 2);
assert.equal(result.suggestions[0].label, 'bar');
assert.equal(result.suggestions[0].insertText, 's1');
assert.equal(result.suggestions[0].range.startColumn, 1);
assert.equal((result.suggestions[0].range as any).startColumn, 1);
assert.equal(result.suggestions[1].label, 'bar-bar');
assert.equal(result.suggestions[1].insertText, 's2');
assert.equal(result.suggestions[1].range.startColumn, 1);
assert.equal((result.suggestions[1].range as any).startColumn, 1);
});
await provider.provideCompletionItems(model, new Position(1, 5), context)!.then(result => {
@ -128,7 +128,7 @@ suite('SnippetsService', function () {
assert.equal(result.suggestions.length, 1);
assert.equal(result.suggestions[0].label, 'bar-bar');
assert.equal(result.suggestions[0].insertText, 's2');
assert.equal(result.suggestions[0].range.startColumn, 1);
assert.equal((result.suggestions[0].range as any).startColumn, 1);
});
await provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => {
@ -136,10 +136,10 @@ suite('SnippetsService', function () {
assert.equal(result.suggestions.length, 2);
assert.equal(result.suggestions[0].label, 'bar');
assert.equal(result.suggestions[0].insertText, 's1');
assert.equal(result.suggestions[0].range.startColumn, 5);
assert.equal((result.suggestions[0].range as any).startColumn, 5);
assert.equal(result.suggestions[1].label, 'bar-bar');
assert.equal(result.suggestions[1].insertText, 's2');
assert.equal(result.suggestions[1].range.startColumn, 1);
assert.equal((result.suggestions[1].range as any).startColumn, 1);
});
});
@ -165,14 +165,14 @@ suite('SnippetsService', function () {
return provider.provideCompletionItems(model, new Position(1, 4), context)!;
}).then(result => {
assert.equal(result.suggestions.length, 1);
assert.equal(result.suggestions[0].range.startColumn, 2);
assert.equal((result.suggestions[0].range as any).startColumn, 2);
model.dispose();
model = TextModel.createFromString('a<?', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 4), context)!;
}).then(result => {
assert.equal(result.suggestions.length, 1);
assert.equal(result.suggestions[0].range.startColumn, 2);
assert.equal((result.suggestions[0].range as any).startColumn, 2);
model.dispose();
});
});
@ -400,13 +400,13 @@ suite('SnippetsService', function () {
assert.equal(result.suggestions.length, 1);
let [first] = result.suggestions;
assert.equal(first.range.startColumn, 2);
assert.equal((first.range as any).startColumn, 2);
model = TextModel.createFromString('1', undefined, modeService.getLanguageIdentifier('fooLang'));
result = await provider.provideCompletionItems(model, new Position(1, 2), context)!;
assert.equal(result.suggestions.length, 1);
[first] = result.suggestions;
assert.equal(first.range.startColumn, 1);
assert.equal((first.range as any).startColumn, 1);
});
});

View file

@ -412,11 +412,8 @@ suite('ExtHostLanguageFeatureCommands', function () {
assert.equal(values.length, 4);
let [first, second, third, fourth] = 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(first.textEdit, undefined);// no text edit, default ranges
assert.ok(!types.Range.isRange(first.range));
assert.equal(second.label, 'item2');
assert.equal(second.textEdit!.newText, 'foo');
@ -434,10 +431,13 @@ suite('ExtHostLanguageFeatureCommands', function () {
assert.equal(fourth.label, 'item4');
assert.equal(fourth.textEdit, undefined);
assert.equal(fourth.range!.start.line, 0);
assert.equal(fourth.range!.start.character, 1);
assert.equal(fourth.range!.end.line, 0);
assert.equal(fourth.range!.end.character, 4);
const range: any = fourth.range!;
assert.ok(types.Range.isRange(range));
assert.equal(range.start.line, 0);
assert.equal(range.start.character, 1);
assert.equal(range.end.line, 0);
assert.equal(range.end.character, 4);
assert.ok(fourth.insertText instanceof types.SnippetString);
assert.equal((<types.SnippetString>fourth.insertText).value, 'foo$0bar');
});