renderer side inline hint and cmd to show/hide (#234421)

This commit is contained in:
Johannes Rieken
2024-11-22 14:36:09 +00:00
committed by GitHub
parent d8d0ddba9c
commit eddef09fa7
5 changed files with 205 additions and 102 deletions

View File

@@ -3,124 +3,49 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ICodeEditor, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';
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 { IChatAgentService } from '../../chat/common/chatAgents.js';
import { InlineChatController, State } from './inlineChatController.js';
import { 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 } 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';
import { Position } from '../../../../editor/common/core/position.js';
import { IPosition, Position } from '../../../../editor/common/core/position.js';
import { AbstractInlineChatAction } from './inlineChatActions.js';
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
import { IValidEditOperation } from '../../../../editor/common/model.js';
import { InjectedTextCursorStops, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js';
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 { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import './media/inlineChat.css';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js';
export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey<boolean>('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint"));
export const CTX_INLINE_CHAT_EXPANSION = new RawContextKey<boolean>('inlineChatExpansion', false, localize('inlineChatExpansion', "Whether the inline chat expansion is enabled when at the end of a just-typed line"));
export class InlineChatExansionContextKey implements IEditorContribution {
static Id = 'editor.inlineChatExpansion';
private readonly _store = new DisposableStore();
private readonly _editorListener = this._store.add(new MutableDisposable());
private readonly _ctxInlineChatExpansion: IContextKey<boolean>;
constructor(
editor: ICodeEditor,
@IContextKeyService contextKeyService: IContextKeyService,
@IChatAgentService chatAgentService: IChatAgentService
) {
this._ctxInlineChatExpansion = CTX_INLINE_CHAT_EXPANSION.bindTo(contextKeyService);
const update = () => {
if (editor.hasModel() && chatAgentService.getAgents().length > 0) {
this._install(editor);
} else {
this._uninstall();
}
};
this._store.add(chatAgentService.onDidChangeAgents(update));
this._store.add(editor.onDidChangeModel(update));
update();
}
dispose(): void {
this._ctxInlineChatExpansion.reset();
this._store.dispose();
}
private _install(editor: IActiveCodeEditor): void {
const store = new DisposableStore();
this._editorListener.value = store;
const model = editor.getModel();
const lastChangeEnds: number[] = [];
store.add(editor.onDidChangeCursorPosition(e => {
let enabled = false;
if (e.reason === CursorChangeReason.NotSet) {
const position = editor.getPosition();
const positionOffset = model.getOffsetAt(position);
const lineLength = model.getLineLength(position.lineNumber);
const firstNonWhitespace = model.getLineFirstNonWhitespaceColumn(position.lineNumber);
if (firstNonWhitespace !== 0 && position.column > lineLength && lastChangeEnds.includes(positionOffset)) {
enabled = true;
}
}
lastChangeEnds.length = 0;
this._ctxInlineChatExpansion.set(enabled);
}));
store.add(editor.onDidChangeModelContent(e => {
lastChangeEnds.length = 0;
for (const change of e.changes) {
const changeEnd = change.rangeOffset + change.text.length;
lastChangeEnds.push(changeEnd);
}
queueMicrotask(() => {
if (lastChangeEnds.length > 0) {
// this is a signal that onDidChangeCursorPosition didn't run which means some outside change
// which means we should disable the context key
this._ctxInlineChatExpansion.set(false);
}
});
}));
}
private _uninstall(): void {
this._editorListener.clear();
}
}
const _inlineChatActionId = 'inlineChat.startWithCurrentLine';
export class InlineChatExpandLineAction extends EditorAction2 {
constructor() {
super({
id: 'inlineChat.startWithCurrentLine',
id: _inlineChatActionId,
category: AbstractInlineChatAction.category,
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: {
// when: CTX_INLINE_CHAT_EXPANSION,
// weight: KeybindingWeight.EditorContrib,
// primary: KeyCode.Tab
// }
keybinding: {
when: CTX_INLINE_CHAT_SHOWING_HINT,
weight: KeybindingWeight.WorkbenchContrib + 1,
primary: KeyMod.CtrlCmd | KeyCode.KeyI
}
});
}
@@ -164,3 +89,167 @@ export class InlineChatExpandLineAction extends EditorAction2 {
}
}
}
export class ShowInlineChatHintAction extends EditorAction2 {
constructor() {
super({
id: 'inlineChat.showHint',
category: AbstractInlineChatAction.category,
title: localize2('showHint', "Show Inline Chat Hint"),
f1: false,
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_HAS_AGENT, EditorContextKeys.writable),
});
}
override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: [uri: URI, position: IPosition, ...rest: any[]]) {
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);
const tokenIndex = tokens.findTokenIndexAtOffset(position.column - 1);
const tokenType = tokens.getStandardTokenType(tokenIndex);
if (tokenType === StandardTokenType.Comment) {
ctrl.hide();
} else {
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,
) {
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) {
return;
}
commandService.executeCommand(_inlineChatActionId);
this.hide();
}));
this._store.add(commandService.onWillExecuteCommand(e => {
if (e.commandId === _inlineChatActionId) {
this.hide();
}
}));
const posObs = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition());
const decos = this._editor.createDecorationsCollection();
const keyObs = observableFromEvent(keybindingService.onDidUpdateKeybindings, _ => keybindingService.lookupKeybinding(ACTION_START)?.getLabel());
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);
// update context key
this._ctxShowingHint.set(visible);
if (!visible || !kb || !position || ghostState !== undefined) {
decos.clear();
return;
}
const column = this._editor.getModel()?.getLineMaxColumn(position.lineNumber);
if (!column) {
return;
}
const range = Range.fromPositions(position);
decos.set([{
range,
options: {
description: 'inline-chat-hint-line',
showIfCollapsed: true,
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
after: {
inlineClassName: 'inline-chat-hint',
content: '\u00A0' + localize('ddd', "{0} to chat", kb),
inlineClassNameAffectsLetterSpacing: true,
cursorStops: InjectedTextCursorStops.Both,
attachedData: this
}
}
}]);
}));
}
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();
}
}