|
|
|
|
@@ -1,366 +0,0 @@
|
|
|
|
|
/*---------------------------------------------------------------------------------------------
|
|
|
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
|
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
|
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
|
|
|
|
import { stringValue } from '../../../../base/browser/cssValue.js';
|
|
|
|
|
import { createStyleSheet2 } from '../../../../base/browser/domStylesheets.js';
|
|
|
|
|
import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';
|
|
|
|
|
import { toAction } from '../../../../base/common/actions.js';
|
|
|
|
|
import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
|
|
|
|
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
|
|
|
|
|
import { autorun, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
|
|
|
|
|
import { isEqual } from '../../../../base/common/resources.js';
|
|
|
|
|
import { URI } from '../../../../base/common/uri.js';
|
|
|
|
|
import { ICodeEditor, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';
|
|
|
|
|
import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
|
|
|
|
import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';
|
|
|
|
|
import { EditOperation } from '../../../../editor/common/core/editOperation.js';
|
|
|
|
|
import { IPosition, Position } from '../../../../editor/common/core/position.js';
|
|
|
|
|
import { Range } from '../../../../editor/common/core/range.js';
|
|
|
|
|
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
|
|
|
|
|
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
|
|
|
|
|
import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js';
|
|
|
|
|
import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js';
|
|
|
|
|
import { IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js';
|
|
|
|
|
import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js';
|
|
|
|
|
import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js';
|
|
|
|
|
import { localize, localize2 } from '../../../../nls.js';
|
|
|
|
|
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
|
|
|
|
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
|
|
|
|
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
|
|
|
|
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
|
|
|
|
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
|
|
|
|
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
|
|
|
|
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
|
|
|
|
|
import { IChatAgentService } from '../../chat/common/chatAgents.js';
|
|
|
|
|
import { ChatAgentLocation } from '../../chat/common/constants.js';
|
|
|
|
|
import { AGENT_FILE_EXTENSION, LEGACY_MODE_FILE_EXTENSION } from '../../chat/common/promptSyntax/config/promptFileLocations.js';
|
|
|
|
|
import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../chat/common/promptSyntax/promptTypes.js';
|
|
|
|
|
import { ACTION_START, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';
|
|
|
|
|
import { AbstractInline1ChatAction } from './inlineChatActions.js';
|
|
|
|
|
import { InlineChatController } from './inlineChatController.js';
|
|
|
|
|
import './media/inlineChat.css';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set of language IDs where inline chat hints should not be shown.
|
|
|
|
|
*/
|
|
|
|
|
const IGNORED_LANGUAGE_IDS = new Set([
|
|
|
|
|
PLAINTEXT_LANGUAGE_ID,
|
|
|
|
|
'markdown',
|
|
|
|
|
'search-result',
|
|
|
|
|
INSTRUCTIONS_LANGUAGE_ID,
|
|
|
|
|
PROMPT_LANGUAGE_ID,
|
|
|
|
|
LEGACY_MODE_FILE_EXTENSION,
|
|
|
|
|
AGENT_FILE_EXTENSION
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey<boolean>('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint"));
|
|
|
|
|
|
|
|
|
|
const _inlineChatActionId = 'inlineChat.startWithCurrentLine';
|
|
|
|
|
|
|
|
|
|
export class InlineChatExpandLineAction extends EditorAction2 {
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super({
|
|
|
|
|
id: _inlineChatActionId,
|
|
|
|
|
category: AbstractInline1ChatAction.category,
|
|
|
|
|
title: localize2('startWithCurrentLine', "Start in Editor with Current Line"),
|
|
|
|
|
f1: true,
|
|
|
|
|
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_V1_ENABLED, EditorContextKeys.writable),
|
|
|
|
|
keybinding: [{
|
|
|
|
|
when: CTX_INLINE_CHAT_SHOWING_HINT,
|
|
|
|
|
weight: KeybindingWeight.WorkbenchContrib + 1,
|
|
|
|
|
primary: KeyMod.CtrlCmd | KeyCode.KeyI
|
|
|
|
|
}, {
|
|
|
|
|
weight: KeybindingWeight.WorkbenchContrib,
|
|
|
|
|
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyI),
|
|
|
|
|
}]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {
|
|
|
|
|
const ctrl = InlineChatController.get(editor);
|
|
|
|
|
if (!ctrl || !editor.hasModel()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const model = editor.getModel();
|
|
|
|
|
const lineNumber = editor.getSelection().positionLineNumber;
|
|
|
|
|
const lineContent = model.getLineContent(lineNumber);
|
|
|
|
|
|
|
|
|
|
const startColumn = model.getLineFirstNonWhitespaceColumn(lineNumber);
|
|
|
|
|
const endColumn = model.getLineMaxColumn(lineNumber);
|
|
|
|
|
|
|
|
|
|
// clear the line
|
|
|
|
|
let undoEdits: IValidEditOperation[] = [];
|
|
|
|
|
model.pushEditOperations(null, [EditOperation.replace(new Range(lineNumber, startColumn, lineNumber, endColumn), '')], (edits) => {
|
|
|
|
|
undoEdits = edits;
|
|
|
|
|
return null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// trigger chat
|
|
|
|
|
const accepted = await ctrl.run({
|
|
|
|
|
autoSend: true,
|
|
|
|
|
message: lineContent.trim(),
|
|
|
|
|
position: new Position(lineNumber, startColumn)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!accepted) {
|
|
|
|
|
model.pushEditOperations(null, undoEdits, () => null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ShowInlineChatHintAction extends EditorAction2 {
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super({
|
|
|
|
|
id: 'inlineChat.showHint',
|
|
|
|
|
category: AbstractInline1ChatAction.category,
|
|
|
|
|
title: localize2('showHint', "Show Inline Chat Hint"),
|
|
|
|
|
f1: false,
|
|
|
|
|
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_V1_ENABLED, EditorContextKeys.writable),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: [uri: URI, position: IPosition, ...rest: unknown[]]) {
|
|
|
|
|
if (!editor.hasModel()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ctrl = InlineChatHintsController.get(editor);
|
|
|
|
|
if (!ctrl) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [uri, position] = args;
|
|
|
|
|
if (!URI.isUri(uri) || !Position.isIPosition(position)) {
|
|
|
|
|
ctrl.hide();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const model = editor.getModel();
|
|
|
|
|
if (!isEqual(model.uri, uri)) {
|
|
|
|
|
ctrl.hide();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
model.tokenization.forceTokenization(position.lineNumber);
|
|
|
|
|
const tokens = model.tokenization.getLineTokens(position.lineNumber);
|
|
|
|
|
|
|
|
|
|
let totalLength = 0;
|
|
|
|
|
let specialLength = 0;
|
|
|
|
|
let lastTokenType: StandardTokenType | undefined;
|
|
|
|
|
|
|
|
|
|
tokens.forEach(idx => {
|
|
|
|
|
const tokenType = tokens.getStandardTokenType(idx);
|
|
|
|
|
const startOffset = tokens.getStartOffset(idx);
|
|
|
|
|
const endOffset = tokens.getEndOffset(idx);
|
|
|
|
|
totalLength += endOffset - startOffset;
|
|
|
|
|
|
|
|
|
|
if (tokenType !== StandardTokenType.Other) {
|
|
|
|
|
specialLength += endOffset - startOffset;
|
|
|
|
|
}
|
|
|
|
|
lastTokenType = tokenType;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (specialLength / totalLength > 0.25) {
|
|
|
|
|
ctrl.hide();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (lastTokenType === StandardTokenType.Comment) {
|
|
|
|
|
ctrl.hide();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ctrl.show();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class InlineChatHintsController extends Disposable implements IEditorContribution {
|
|
|
|
|
|
|
|
|
|
public static readonly ID = 'editor.contrib.inlineChatHints';
|
|
|
|
|
|
|
|
|
|
static get(editor: ICodeEditor): InlineChatHintsController | null {
|
|
|
|
|
return editor.getContribution<InlineChatHintsController>(InlineChatHintsController.ID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private readonly _editor: ICodeEditor;
|
|
|
|
|
private readonly _ctxShowingHint: IContextKey<boolean>;
|
|
|
|
|
private readonly _visibilityObs = observableValue<boolean>(this, false);
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
editor: ICodeEditor,
|
|
|
|
|
@IContextKeyService contextKeyService: IContextKeyService,
|
|
|
|
|
@ICommandService commandService: ICommandService,
|
|
|
|
|
@IKeybindingService keybindingService: IKeybindingService,
|
|
|
|
|
@IChatAgentService chatAgentService: IChatAgentService,
|
|
|
|
|
@IMarkerDecorationsService markerDecorationService: IMarkerDecorationsService,
|
|
|
|
|
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
|
|
|
|
|
@IConfigurationService private readonly _configurationService: IConfigurationService
|
|
|
|
|
) {
|
|
|
|
|
super();
|
|
|
|
|
this._editor = editor;
|
|
|
|
|
this._ctxShowingHint = CTX_INLINE_CHAT_SHOWING_HINT.bindTo(contextKeyService);
|
|
|
|
|
|
|
|
|
|
const ghostCtrl = InlineCompletionsController.get(editor);
|
|
|
|
|
|
|
|
|
|
this._store.add(commandService.onWillExecuteCommand(e => {
|
|
|
|
|
if (e.commandId === _inlineChatActionId || e.commandId === ACTION_START) {
|
|
|
|
|
this.hide();
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
this._store.add(this._editor.onMouseDown(e => {
|
|
|
|
|
if (e.target.type !== MouseTargetType.CONTENT_TEXT) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!e.target.element?.classList.contains('inline-chat-hint-text')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.event.leftButton) {
|
|
|
|
|
commandService.executeCommand(_inlineChatActionId);
|
|
|
|
|
this.hide();
|
|
|
|
|
} else if (e.event.rightButton) {
|
|
|
|
|
e.event.preventDefault();
|
|
|
|
|
this._showContextMenu(e.event, e.target.element?.classList.contains('whitespace')
|
|
|
|
|
? InlineChatConfigKeys.LineEmptyHint
|
|
|
|
|
: InlineChatConfigKeys.LineNLHint
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const markerSuppression = this._store.add(new MutableDisposable());
|
|
|
|
|
const decos = this._editor.createDecorationsCollection();
|
|
|
|
|
|
|
|
|
|
const editorObs = observableCodeEditor(editor);
|
|
|
|
|
const keyObs = observableFromEvent(keybindingService.onDidUpdateKeybindings, _ => keybindingService.lookupKeybinding(ACTION_START)?.getLabel());
|
|
|
|
|
const configHintEmpty = observableConfigValue(InlineChatConfigKeys.LineEmptyHint, false, this._configurationService);
|
|
|
|
|
const configHintNL = observableConfigValue(InlineChatConfigKeys.LineNLHint, false, this._configurationService);
|
|
|
|
|
|
|
|
|
|
const showDataObs = derived((r) => {
|
|
|
|
|
const ghostState = ghostCtrl?.model.read(r)?.state.read(r);
|
|
|
|
|
|
|
|
|
|
const textFocus = editorObs.isTextFocused.read(r);
|
|
|
|
|
const position = editorObs.cursorPosition.read(r);
|
|
|
|
|
const model = editorObs.model.read(r);
|
|
|
|
|
|
|
|
|
|
const kb = keyObs.read(r);
|
|
|
|
|
|
|
|
|
|
if (ghostState !== undefined || !kb || !position || !model || !textFocus) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IGNORED_LANGUAGE_IDS.has(model.getLanguageId())) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
editorObs.versionId.read(r);
|
|
|
|
|
|
|
|
|
|
const visible = this._visibilityObs.read(r);
|
|
|
|
|
const isEol = model.getLineMaxColumn(position.lineNumber) === position.column;
|
|
|
|
|
const isWhitespace = model.getLineLastNonWhitespaceColumn(position.lineNumber) === 0 && model.getValueLength() > 0 && position.column > 1;
|
|
|
|
|
|
|
|
|
|
if (isWhitespace) {
|
|
|
|
|
return configHintEmpty.read(r)
|
|
|
|
|
? { isEol, isWhitespace, kb, position, model }
|
|
|
|
|
: undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (visible && isEol && configHintNL.read(r)) {
|
|
|
|
|
return { isEol, isWhitespace, kb, position, model };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const style = createStyleSheet2();
|
|
|
|
|
this._store.add(style);
|
|
|
|
|
|
|
|
|
|
this._store.add(autorun(r => {
|
|
|
|
|
|
|
|
|
|
const showData = showDataObs.read(r);
|
|
|
|
|
if (!showData) {
|
|
|
|
|
decos.clear();
|
|
|
|
|
markerSuppression.clear();
|
|
|
|
|
this._ctxShowingHint.reset();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)?.name ?? localize('defaultTitle', "Chat");
|
|
|
|
|
const { position, isEol, isWhitespace, kb, model } = showData;
|
|
|
|
|
|
|
|
|
|
const inlineClassName: string[] = ['a' /*HACK but sorts as we want*/, 'inline-chat-hint', 'inline-chat-hint-text'];
|
|
|
|
|
let content: string;
|
|
|
|
|
if (isWhitespace) {
|
|
|
|
|
content = '\u00a0' + localize('title2', "{0} to edit with {1}", kb, agentName);
|
|
|
|
|
} else if (isEol) {
|
|
|
|
|
content = '\u00a0' + localize('title1', "{0} to continue with {1}", kb, agentName);
|
|
|
|
|
} else {
|
|
|
|
|
content = '\u200a' + kb + '\u200a';
|
|
|
|
|
inlineClassName.push('embedded');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
style.setStyle(`.inline-chat-hint-text::after { content: ${stringValue(content)} }`);
|
|
|
|
|
if (isWhitespace) {
|
|
|
|
|
inlineClassName.push('whitespace');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._ctxShowingHint.set(true);
|
|
|
|
|
|
|
|
|
|
decos.set([{
|
|
|
|
|
range: Range.fromPositions(position),
|
|
|
|
|
options: {
|
|
|
|
|
description: 'inline-chat-hint-line',
|
|
|
|
|
showIfCollapsed: true,
|
|
|
|
|
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
|
|
|
|
afterContentClassName: inlineClassName.join(' '),
|
|
|
|
|
}
|
|
|
|
|
}]);
|
|
|
|
|
|
|
|
|
|
markerSuppression.value = markerDecorationService.addMarkerSuppression(model.uri, model.validateRange(new Range(position.lineNumber, 1, position.lineNumber, Number.MAX_SAFE_INTEGER)));
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _showContextMenu(event: IMouseEvent, setting: string): void {
|
|
|
|
|
this._contextMenuService.showContextMenu({
|
|
|
|
|
getAnchor: () => ({ x: event.posx, y: event.posy }),
|
|
|
|
|
getActions: () => [
|
|
|
|
|
toAction({
|
|
|
|
|
id: 'inlineChat.disableHint',
|
|
|
|
|
label: localize('disableHint', "Disable Inline Chat Hint"),
|
|
|
|
|
run: async () => {
|
|
|
|
|
await this._configurationService.updateValue(setting, false);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
show(): void {
|
|
|
|
|
this._visibilityObs.set(true, undefined);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hide(): void {
|
|
|
|
|
this._visibilityObs.set(false, undefined);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class HideInlineChatHintAction extends EditorAction2 {
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super({
|
|
|
|
|
id: 'inlineChat.hideHint',
|
|
|
|
|
title: localize2('hideHint', "Hide Inline Chat Hint"),
|
|
|
|
|
precondition: CTX_INLINE_CHAT_SHOWING_HINT,
|
|
|
|
|
keybinding: {
|
|
|
|
|
weight: KeybindingWeight.EditorContrib - 10,
|
|
|
|
|
primary: KeyCode.Escape
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
|
|
|
|
InlineChatHintsController.get(editor)?.hide();
|
|
|
|
|
}
|
|
|
|
|
}
|