Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts
3296 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 { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
7
import { ICodeEditor, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';
8
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
9
import { localize, localize2 } from '../../../../nls.js';
10
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
11
import { InlineChatController } from './inlineChatController.js';
12
import { ACTION_START, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';
13
import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
14
import { EditOperation } from '../../../../editor/common/core/editOperation.js';
15
import { Range } from '../../../../editor/common/core/range.js';
16
import { IPosition, Position } from '../../../../editor/common/core/position.js';
17
import { AbstractInline1ChatAction } from './inlineChatActions.js';
18
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
19
import { IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js';
20
import { URI } from '../../../../base/common/uri.js';
21
import { isEqual } from '../../../../base/common/resources.js';
22
import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js';
23
import { autorun, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
24
import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
25
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
26
import './media/inlineChat.css';
27
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
28
import { ICommandService } from '../../../../platform/commands/common/commands.js';
29
import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js';
30
import { IChatAgentService } from '../../chat/common/chatAgents.js';
31
import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js';
32
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
33
import { toAction } from '../../../../base/common/actions.js';
34
import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';
35
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
36
import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';
37
import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js';
38
import { createStyleSheet2 } from '../../../../base/browser/domStylesheets.js';
39
import { stringValue } from '../../../../base/browser/cssValue.js';
40
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
41
import { ChatAgentLocation } from '../../chat/common/constants.js';
42
import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../chat/common/promptSyntax/promptTypes.js';
43
import { MODE_FILE_EXTENSION } from '../../chat/common/promptSyntax/config/promptFileLocations.js';
44
45
/**
46
* Set of language IDs where inline chat hints should not be shown.
47
*/
48
const IGNORED_LANGUAGE_IDS = new Set([
49
PLAINTEXT_LANGUAGE_ID,
50
'markdown',
51
'search-result',
52
INSTRUCTIONS_LANGUAGE_ID,
53
PROMPT_LANGUAGE_ID,
54
MODE_FILE_EXTENSION
55
]);
56
57
export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey<boolean>('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint"));
58
59
const _inlineChatActionId = 'inlineChat.startWithCurrentLine';
60
61
export class InlineChatExpandLineAction extends EditorAction2 {
62
63
constructor() {
64
super({
65
id: _inlineChatActionId,
66
category: AbstractInline1ChatAction.category,
67
title: localize2('startWithCurrentLine', "Start in Editor with Current Line"),
68
f1: true,
69
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_HAS_AGENT, EditorContextKeys.writable),
70
keybinding: [{
71
when: CTX_INLINE_CHAT_SHOWING_HINT,
72
weight: KeybindingWeight.WorkbenchContrib + 1,
73
primary: KeyMod.CtrlCmd | KeyCode.KeyI
74
}, {
75
weight: KeybindingWeight.WorkbenchContrib,
76
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyI),
77
}]
78
});
79
}
80
81
override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {
82
const ctrl = InlineChatController.get(editor);
83
if (!ctrl || !editor.hasModel()) {
84
return;
85
}
86
87
const model = editor.getModel();
88
const lineNumber = editor.getSelection().positionLineNumber;
89
const lineContent = model.getLineContent(lineNumber);
90
91
const startColumn = model.getLineFirstNonWhitespaceColumn(lineNumber);
92
const endColumn = model.getLineMaxColumn(lineNumber);
93
94
// clear the line
95
let undoEdits: IValidEditOperation[] = [];
96
model.pushEditOperations(null, [EditOperation.replace(new Range(lineNumber, startColumn, lineNumber, endColumn), '')], (edits) => {
97
undoEdits = edits;
98
return null;
99
});
100
101
// trigger chat
102
const accepted = await ctrl.run({
103
autoSend: true,
104
message: lineContent.trim(),
105
position: new Position(lineNumber, startColumn)
106
});
107
108
if (!accepted) {
109
model.pushEditOperations(null, undoEdits, () => null);
110
}
111
}
112
}
113
114
export class ShowInlineChatHintAction extends EditorAction2 {
115
116
constructor() {
117
super({
118
id: 'inlineChat.showHint',
119
category: AbstractInline1ChatAction.category,
120
title: localize2('showHint', "Show Inline Chat Hint"),
121
f1: false,
122
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_HAS_AGENT, EditorContextKeys.writable),
123
});
124
}
125
126
override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: [uri: URI, position: IPosition, ...rest: any[]]) {
127
if (!editor.hasModel()) {
128
return;
129
}
130
131
const ctrl = InlineChatHintsController.get(editor);
132
if (!ctrl) {
133
return;
134
}
135
136
const [uri, position] = args;
137
if (!URI.isUri(uri) || !Position.isIPosition(position)) {
138
ctrl.hide();
139
return;
140
}
141
142
const model = editor.getModel();
143
if (!isEqual(model.uri, uri)) {
144
ctrl.hide();
145
return;
146
}
147
148
model.tokenization.forceTokenization(position.lineNumber);
149
const tokens = model.tokenization.getLineTokens(position.lineNumber);
150
151
let totalLength = 0;
152
let specialLength = 0;
153
let lastTokenType: StandardTokenType | undefined;
154
155
tokens.forEach(idx => {
156
const tokenType = tokens.getStandardTokenType(idx);
157
const startOffset = tokens.getStartOffset(idx);
158
const endOffset = tokens.getEndOffset(idx);
159
totalLength += endOffset - startOffset;
160
161
if (tokenType !== StandardTokenType.Other) {
162
specialLength += endOffset - startOffset;
163
}
164
lastTokenType = tokenType;
165
});
166
167
if (specialLength / totalLength > 0.25) {
168
ctrl.hide();
169
return;
170
}
171
if (lastTokenType === StandardTokenType.Comment) {
172
ctrl.hide();
173
return;
174
}
175
ctrl.show();
176
}
177
}
178
179
export class InlineChatHintsController extends Disposable implements IEditorContribution {
180
181
public static readonly ID = 'editor.contrib.inlineChatHints';
182
183
static get(editor: ICodeEditor): InlineChatHintsController | null {
184
return editor.getContribution<InlineChatHintsController>(InlineChatHintsController.ID);
185
}
186
187
private readonly _editor: ICodeEditor;
188
private readonly _ctxShowingHint: IContextKey<boolean>;
189
private readonly _visibilityObs = observableValue<boolean>(this, false);
190
191
constructor(
192
editor: ICodeEditor,
193
@IContextKeyService contextKeyService: IContextKeyService,
194
@ICommandService commandService: ICommandService,
195
@IKeybindingService keybindingService: IKeybindingService,
196
@IChatAgentService chatAgentService: IChatAgentService,
197
@IMarkerDecorationsService markerDecorationService: IMarkerDecorationsService,
198
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
199
@IConfigurationService private readonly _configurationService: IConfigurationService
200
) {
201
super();
202
this._editor = editor;
203
this._ctxShowingHint = CTX_INLINE_CHAT_SHOWING_HINT.bindTo(contextKeyService);
204
205
const ghostCtrl = InlineCompletionsController.get(editor);
206
207
this._store.add(commandService.onWillExecuteCommand(e => {
208
if (e.commandId === _inlineChatActionId || e.commandId === ACTION_START) {
209
this.hide();
210
}
211
}));
212
213
this._store.add(this._editor.onMouseDown(e => {
214
if (e.target.type !== MouseTargetType.CONTENT_TEXT) {
215
return;
216
}
217
if (!e.target.element?.classList.contains('inline-chat-hint-text')) {
218
return;
219
}
220
if (e.event.leftButton) {
221
commandService.executeCommand(_inlineChatActionId);
222
this.hide();
223
} else if (e.event.rightButton) {
224
e.event.preventDefault();
225
this._showContextMenu(e.event, e.target.element?.classList.contains('whitespace')
226
? InlineChatConfigKeys.LineEmptyHint
227
: InlineChatConfigKeys.LineNLHint
228
);
229
}
230
}));
231
232
const markerSuppression = this._store.add(new MutableDisposable());
233
const decos = this._editor.createDecorationsCollection();
234
235
const editorObs = observableCodeEditor(editor);
236
const keyObs = observableFromEvent(keybindingService.onDidUpdateKeybindings, _ => keybindingService.lookupKeybinding(ACTION_START)?.getLabel());
237
const configHintEmpty = observableConfigValue(InlineChatConfigKeys.LineEmptyHint, false, this._configurationService);
238
const configHintNL = observableConfigValue(InlineChatConfigKeys.LineNLHint, false, this._configurationService);
239
240
const showDataObs = derived((r) => {
241
const ghostState = ghostCtrl?.model.read(r)?.state.read(r);
242
243
const textFocus = editorObs.isTextFocused.read(r);
244
const position = editorObs.cursorPosition.read(r);
245
const model = editorObs.model.read(r);
246
247
const kb = keyObs.read(r);
248
249
if (ghostState !== undefined || !kb || !position || !model || !textFocus) {
250
return undefined;
251
}
252
253
if (IGNORED_LANGUAGE_IDS.has(model.getLanguageId())) {
254
return undefined;
255
}
256
257
editorObs.versionId.read(r);
258
259
const visible = this._visibilityObs.read(r);
260
const isEol = model.getLineMaxColumn(position.lineNumber) === position.column;
261
const isWhitespace = model.getLineLastNonWhitespaceColumn(position.lineNumber) === 0 && model.getValueLength() > 0 && position.column > 1;
262
263
if (isWhitespace) {
264
return configHintEmpty.read(r)
265
? { isEol, isWhitespace, kb, position, model }
266
: undefined;
267
}
268
269
if (visible && isEol && configHintNL.read(r)) {
270
return { isEol, isWhitespace, kb, position, model };
271
}
272
273
return undefined;
274
});
275
276
const style = createStyleSheet2();
277
this._store.add(style);
278
279
this._store.add(autorun(r => {
280
281
const showData = showDataObs.read(r);
282
if (!showData) {
283
decos.clear();
284
markerSuppression.clear();
285
this._ctxShowingHint.reset();
286
return;
287
}
288
289
const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)?.name ?? localize('defaultTitle', "Chat");
290
const { position, isEol, isWhitespace, kb, model } = showData;
291
292
const inlineClassName: string[] = ['a' /*HACK but sorts as we want*/, 'inline-chat-hint', 'inline-chat-hint-text'];
293
let content: string;
294
if (isWhitespace) {
295
content = '\u00a0' + localize('title2', "{0} to edit with {1}", kb, agentName);
296
} else if (isEol) {
297
content = '\u00a0' + localize('title1', "{0} to continue with {1}", kb, agentName);
298
} else {
299
content = '\u200a' + kb + '\u200a';
300
inlineClassName.push('embedded');
301
}
302
303
style.setStyle(`.inline-chat-hint-text::after { content: ${stringValue(content)} }`);
304
if (isWhitespace) {
305
inlineClassName.push('whitespace');
306
}
307
308
this._ctxShowingHint.set(true);
309
310
decos.set([{
311
range: Range.fromPositions(position),
312
options: {
313
description: 'inline-chat-hint-line',
314
showIfCollapsed: true,
315
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
316
afterContentClassName: inlineClassName.join(' '),
317
}
318
}]);
319
320
markerSuppression.value = markerDecorationService.addMarkerSuppression(model.uri, model.validateRange(new Range(position.lineNumber, 1, position.lineNumber, Number.MAX_SAFE_INTEGER)));
321
}));
322
}
323
324
private _showContextMenu(event: IMouseEvent, setting: string): void {
325
this._contextMenuService.showContextMenu({
326
getAnchor: () => ({ x: event.posx, y: event.posy }),
327
getActions: () => [
328
toAction({
329
id: 'inlineChat.disableHint',
330
label: localize('disableHint', "Disable Inline Chat Hint"),
331
run: async () => {
332
await this._configurationService.updateValue(setting, false);
333
}
334
})
335
]
336
});
337
}
338
339
show(): void {
340
this._visibilityObs.set(true, undefined);
341
}
342
343
hide(): void {
344
this._visibilityObs.set(false, undefined);
345
}
346
}
347
348
export class HideInlineChatHintAction extends EditorAction2 {
349
350
constructor() {
351
super({
352
id: 'inlineChat.hideHint',
353
title: localize2('hideHint', "Hide Inline Chat Hint"),
354
precondition: CTX_INLINE_CHAT_SHOWING_HINT,
355
keybinding: {
356
weight: KeybindingWeight.EditorContrib - 10,
357
primary: KeyCode.Escape
358
}
359
});
360
}
361
362
override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
363
InlineChatHintsController.get(editor)?.hide();
364
}
365
}
366
367