diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 2cc76db29aa..64d4f2a11d9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -24,7 +24,7 @@ import { localize } from '../../../../nls.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; -import { InlineChatExansionContextKey, InlineChatExpandLineAction } from './inlineChatCurrentLine.js'; +import { InlineChatExpandLineAction, InlineChatHintsController, HideInlineChatHintAction, ShowInlineChatHintAction } from './inlineChatCurrentLine.js'; // --- browser @@ -34,8 +34,10 @@ registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, Instant registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -registerEditorContribution(InlineChatExansionContextKey.Id, InlineChatExansionContextKey, EditorContributionInstantiation.BeforeFirstInteraction); registerAction2(InlineChatExpandLineAction); +registerAction2(ShowInlineChatHintAction); +registerAction2(HideInlineChatHintAction); +registerEditorContribution(InlineChatHintsController.ID, InlineChatHintsController, EditorContributionInstantiation.Lazy); // --- MENU special --- diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 84f00a76493..b101d5c1d59 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START } from '../common/inlineChat.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; @@ -49,7 +49,7 @@ export class StartSessionAction extends EditorAction2 { constructor() { super({ - id: 'inlineChat.start', + id: ACTION_START, title: localize2('run', 'Editor Inline Chat'), category: AbstractInlineChatAction.category, f1: true, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index d3df7bce374..78a792955d1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -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('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); -export const CTX_INLINE_CHAT_EXPANSION = new RawContextKey('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; - - 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.ID); + } + + private readonly _editor: ICodeEditor; + private readonly _ctxShowingHint: IContextKey; + private readonly _visibilityObs = observableValue(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 { + InlineChatHintsController.get(editor)?.hide(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index d5c7d7ee031..5b10211a242 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -336,3 +336,14 @@ .monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { opacity: 1; } + + +/* HINT */ + +.monaco-workbench .monaco-editor .inline-chat-hint { + /* padding: 0 8px; */ + cursor: pointer; + color: var(--vscode-editorGhostText-foreground); + border: 1px solid var(--vscode-editorGhostText-border); + border-radius: 3px; +} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index ba46ea0192f..e2912601816 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -98,6 +98,7 @@ export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey