Skip to content

Commit eea50bf

Browse files
authored
Merge pull request microsoft#83075 from microsoft/joh/completionOverwrite
API for completion item replace and insert ranges
2 parents 6ee4811 + 0bfdfa6 commit eea50bf

13 files changed

Lines changed: 111 additions & 73 deletions

File tree

src/vs/editor/common/modes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ export interface CompletionItem {
453453
* *Note:* The range must be a [single line](#Range.isSingleLine) and it must
454454
* [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems).
455455
*/
456-
range: IRange;
456+
range: IRange | { insert: IRange, replace: IRange };
457457
/**
458458
* An optional set of characters that when pressed while this completion is active will accept it first and
459459
* then type that character. *Note* that all commit characters should have `length=1` and that superfluous

src/vs/editor/contrib/suggest/completionModel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export class CompletionModel {
160160
// 'word' is that remainder of the current line that we
161161
// filter and score against. In theory each suggestion uses a
162162
// different word, but in practice not - that's why we cache
163-
const overwriteBefore = item.position.column - item.completion.range.startColumn;
163+
const overwriteBefore = item.position.column - item.editStart.column;
164164
const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);
165165
if (word.length !== wordLen) {
166166
word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);

src/vs/editor/contrib/suggest/suggest.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ export class CompletionItem {
3131

3232
readonly resolve: (token: CancellationToken) => Promise<void>;
3333

34+
//
35+
readonly editStart: IPosition;
36+
readonly editInsertEnd: IPosition;
37+
readonly editReplaceEnd: IPosition;
38+
3439
// perf
3540
readonly labelLow: string;
3641
readonly sortTextLow?: string;
@@ -54,6 +59,17 @@ export class CompletionItem {
5459
this.sortTextLow = completion.sortText && completion.sortText.toLowerCase();
5560
this.filterTextLow = completion.filterText && completion.filterText.toLowerCase();
5661

62+
// normalize ranges
63+
if (Range.isIRange(completion.range)) {
64+
this.editStart = new Position(completion.range.startLineNumber, completion.range.startColumn);
65+
this.editInsertEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
66+
this.editReplaceEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
67+
} else {
68+
this.editStart = new Position(completion.range.insert.startLineNumber, completion.range.insert.startColumn);
69+
this.editInsertEnd = new Position(completion.range.insert.endLineNumber, completion.range.insert.endColumn);
70+
this.editReplaceEnd = new Position(completion.range.replace.endLineNumber, completion.range.replace.endColumn);
71+
}
72+
5773
// create the suggestion resolver
5874
const { resolveCompletionItem } = provider;
5975
if (typeof resolveCompletionItem !== 'function') {
@@ -122,8 +138,12 @@ export function provideSuggestionItems(
122138
token: CancellationToken = CancellationToken.None
123139
): Promise<CompletionItem[]> {
124140

125-
const wordUntil = model.getWordUntilPosition(position);
126-
const defaultRange = new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);
141+
const word = model.getWordAtPosition(position);
142+
const defaultReplaceRange = word ? new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) : Range.fromPositions(position);
143+
const defaultInsertRange = defaultReplaceRange.setEndPosition(position.lineNumber, position.column);
144+
145+
// const wordUntil = model.getWordUntilPosition(position);
146+
// const defaultRange = new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);
127147

128148
position = position.clone();
129149

@@ -159,7 +179,7 @@ export function provideSuggestionItems(
159179

160180
// fill in default range when missing
161181
if (!suggestion.range) {
162-
suggestion.range = defaultRange;
182+
suggestion.range = { insert: defaultInsertRange, replace: defaultReplaceRange };
163183
}
164184
// fill in default sortText when missing
165185
if (!suggestion.sortText) {

src/vs/editor/contrib/suggest/suggestController.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export class SuggestController implements IEditorContribution {
138138
this._toDispose.add(widget.onDidFocus(({ item }) => {
139139

140140
const position = this._editor.getPosition()!;
141-
const startColumn = item.completion.range.startColumn;
141+
const startColumn = item.editStart.column;
142142
const endColumn = position.column;
143143
let value = true;
144144
if (
@@ -241,7 +241,8 @@ export class SuggestController implements IEditorContribution {
241241

242242
const model = this._editor.getModel();
243243
const modelVersionNow = model.getAlternativeVersionId();
244-
const { completion: suggestion, position } = event.item;
244+
const { item } = event;
245+
const { completion: suggestion, position } = item;
245246
const columnDelta = this._editor.getPosition().column - position.column;
246247

247248
// pushing undo stops *before* additional text edits and
@@ -255,33 +256,20 @@ export class SuggestController implements IEditorContribution {
255256
}
256257

257258
// keep item in memory
258-
this._memoryService.memorize(model, this._editor.getPosition(), event.item);
259+
this._memoryService.memorize(model, this._editor.getPosition(), item);
259260

260261
let { insertText } = suggestion;
261262
if (!(suggestion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) {
262263
insertText = SnippetParser.escape(insertText);
263264
}
264265

265-
let overwriteBefore = position.column - suggestion.range.startColumn;
266-
let overwriteAfter = suggestion.range.endColumn - position.column;
267-
let suffixDelta = this._lineSuffix.value ? this._lineSuffix.value.delta(this._editor.getPosition()) : 0;
268-
let word = model.getWordAtPosition(this._editor.getPosition());
269-
270266
const overwriteConfig = flags & InsertFlags.AlternativeOverwriteConfig
271267
? !this._editor.getOption(EditorOption.suggest).overwriteOnAccept
272268
: this._editor.getOption(EditorOption.suggest).overwriteOnAccept;
273-
if (!overwriteConfig) {
274-
if (overwriteAfter > 0 && word && suggestion.range.endColumn === word.endColumn) {
275-
// don't overwrite anything right of the cursor, overrule extension even when the
276-
// completion only replaces a word...
277-
overwriteAfter = 0;
278-
}
279-
} else {
280-
if (overwriteAfter === 0 && word) {
281-
// compute fallback overwrite length
282-
overwriteAfter = word.endColumn - this._editor.getPosition().column;
283-
}
284-
}
269+
270+
const overwriteBefore = position.column - item.editStart.column;
271+
const overwriteAfter = (overwriteConfig ? item.editReplaceEnd.column : item.editInsertEnd.column) - position.column;
272+
const suffixDelta = this._lineSuffix.value ? this._lineSuffix.value.delta(this._editor.getPosition()) : 0;
285273

286274
SnippetController2.get(this._editor).insert(insertText, {
287275
overwriteBefore: overwriteBefore + columnDelta,
@@ -367,7 +355,7 @@ export class SuggestController implements IEditorContribution {
367355
return true;
368356
}
369357
const position = this._editor.getPosition()!;
370-
const startColumn = item.completion.range.startColumn;
358+
const startColumn = item.editStart.column;
371359
const endColumn = position.column;
372360
if (endColumn - startColumn !== item.completion.insertText.length) {
373361
// unequal lengths -> makes edit

src/vs/monaco.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4779,7 +4779,10 @@ declare namespace monaco.languages {
47794779
* *Note:* The range must be a [single line](#Range.isSingleLine) and it must
47804780
* [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems).
47814781
*/
4782-
range: IRange;
4782+
range: IRange | {
4783+
insert: IRange;
4784+
replace: IRange;
4785+
};
47834786
/**
47844787
* An optional set of characters that when pressed while this completion is active will accept it first and
47854788
* then type that character. *Note* that all commit characters should have `length=1` and that superfluous

src/vs/vscode.d.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3393,15 +3393,17 @@ declare module 'vscode' {
33933393
insertText?: string | SnippetString;
33943394

33953395
/**
3396-
* A range of text that should be replaced by this completion item.
3396+
* A range or a insert and replace range selecting the text that should be replaced by this completion item.
33973397
*
3398-
* Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the
3399-
* current position.
3398+
* When omitted, the range of the [current word](#TextDocument.getWordRangeAtPosition) is used as replace-range
3399+
* and as insert-range the start of the [current word](#TextDocument.getWordRangeAtPosition) to the
3400+
* current position is used.
34003401
*
3401-
* *Note:* The range must be a [single line](#Range.isSingleLine) and it must
3402+
* *Note 1:* A range must be a [single line](#Range.isSingleLine) and it must
34023403
* [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems).
3404+
* *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.
34033405
*/
3404-
range?: Range;
3406+
range?: Range | { insert: Range; replace: Range; };
34053407

34063408
/**
34073409
* An optional set of characters that when pressed while this completion is active will accept it first and

src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
326326

327327
// --- suggest
328328

329-
private static _inflateSuggestDto(defaultRange: IRange, data: ISuggestDataDto): modes.CompletionItem {
329+
private static _inflateSuggestDto(defaultRange: IRange | { insert: IRange, replace: IRange }, data: ISuggestDataDto): modes.CompletionItem {
330+
330331
return {
331332
label: data[ISuggestDataDtoField.label],
332333
kind: data[ISuggestDataDtoField.kind],
@@ -337,8 +338,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
337338
filterText: data[ISuggestDataDtoField.filterText],
338339
preselect: data[ISuggestDataDtoField.preselect],
339340
insertText: typeof data.h === 'undefined' ? data[ISuggestDataDtoField.label] : data.h,
340-
insertTextRules: data[ISuggestDataDtoField.insertTextRules],
341341
range: data[ISuggestDataDtoField.range] || defaultRange,
342+
insertTextRules: data[ISuggestDataDtoField.insertTextRules],
342343
commitCharacters: data[ISuggestDataDtoField.commitCharacters],
343344
additionalTextEdits: data[ISuggestDataDtoField.additionalTextEdits],
344345
command: data[ISuggestDataDtoField.command],
@@ -370,6 +371,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
370371
if (!result) {
371372
return suggestion;
372373
}
374+
373375
let newSuggestion = MainThreadLanguageFeatures._inflateSuggestDto(suggestion.range, result);
374376
return mixin(suggestion, newSuggestion, true);
375377
});

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -970,7 +970,7 @@ export interface ISuggestDataDto {
970970
[ISuggestDataDtoField.preselect]?: boolean;
971971
[ISuggestDataDtoField.insertText]?: string;
972972
[ISuggestDataDtoField.insertTextRules]?: modes.CompletionItemInsertTextRule;
973-
[ISuggestDataDtoField.range]?: IRange;
973+
[ISuggestDataDtoField.range]?: IRange | { insert: IRange, replace: IRange };
974974
[ISuggestDataDtoField.commitCharacters]?: string[];
975975
[ISuggestDataDtoField.additionalTextEdits]?: ISingleEditOperation[];
976976
[ISuggestDataDtoField.command]?: modes.Command;
@@ -981,7 +981,7 @@ export interface ISuggestDataDto {
981981

982982
export interface ISuggestResultDto {
983983
x?: number;
984-
a: IRange;
984+
a: { insert: IRange, replace: IRange };
985985
b: ISuggestDataDto[];
986986
c?: boolean;
987987
}

src/vs/workbench/api/common/extHostLanguageFeatures.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -658,13 +658,13 @@ class SuggestAdapter {
658658
this._disposables.set(pid, disposables);
659659

660660
// the default text edit range
661-
const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos))
662-
.with({ end: pos });
661+
const replaceRange = doc.getWordRangeAtPosition(pos) || new Range(pos, pos);
662+
const insertRange = replaceRange.with({ end: pos });
663663

664664
const result: extHostProtocol.ISuggestResultDto = {
665665
x: pid,
666666
b: [],
667-
a: typeConvert.Range.from(wordRangeBeforePos),
667+
a: { replace: typeConvert.Range.from(replaceRange), insert: typeConvert.Range.from(insertRange) },
668668
c: list.isIncomplete || undefined
669669
};
670670

@@ -751,21 +751,44 @@ class SuggestAdapter {
751751
}
752752

753753
// 'overwrite[Before|After]'-logic
754-
let range: vscode.Range | undefined;
754+
let range: vscode.Range | { insert: vscode.Range, replace: vscode.Range } | undefined;
755755
if (item.textEdit) {
756756
range = item.textEdit.range;
757757
} else if (item.range) {
758758
range = item.range;
759759
}
760-
result[extHostProtocol.ISuggestDataDtoField.range] = typeConvert.Range.from(range);
761760

762-
if (range && (!range.isSingleLine || range.start.line !== position.line)) {
763-
console.warn('INVALID text edit -> must be single line and on the same line');
764-
return undefined;
761+
if (range) {
762+
if (Range.isRange(range)) {
763+
if (!SuggestAdapter._isValidRangeForCompletion(range, position)) {
764+
console.trace('INVALID range -> must be single line and on the same line');
765+
return undefined;
766+
}
767+
result[extHostProtocol.ISuggestDataDtoField.range] = typeConvert.Range.from(range);
768+
769+
} else {
770+
if (
771+
!SuggestAdapter._isValidRangeForCompletion(range.insert, position)
772+
|| !SuggestAdapter._isValidRangeForCompletion(range.replace, position)
773+
|| !range.insert.start.isEqual(range.replace.start)
774+
|| !range.replace.contains(range.insert)
775+
) {
776+
console.trace('INVALID range -> must be single line, on the same line, insert range must be a prefix of replace range');
777+
return undefined;
778+
}
779+
result[extHostProtocol.ISuggestDataDtoField.range] = {
780+
insert: typeConvert.Range.from(range.insert),
781+
replace: typeConvert.Range.from(range.replace)
782+
};
783+
}
765784
}
766785

767786
return result;
768787
}
788+
789+
private static _isValidRangeForCompletion(range: vscode.Range, position: vscode.Position): boolean {
790+
return range.isSingleLine || range.start.line === position.line;
791+
}
769792
}
770793

771794
class SignatureHelpAdapter {

src/vs/workbench/api/common/extHostTypeConverters.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
1515
import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress';
1616
import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
1717
import { IPosition } from 'vs/editor/common/core/position';
18-
import { IRange } from 'vs/editor/common/core/range';
18+
import * as editorRange from 'vs/editor/common/core/range';
1919
import { ISelection } from 'vs/editor/common/core/selection';
2020
import * as htmlContent from 'vs/base/common/htmlContent';
2121
import * as languageSelector from 'vs/editor/common/modes/languageSelector';
@@ -68,9 +68,9 @@ export namespace Selection {
6868
export namespace Range {
6969

7070
export function from(range: undefined): undefined;
71-
export function from(range: RangeLike): IRange;
72-
export function from(range: RangeLike | undefined): IRange | undefined;
73-
export function from(range: RangeLike | undefined): IRange | undefined {
71+
export function from(range: RangeLike): editorRange.IRange;
72+
export function from(range: RangeLike | undefined): editorRange.IRange | undefined;
73+
export function from(range: RangeLike | undefined): editorRange.IRange | undefined {
7474
if (!range) {
7575
return undefined;
7676
}
@@ -84,9 +84,9 @@ export namespace Range {
8484
}
8585

8686
export function to(range: undefined): types.Range;
87-
export function to(range: IRange): types.Range;
88-
export function to(range: IRange | undefined): types.Range | undefined;
89-
export function to(range: IRange | undefined): types.Range | undefined {
87+
export function to(range: editorRange.IRange): types.Range;
88+
export function to(range: editorRange.IRange | undefined): types.Range | undefined;
89+
export function to(range: editorRange.IRange | undefined): types.Range | undefined {
9090
if (!range) {
9191
return undefined;
9292
}
@@ -821,14 +821,14 @@ export namespace CompletionItem {
821821
result.filterText = suggestion.filterText;
822822
result.preselect = suggestion.preselect;
823823
result.commitCharacters = suggestion.commitCharacters;
824-
result.range = Range.to(suggestion.range);
824+
result.range = editorRange.Range.isIRange(suggestion.range) ? Range.to(suggestion.range) : { insert: Range.to(suggestion.range.insert), replace: Range.to(suggestion.range.replace) };
825825
result.keepWhitespace = typeof suggestion.insertTextRules === 'undefined' ? false : Boolean(suggestion.insertTextRules & modes.CompletionItemInsertTextRule.KeepWhitespace);
826826
// 'inserText'-logic
827827
if (typeof suggestion.insertTextRules !== 'undefined' && suggestion.insertTextRules & modes.CompletionItemInsertTextRule.InsertAsSnippet) {
828828
result.insertText = new types.SnippetString(suggestion.insertText);
829829
} else {
830830
result.insertText = suggestion.insertText;
831-
result.textEdit = new types.TextEdit(result.range, result.insertText);
831+
result.textEdit = result.range instanceof types.Range ? new types.TextEdit(result.range, result.insertText) : undefined;
832832
}
833833
// TODO additionalEdits, command
834834

0 commit comments

Comments
 (0)