mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-23 10:08:49 +01:00
inline hint for empty lines, right-click to hide, differentiate between empty line and suffix hint (#234773)
This commit is contained in:
@@ -9,7 +9,7 @@ import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
|
||||
import { localize, localize2 } from '../../../../nls.js';
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { InlineChatController, State } from './inlineChatController.js';
|
||||
import { ACTION_START, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_VISIBLE } from '../common/inlineChat.js';
|
||||
import { ACTION_START, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';
|
||||
import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { EditOperation } from '../../../../editor/common/core/editOperation.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
@@ -20,8 +20,8 @@ import { InjectedTextCursorStops, IValidEditOperation, TrackedRangeStickiness }
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { isEqual } from '../../../../base/common/resources.js';
|
||||
import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js';
|
||||
import { autorun, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { autorun, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
|
||||
import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import './media/inlineChat.css';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
@@ -30,6 +30,12 @@ import { InlineCompletionsController } from '../../../../editor/contrib/inlineCo
|
||||
import { ChatAgentLocation, IChatAgentService } from '../../chat/common/chatAgents.js';
|
||||
import { MarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js';
|
||||
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
||||
import { toAction } from '../../../../base/common/actions.js';
|
||||
import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';
|
||||
|
||||
export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey<boolean>('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint"));
|
||||
|
||||
@@ -44,11 +50,14 @@ export class InlineChatExpandLineAction extends EditorAction2 {
|
||||
title: localize2('startWithCurrentLine', "Start in Editor with Current Line"),
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_HAS_AGENT, EditorContextKeys.writable),
|
||||
keybinding: {
|
||||
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),
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,6 +149,12 @@ export class ShowInlineChatHintAction extends EditorAction2 {
|
||||
}
|
||||
}
|
||||
|
||||
class HintData {
|
||||
constructor(
|
||||
readonly setting: string
|
||||
) { }
|
||||
}
|
||||
|
||||
export class InlineChatHintsController extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.inlineChatHints';
|
||||
@@ -151,6 +166,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont
|
||||
private readonly _editor: ICodeEditor;
|
||||
private readonly _ctxShowingHint: IContextKey<boolean>;
|
||||
private readonly _visibilityObs = observableValue<boolean>(this, false);
|
||||
private readonly _ctxMenuVisibleObs = observableValue<boolean>(this, false);
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@@ -158,70 +174,101 @@ export class InlineChatHintsController extends Disposable implements IEditorCont
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IChatAgentService chatAgentService: IChatAgentService,
|
||||
@IMarkerDecorationsService markerDecorationService: IMarkerDecorationsService
|
||||
@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(this._editor.onMouseDown(e => {
|
||||
if (e.target.type !== MouseTargetType.CONTENT_TEXT) {
|
||||
return;
|
||||
}
|
||||
if (e.target.detail.injectedText?.options.attachedData !== this) {
|
||||
const attachedData = e.target.detail.injectedText?.options.attachedData;
|
||||
if (!(attachedData instanceof HintData)) {
|
||||
return;
|
||||
}
|
||||
commandService.executeCommand(_inlineChatActionId);
|
||||
this.hide();
|
||||
}));
|
||||
|
||||
this._store.add(commandService.onWillExecuteCommand(e => {
|
||||
if (e.commandId === _inlineChatActionId) {
|
||||
if (e.event.leftButton) {
|
||||
commandService.executeCommand(_inlineChatActionId);
|
||||
this.hide();
|
||||
} else if (e.event.rightButton) {
|
||||
e.event.preventDefault();
|
||||
this._showContextMenu(e.event, attachedData.setting);
|
||||
}
|
||||
}));
|
||||
|
||||
const markerSuppression = this._store.add(new MutableDisposable());
|
||||
const decos = this._editor.createDecorationsCollection();
|
||||
|
||||
const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel());
|
||||
const posObs = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition());
|
||||
const editorObs = observableCodeEditor(editor);
|
||||
|
||||
const keyObs = observableFromEvent(keybindingService.onDidUpdateKeybindings, _ => keybindingService.lookupKeybinding(ACTION_START)?.getLabel());
|
||||
|
||||
const configSignal = observableFromEvent(Event.filter(_configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(InlineChatConfigKeys.LineEmptyHint) || e.affectsConfiguration(InlineChatConfigKeys.LineSuffixHint)), () => undefined);
|
||||
|
||||
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);
|
||||
|
||||
configSignal.read(r);
|
||||
|
||||
if (ghostState !== undefined || !kb || !position || !model || !textFocus) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const visible = this._visibilityObs.read(r);// || this._ctxMenuVisibleObs.read(r);
|
||||
const isEol = model.getLineMaxColumn(position.lineNumber) === position.column;
|
||||
const isWhitespace = model.getLineLastNonWhitespaceColumn(position.lineNumber) === 0 && model.getValueLength() > 0;
|
||||
|
||||
const shouldShow = (isWhitespace && _configurationService.getValue(InlineChatConfigKeys.LineEmptyHint))
|
||||
|| (visible && isEol && _configurationService.getValue(InlineChatConfigKeys.LineSuffixHint));
|
||||
|
||||
if (!shouldShow) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { isEol, isWhitespace, kb, position, model };
|
||||
});
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
|
||||
const ghostState = ghostCtrl?.model.read(r)?.state.read(r);
|
||||
const visible = this._visibilityObs.read(r);
|
||||
const kb = keyObs.read(r);
|
||||
const position = posObs.read(r);
|
||||
const model = modelObs.read(r);
|
||||
|
||||
// update context key
|
||||
this._ctxShowingHint.set(visible);
|
||||
|
||||
if (!visible || !kb || !position || ghostState !== undefined || !model) {
|
||||
const showData = showDataObs.read(r);
|
||||
if (!showData) {
|
||||
decos.clear();
|
||||
markerSuppression.clear();
|
||||
this._ctxShowingHint.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)?.fullName ?? localize('defaultTitle', "Chat");
|
||||
const isEol = model.getLineMaxColumn(position.lineNumber) === position.column;
|
||||
const { position, isEol, isWhitespace, kb, model } = showData;
|
||||
|
||||
const inlineClassName: string[] = ['inline-chat-hint'];
|
||||
let content: string;
|
||||
let inlineClassName: string;
|
||||
|
||||
if (isEol) {
|
||||
content = '\u00A0' + localize('title', "{0} to continue with {1}...", kb, agentName);
|
||||
inlineClassName = `inline-chat-hint${decos.length === 0 ? ' first' : ''}`;
|
||||
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 = 'inline-chat-hint embedded';
|
||||
inlineClassName.push('embedded');
|
||||
}
|
||||
|
||||
if (decos.length === 0) {
|
||||
inlineClassName.push('first');
|
||||
}
|
||||
|
||||
this._ctxShowingHint.set(true);
|
||||
|
||||
decos.set([{
|
||||
range: Range.fromPositions(position),
|
||||
options: {
|
||||
@@ -231,10 +278,10 @@ export class InlineChatHintsController extends Disposable implements IEditorCont
|
||||
hoverMessage: new MarkdownString(localize('toolttip', "Continue this with {0}...", agentName)),
|
||||
after: {
|
||||
content,
|
||||
inlineClassName,
|
||||
inlineClassName: inlineClassName.join(' '),
|
||||
inlineClassNameAffectsLetterSpacing: true,
|
||||
cursorStops: InjectedTextCursorStops.Both,
|
||||
attachedData: this
|
||||
cursorStops: InjectedTextCursorStops.None,
|
||||
attachedData: new HintData(isWhitespace ? InlineChatConfigKeys.LineEmptyHint : InlineChatConfigKeys.LineSuffixHint)
|
||||
}
|
||||
}
|
||||
}]);
|
||||
@@ -243,6 +290,23 @@ export class InlineChatHintsController extends Disposable implements IEditorCont
|
||||
}));
|
||||
}
|
||||
|
||||
private _showContextMenu(event: IMouseEvent, setting: string): void {
|
||||
this._ctxMenuVisibleObs.set(true, undefined);
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => ({ x: event.posx, y: event.posy }),
|
||||
onHide: () => this._ctxMenuVisibleObs.set(false, undefined),
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user