Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts
4780 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 { createStyleSheetFromObservable } from '../../../../../base/browser/domStylesheets.js';
7
import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js';
8
import { Disposable } from '../../../../../base/common/lifecycle.js';
9
import { derived, mapObservableArrayCached, derivedDisposable, derivedObservableWithCache, IObservable, ISettableObservable, constObservable, observableValue } from '../../../../../base/common/observable.js';
10
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
11
import { ICodeEditor } from '../../../../browser/editorBrowser.js';
12
import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js';
13
import { EditorOption } from '../../../../common/config/editorOptions.js';
14
import { LineRange } from '../../../../common/core/ranges/lineRange.js';
15
import { InlineCompletionsHintsWidget } from '../hintsWidget/inlineCompletionsHintsWidget.js';
16
import { GhostTextOrReplacement } from '../model/ghostText.js';
17
import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js';
18
import { InlineCompletionItem } from '../model/inlineSuggestionItem.js';
19
import { convertItemsToStableObservables } from '../utils.js';
20
import { GhostTextView, GhostTextWidgetWarning, IGhostTextWidgetData } from './ghostText/ghostTextView.js';
21
import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from './inlineEdits/components/gutterIndicatorView.js';
22
import { InlineEditsOnboardingExperience } from './inlineEdits/inlineEditsNewUsers.js';
23
import { InlineCompletionViewKind, InlineEditTabAction } from './inlineEdits/inlineEditsViewInterface.js';
24
import { InlineEditsViewAndDiffProducer } from './inlineEdits/inlineEditsViewProducer.js';
25
26
export class InlineSuggestionsView extends Disposable {
27
public static hot = createHotClass(this);
28
29
private readonly _ghostTexts = derived(this, (reader) => {
30
const model = this._model.read(reader);
31
return model?.ghostTexts.read(reader) ?? [];
32
});
33
34
private readonly _stablizedGhostTexts;
35
private readonly _editorObs;
36
private readonly _ghostTextWidgets;
37
38
private readonly _inlineEdit = derived(this, reader => this._model.read(reader)?.inlineEditState.read(reader)?.inlineSuggestion);
39
private readonly _everHadInlineEdit = derivedObservableWithCache<boolean>(this,
40
(reader, last) => last || !!this._inlineEdit.read(reader)
41
|| !!this._model.read(reader)?.inlineCompletionState.read(reader)?.inlineSuggestion?.showInlineEditMenu
42
);
43
44
// To break a cyclic dependency
45
private readonly _indicatorIsHoverVisible = observableValue<IObservable<boolean> | undefined>(this, undefined);
46
47
private readonly _showInlineEditCollapsed = derived(this, reader => {
48
const s = this._model.read(reader)?.showCollapsed.read(reader) ?? false;
49
return s && !this._indicatorIsHoverVisible.read(reader)?.read(reader);
50
});
51
52
private readonly _inlineEditWidget = derivedDisposable(reader => {
53
if (!this._everHadInlineEdit.read(reader)) {
54
return undefined;
55
}
56
return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer, this._editor, this._model, this._showInlineEditCollapsed);
57
});
58
59
private readonly _fontFamily;
60
61
constructor(
62
private readonly _editor: ICodeEditor,
63
private readonly _model: IObservable<InlineCompletionsModel | undefined>,
64
private readonly _focusIsInMenu: ISettableObservable<boolean>,
65
@IInstantiationService private readonly _instantiationService: IInstantiationService
66
) {
67
super();
68
69
this._stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store);
70
this._editorObs = observableCodeEditor(this._editor);
71
72
this._ghostTextWidgets = mapObservableArrayCached(
73
this,
74
this._stablizedGhostTexts,
75
(ghostText, store) => store.add(this._createGhostText(ghostText))
76
).recomputeInitiallyAndOnChange(this._store);
77
78
this._inlineEditWidget.recomputeInitiallyAndOnChange(this._store);
79
80
this._fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily);
81
82
this._register(createStyleSheetFromObservable(derived(reader => {
83
const fontFamily = this._fontFamily.read(reader);
84
return `
85
.monaco-editor .ghost-text-decoration,
86
.monaco-editor .ghost-text-decoration-preview,
87
.monaco-editor .ghost-text {
88
font-family: ${fontFamily};
89
}`;
90
})));
91
92
this._register(new InlineCompletionsHintsWidget(this._editor, this._model, this._instantiationService));
93
94
this._indicator = this._register(this._instantiationService.createInstance(
95
InlineEditsGutterIndicator,
96
this._editorObs,
97
derived(reader => {
98
const s = this._gutterIndicatorState.read(reader);
99
if (!s) { return undefined; }
100
return new InlineEditsGutterIndicatorData(
101
InlineSuggestionGutterMenuData.fromInlineSuggestion(s.inlineSuggestion),
102
s.displayRange,
103
SimpleInlineSuggestModel.fromInlineCompletionModel(s.model),
104
s.inlineSuggestion.action?.kind === 'edit' ? s.inlineSuggestion.action.alternativeAction : undefined,
105
);
106
}),
107
this._gutterIndicatorState.map((s, reader) => s?.tabAction.read(reader) ?? InlineEditTabAction.Inactive),
108
this._gutterIndicatorState.map((s, reader) => s?.gutterIndicatorOffset.read(reader) ?? 0),
109
this._inlineEditWidget.map((w, reader) => w?.view.inlineEditsIsHovered.read(reader) ?? false),
110
this._focusIsInMenu,
111
));
112
this._indicatorIsHoverVisible.set(this._indicator.isHoverVisible, undefined);
113
114
derived(reader => {
115
const w = this._inlineEditWidget.read(reader);
116
if (!w) { return undefined; }
117
return reader.store.add(this._instantiationService.createInstance(
118
InlineEditsOnboardingExperience,
119
w._inlineEditModel,
120
constObservable(this._indicator),
121
w.view._inlineCollapsedView,
122
));
123
}).recomputeInitiallyAndOnChange(this._store);
124
}
125
126
private _createGhostText(ghostText: IObservable<GhostTextOrReplacement>): GhostTextView {
127
return this._instantiationService.createInstance(
128
GhostTextView,
129
this._editor,
130
derived(reader => {
131
const model = this._model.read(reader);
132
const inlineCompletion = model?.inlineCompletionState.read(reader)?.inlineSuggestion;
133
if (!model || !inlineCompletion) {
134
// editor.suggest.preview: true causes situations where we have ghost text, but no suggest preview.
135
return {
136
ghostText: ghostText.read(reader),
137
handleInlineCompletionShown: () => { /* no-op */ },
138
warning: undefined,
139
};
140
}
141
return {
142
ghostText: ghostText.read(reader),
143
handleInlineCompletionShown: (viewData) => model.handleInlineSuggestionShown(inlineCompletion, InlineCompletionViewKind.GhostText, viewData, Date.now()),
144
warning: GhostTextWidgetWarning.from(model?.warning.read(reader)),
145
} satisfies IGhostTextWidgetData;
146
}),
147
{
148
useSyntaxHighlighting: this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.syntaxHighlightingEnabled),
149
},
150
);
151
}
152
153
public shouldShowHoverAtViewZone(viewZoneId: string): boolean {
154
return this._ghostTextWidgets.get()[0]?.ownsViewZone(viewZoneId) ?? false;
155
}
156
157
private readonly _gutterIndicatorState = derived(reader => {
158
const model = this._model.read(reader);
159
if (!model) {
160
return undefined;
161
}
162
163
const state = model.state.read(reader);
164
165
if (state?.kind === 'ghostText' && state.inlineSuggestion?.showInlineEditMenu) {
166
return {
167
displayRange: LineRange.ofLength(state.primaryGhostText.lineNumber, 1),
168
tabAction: derived<InlineEditTabAction>(this,
169
reader => this._editorObs.isFocused.read(reader) ? InlineEditTabAction.Accept : InlineEditTabAction.Inactive
170
),
171
gutterIndicatorOffset: constObservable(getGhostTextTopOffset(state.inlineSuggestion, this._editor)),
172
inlineSuggestion: state.inlineSuggestion,
173
model,
174
};
175
} else if (state?.kind === 'inlineEdit') {
176
const inlineEditWidget = this._inlineEditWidget.read(reader)?.view;
177
if (!inlineEditWidget) { return undefined; }
178
179
const displayRange = inlineEditWidget.displayRange.read(reader);
180
if (!displayRange) { return undefined; }
181
return {
182
displayRange,
183
tabAction: derived(reader => {
184
if (this._editorObs.isFocused.read(reader)) {
185
if (model.tabShouldJumpToInlineEdit.read(reader)) { return InlineEditTabAction.Jump; }
186
if (model.tabShouldAcceptInlineEdit.read(reader)) { return InlineEditTabAction.Accept; }
187
}
188
return InlineEditTabAction.Inactive;
189
}),
190
gutterIndicatorOffset: inlineEditWidget.gutterIndicatorOffset,
191
inlineSuggestion: state.inlineSuggestion,
192
model,
193
};
194
} else {
195
return undefined;
196
}
197
});
198
199
protected readonly _indicator;
200
}
201
202
function getGhostTextTopOffset(inlineCompletion: InlineCompletionItem, editor: ICodeEditor): number {
203
const replacement = inlineCompletion.getSingleTextEdit();
204
const textModel = editor.getModel();
205
if (!textModel) {
206
return 0;
207
}
208
209
const EOL = textModel.getEOL();
210
if (replacement.range.isEmpty() && replacement.text.startsWith(EOL)) {
211
const lineHeight = editor.getLineHeightForPosition(replacement.range.getStartPosition());
212
return countPrefixRepeats(replacement.text, EOL) * lineHeight;
213
}
214
215
return 0;
216
}
217
218
function countPrefixRepeats(str: string, prefix: string): number {
219
if (!prefix.length) {
220
return 0;
221
}
222
let count = 0;
223
let i = 0;
224
while (str.startsWith(prefix, i)) {
225
count++;
226
i += prefix.length;
227
}
228
return count;
229
}
230
231