Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts
4798 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { compareBy, numberComparator } from '../../../../../base/common/arrays.js';
7
import { findFirstMax } from '../../../../../base/common/arraysFind.js';
8
import { Emitter, Event } from '../../../../../base/common/event.js';
9
import { Disposable } from '../../../../../base/common/lifecycle.js';
10
import { ICodeEditor } from '../../../../browser/editorBrowser.js';
11
import { Position } from '../../../../common/core/position.js';
12
import { Range } from '../../../../common/core/range.js';
13
import { TextReplacement } from '../../../../common/core/edits/textEdit.js';
14
import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from '../../../../common/languages.js';
15
import { ITextModel } from '../../../../common/model.js';
16
import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js';
17
import { SnippetParser } from '../../../snippet/browser/snippetParser.js';
18
import { SnippetSession } from '../../../snippet/browser/snippetSession.js';
19
import { CompletionItem } from '../../../suggest/browser/suggest.js';
20
import { SuggestController } from '../../../suggest/browser/suggestController.js';
21
import { ObservableCodeEditor } from '../../../../browser/observableCodeEditor.js';
22
import { observableFromEvent } from '../../../../../base/common/observable.js';
23
24
export class SuggestWidgetAdaptor extends Disposable {
25
private isSuggestWidgetVisible: boolean = false;
26
private isShiftKeyPressed = false;
27
private _isActive = false;
28
private _currentSuggestItemInfo: SuggestItemInfo | undefined = undefined;
29
public get selectedItem(): SuggestItemInfo | undefined {
30
return this._currentSuggestItemInfo;
31
}
32
private _onDidSelectedItemChange = this._register(new Emitter<void>());
33
public readonly onDidSelectedItemChange: Event<void> = this._onDidSelectedItemChange.event;
34
35
constructor(
36
private readonly editor: ICodeEditor,
37
private readonly suggestControllerPreselector: () => TextReplacement | undefined,
38
private readonly onWillAccept: (item: SuggestItemInfo) => void,
39
) {
40
super();
41
42
// See the command acceptAlternativeSelectedSuggestion that is bound to shift+tab
43
this._register(editor.onKeyDown(e => {
44
if (e.shiftKey && !this.isShiftKeyPressed) {
45
this.isShiftKeyPressed = true;
46
this.update(this._isActive);
47
}
48
}));
49
this._register(editor.onKeyUp(e => {
50
if (e.shiftKey && this.isShiftKeyPressed) {
51
this.isShiftKeyPressed = false;
52
this.update(this._isActive);
53
}
54
}));
55
56
const suggestController = SuggestController.get(this.editor);
57
if (suggestController) {
58
this._register(suggestController.registerSelector({
59
priority: 100,
60
select: (model, pos, suggestItems) => {
61
const textModel = this.editor.getModel();
62
if (!textModel) {
63
// Should not happen
64
return -1;
65
}
66
67
const i = this.suggestControllerPreselector();
68
const itemToPreselect = i ? singleTextRemoveCommonPrefix(i, textModel) : undefined;
69
if (!itemToPreselect) {
70
return -1;
71
}
72
const position = Position.lift(pos);
73
74
const candidates = suggestItems
75
.map((suggestItem, index) => {
76
const suggestItemInfo = SuggestItemInfo.fromSuggestion(suggestController, textModel, position, suggestItem, this.isShiftKeyPressed);
77
const suggestItemTextEdit = singleTextRemoveCommonPrefix(suggestItemInfo.getSingleTextEdit(), textModel);
78
const valid = singleTextEditAugments(itemToPreselect, suggestItemTextEdit);
79
return { index, valid, prefixLength: suggestItemTextEdit.text.length, suggestItem };
80
})
81
.filter(item => item && item.valid && item.prefixLength > 0);
82
83
const result = findFirstMax(
84
candidates,
85
compareBy(s => s.prefixLength, numberComparator)
86
);
87
return result ? result.index : -1;
88
}
89
}));
90
91
let isBoundToSuggestWidget = false;
92
const bindToSuggestWidget = () => {
93
if (isBoundToSuggestWidget) {
94
return;
95
}
96
isBoundToSuggestWidget = true;
97
98
this._register(suggestController.widget.value.onDidShow(() => {
99
this.isSuggestWidgetVisible = true;
100
this.update(true);
101
}));
102
this._register(suggestController.widget.value.onDidHide(() => {
103
this.isSuggestWidgetVisible = false;
104
this.update(false);
105
}));
106
this._register(suggestController.widget.value.onDidFocus(() => {
107
this.isSuggestWidgetVisible = true;
108
this.update(true);
109
}));
110
};
111
112
this._register(Event.once(suggestController.model.onDidTrigger)(e => {
113
bindToSuggestWidget();
114
}));
115
116
this._register(suggestController.onWillInsertSuggestItem(e => {
117
const position = this.editor.getPosition();
118
const model = this.editor.getModel();
119
if (!position || !model) { return undefined; }
120
121
const suggestItemInfo = SuggestItemInfo.fromSuggestion(
122
suggestController,
123
model,
124
position,
125
e.item,
126
this.isShiftKeyPressed
127
);
128
129
this.onWillAccept(suggestItemInfo);
130
}));
131
}
132
this.update(this._isActive);
133
}
134
135
private update(newActive: boolean): void {
136
const newInlineCompletion = this.getSuggestItemInfo();
137
138
if (this._isActive !== newActive || !suggestItemInfoEquals(this._currentSuggestItemInfo, newInlineCompletion)) {
139
this._isActive = newActive;
140
this._currentSuggestItemInfo = newInlineCompletion;
141
142
this._onDidSelectedItemChange.fire();
143
}
144
}
145
146
private getSuggestItemInfo(): SuggestItemInfo | undefined {
147
const suggestController = SuggestController.get(this.editor);
148
if (!suggestController || !this.isSuggestWidgetVisible) {
149
return undefined;
150
}
151
152
const focusedItem = suggestController.widget.value.getFocusedItem();
153
const position = this.editor.getPosition();
154
const model = this.editor.getModel();
155
156
if (!focusedItem || !position || !model) {
157
return undefined;
158
}
159
160
return SuggestItemInfo.fromSuggestion(
161
suggestController,
162
model,
163
position,
164
focusedItem.item,
165
this.isShiftKeyPressed
166
);
167
}
168
169
public stopForceRenderingAbove(): void {
170
const suggestController = SuggestController.get(this.editor);
171
suggestController?.stopForceRenderingAbove();
172
}
173
174
public forceRenderingAbove(): void {
175
const suggestController = SuggestController.get(this.editor);
176
suggestController?.forceRenderingAbove();
177
}
178
}
179
180
export class SuggestItemInfo {
181
public static fromSuggestion(suggestController: SuggestController, model: ITextModel, position: Position, item: CompletionItem, toggleMode: boolean): SuggestItemInfo {
182
let { insertText } = item.completion;
183
let isSnippetText = false;
184
if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) {
185
const snippet = new SnippetParser().parse(insertText);
186
187
if (snippet.children.length < 100) {
188
// Adjust whitespace is expensive.
189
SnippetSession.adjustWhitespace(model, position, true, snippet);
190
}
191
192
insertText = snippet.toString();
193
isSnippetText = true;
194
}
195
196
const info = suggestController.getOverwriteInfo(item, toggleMode);
197
198
return new SuggestItemInfo(
199
Range.fromPositions(
200
position.delta(0, -info.overwriteBefore),
201
position.delta(0, Math.max(info.overwriteAfter, 0))
202
),
203
insertText,
204
item.completion.kind,
205
isSnippetText,
206
item.container.incomplete ?? false,
207
);
208
}
209
210
private constructor(
211
public readonly range: Range,
212
public readonly insertText: string,
213
public readonly completionItemKind: CompletionItemKind,
214
public readonly isSnippetText: boolean,
215
public readonly listIncomplete: boolean,
216
) { }
217
218
public equals(other: SuggestItemInfo): boolean {
219
return this.range.equalsRange(other.range)
220
&& this.insertText === other.insertText
221
&& this.completionItemKind === other.completionItemKind
222
&& this.isSnippetText === other.isSnippetText;
223
}
224
225
public toSelectedSuggestionInfo(): SelectedSuggestionInfo {
226
return new SelectedSuggestionInfo(this.range, this.insertText, this.completionItemKind, this.isSnippetText);
227
}
228
229
public getSingleTextEdit(): TextReplacement {
230
return new TextReplacement(this.range, this.insertText);
231
}
232
}
233
234
function suggestItemInfoEquals(a: SuggestItemInfo | undefined, b: SuggestItemInfo | undefined): boolean {
235
if (a === b) {
236
return true;
237
}
238
if (!a || !b) {
239
return false;
240
}
241
return a.equals(b);
242
}
243
244
export class ObservableSuggestWidgetAdapter extends Disposable {
245
private readonly _suggestWidgetAdaptor;
246
247
public readonly selectedItem;
248
249
constructor(
250
private readonly _editorObs: ObservableCodeEditor,
251
252
private readonly _handleSuggestAccepted: (item: SuggestItemInfo) => void,
253
private readonly _suggestControllerPreselector: () => TextReplacement | undefined,
254
) {
255
super();
256
this._suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor(
257
this._editorObs.editor,
258
() => {
259
this._editorObs.forceUpdate();
260
return this._suggestControllerPreselector();
261
},
262
(item) => this._editorObs.forceUpdate(_tx => {
263
/** @description InlineCompletionsController.handleSuggestAccepted */
264
this._handleSuggestAccepted(item);
265
})
266
));
267
this.selectedItem = observableFromEvent(this, cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => {
268
this._editorObs.forceUpdate(_tx => cb(undefined));
269
}), () => this._suggestWidgetAdaptor.selectedItem);
270
}
271
272
public stopForceRenderingAbove(): void {
273
this._suggestWidgetAdaptor.stopForceRenderingAbove();
274
}
275
276
public forceRenderingAbove(): void {
277
this._suggestWidgetAdaptor.forceRenderingAbove();
278
}
279
}
280
281