diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 51d8c95ae17..a965cfcdb9f 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -790,8 +790,6 @@ "--vscode-hover-maxWidth", "--vscode-hover-sourceWhiteSpace", "--vscode-hover-whiteSpace", - "--vscode-inline-chat-cropped", - "--vscode-inline-chat-expanded", "--vscode-inline-chat-quick-voice-height", "--vscode-inline-chat-quick-voice-width", "--vscode-editor-dictation-widget-height", diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index ea70b146a8c..c6a996a178c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -51,8 +51,6 @@ registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.PreviousFromHistory); registerAction2(InlineChatActions.NextFromHistory); registerAction2(InlineChatActions.ViewInChatAction); -registerAction2(InlineChatActions.ExpandMessageAction); -registerAction2(InlineChatActions.ContractMessageAction); registerAction2(InlineChatActions.ToggleDiffForChange); registerAction2(InlineChatActions.FeebackHelpfulCommand); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 7d299bbc1e5..21c2b268b5e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -175,37 +175,13 @@ cursor: pointer; } -/* .monaco-editor .inline-chat .markdownMessage .message * { - margin: unset; -} - -.monaco-editor .inline-chat .markdownMessage .message code { - font-family: var(--monaco-monospace-font); - font-size: 12px; - color: var(--vscode-textPreformat-foreground); - background-color: var(--vscode-textPreformat-background); - padding: 1px 3px; - border-radius: 4px; -} */ - .monaco-editor .inline-chat .chatMessage .chatMessageContent .value { - -webkit-line-clamp: initial; - -webkit-box-orient: vertical; overflow: hidden; - display: -webkit-box; -webkit-user-select: text; user-select: text; } -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="cropped"] .value { - -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); -} - -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="expanded"] .value { - -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); -} - .monaco-editor .inline-chat .followUps { padding: 5px 5px; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 75ca16a1f24..7a6b95e0767 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 'vs/editor/browser/widget/diffEditor/em import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -737,46 +737,6 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } -export class ExpandMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.expandMessageAction', - title: localize('expandMessage', 'Show More'), - icon: Codicon.chevronDown, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('cropped')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(true); - } -} - -export class ContractMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.contractMessageAction', - title: localize('contractMessage', 'Show Less'), - icon: Codicon.chevronUp, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('expanded')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(false); - } -} - export class InlineAccessibilityHelpContribution extends Disposable { constructor() { super(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index cfb18aa49f0..e2feff8c52f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -39,10 +39,11 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IInlineChatSavingService } from './inlineChatSavingService'; -import { EmptyResponse, ErrorResponse, ExpansionState, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { IInlineChatMessageAppender } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { StashedSession } from './inlineChatSession'; @@ -379,7 +380,6 @@ export class InlineChatController implements IEditorContribution { this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []); this._updatePlaceholder(); this._zone.value.widget.updateInfo(this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); - this._zone.value.widget.preferredExpansionState = this._session.lastExpansionState; this._zone.value.widget.value = this._session.session.input ?? this._session.lastInput?.value ?? this._zone.value.widget.value; if (this._session.session.input) { this._zone.value.widget.selectAll(); @@ -795,8 +795,6 @@ export class InlineChatController implements IEditorContribution { const message = { message: response.mdContent, providerId: this._session.provider.debugName, requestId: response.requestId }; this._zone.value.widget.updateChatMessage(message); - //this._zone.value.widget.updateMarkdownMessage(response.mdContent); - this._session.lastExpansionState = this._zone.value.widget.expansionState; this._zone.value.widget.updateToolbar(true); newPosition = await this._strategy.renderChanges(response); @@ -1088,14 +1086,6 @@ export class InlineChatController implements IEditorContribution { } } - updateExpansionState(expand: boolean) { - if (this._session) { - const expansionState = expand ? ExpansionState.EXPANDED : ExpansionState.CROPPED; - this._zone.value.widget.updateChatMessageExpansionState(expansionState); - this._session.lastExpansionState = expansionState; - } - } - toggleDiff() { this._strategy?.toggleDiff?.(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index a3e3865631d..109875de35f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -67,11 +67,6 @@ export type TelemetryDataClassification = { responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; }; -export enum ExpansionState { - EXPANDED = 'expanded', - CROPPED = 'cropped', - NOT_CROPPED = 'not_cropped' -} export class SessionWholeRange { @@ -142,7 +137,6 @@ export class SessionWholeRange { export class Session { private _lastInput: SessionPrompt | undefined; - private _lastExpansionState: ExpansionState | undefined; private _isUnstashed: boolean = false; private readonly _exchange: SessionExchange[] = []; private readonly _startTime = new Date(); @@ -204,14 +198,6 @@ export class Session { this._isUnstashed = true; } - get lastExpansionState(): ExpansionState | undefined { - return this._lastExpansionState; - } - - set lastExpansionState(state: ExpansionState) { - this._lastExpansionState = state; - } - get textModelNSnapshotAltVersion(): number | undefined { return this._textModelNSnapshotAltVersion; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 30aa3f46b10..500d978c389 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -30,7 +30,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { InlineChatFileCreatePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget'; import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { HunkState } from './inlineChatSession'; import { assertType } from 'vs/base/common/types'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 6f1cd922882..67cb9f527c0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -13,7 +13,6 @@ import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ISettableObservable, constObservable, derived, observableValue } from 'vs/base/common/observable'; -import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./inlineChat'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; @@ -23,10 +22,10 @@ import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/co import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; -import { EditorLayoutInfo, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; @@ -37,7 +36,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; @@ -63,11 +61,12 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; -import { ExpansionState, HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { asRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_VISIBLE, IInlineChatFollowup, IInlineChatSlashCommand, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -137,9 +136,25 @@ export interface InlineChatWidgetViewState { } export interface IInlineChatWidgetConstructionOptions { - menuId: MenuId; + /** + * The telemetry source for all commands of this widget + */ + telemetrySource: string; + /** + * The menu that is inside the input editor, use for send, dictation + */ + inputMenuId: MenuId; + /** + * The menu that next to the input editor, use for close, config etc + */ widgetMenuId: MenuId; - statusMenuId: MenuId; + /** + * The menu that rendered as button bar, use for accept, discard etc + */ + statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions }; + /** + * The men that rendered in the lower right corner, use for feedback + */ feedbackMenuId: MenuId; } @@ -177,10 +192,7 @@ export class InlineChatWidget { h('div.previewDiff.hidden@previewDiff'), h('div.previewCreateTitle.show-file-icons@previewCreateTitle'), h('div.previewCreate.hidden@previewCreate'), - h('div.chatMessage.hidden@chatMessage', [ - h('div.chatMessageContent@chatMessageContent'), - h('div.messageActions@messageActions') - ]), + h('div.chatMessage.hidden@chatMessage'), h('div.followUps.hidden@followUps'), h('div.accessibleViewer@accessibleViewer'), h('div.status@status', [ @@ -192,13 +204,15 @@ export class InlineChatWidget { ] ); + private readonly _chatMessageContents: HTMLDivElement; + private readonly _chatMessageScrollable: DomScrollableElement; + private readonly _store = new DisposableStore(); private readonly _slashCommands = this._store.add(new DisposableStore()); private readonly _inputEditor: IActiveCodeEditor; private readonly _inputModel: ITextModel; private readonly _ctxInputEmpty: IContextKey; - private readonly _ctxMessageCropState: IContextKey<'cropped' | 'not_cropped' | 'expanded'>; private readonly _ctxInnerCursorFirst: IContextKey; private readonly _ctxInnerCursorLast: IContextKey; private readonly _ctxInnerCursorStart: IContextKey; @@ -229,8 +243,6 @@ export class InlineChatWidget { private _lastDim: Dimension | undefined; private _isLayouting: boolean = false; - private _preferredExpansionState: ExpansionState | undefined; - private _expansionState: ExpansionState = ExpansionState.NOT_CROPPED; private _slashCommandDetails: { command: string; detail: string }[] = []; private _slashCommandContentWidget: SlashCommandContentWidget; @@ -244,8 +256,8 @@ export class InlineChatWidget { private readonly _codeBlockModelCollection: CodeBlockModelCollection; constructor( - private readonly parentEditor: ICodeEditor, - _options: IInlineChatWidgetConstructionOptions, + private readonly _parentEditor: ICodeEditor, + options: IInlineChatWidgetConstructionOptions, @IModelService private readonly _modelService: IModelService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @@ -268,20 +280,13 @@ export class InlineChatWidget { ]) }; - this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this.parentEditor); + this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this._parentEditor); this._updateAriaLabel(); this._store.add(this._inputEditor); this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); - this._store.add(addDisposableListener(this._elements.chatMessageContent, 'focus', () => this._ctxResponseFocused.set(true))); - this._store.add(addDisposableListener(this._elements.chatMessageContent, 'blur', () => this._ctxResponseFocused.reset())); - this._store.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._updateAriaLabel(); - } - })); const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${InlineChatWidget._modelPool++}.txt` }); this._inputModel = this._store.add(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri)); @@ -289,10 +294,24 @@ export class InlineChatWidget { this._editorOptions = this._store.add(_instantiationService.createInstance(ChatEditorOptions, undefined, editorForeground, inputBackground, editorBackground)); + this._chatMessageContents = document.createElement('div'); + this._chatMessageContents.className = 'chatMessageContent'; + this._chatMessageContents.tabIndex = 0; + this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._chatMessageScrollable = new DomScrollableElement(this._chatMessageContents, { alwaysConsumeMouseWheel: true }); + this._store.add(this._chatMessageScrollable); + this._elements.chatMessage.appendChild(this._chatMessageScrollable.getDomNode()); + this._store.add(addDisposableListener(this._chatMessageContents, 'focus', () => this._ctxResponseFocused.set(true))); + this._store.add(addDisposableListener(this._chatMessageContents, 'blur', () => this._ctxResponseFocused.reset())); + + this._store.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { + this._updateAriaLabel(); + } + })); // --- context keys - this._ctxMessageCropState = CTX_INLINE_CHAT_MESSAGE_CROP_STATE.bindTo(this._contextKeyService); this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(this._contextKeyService); this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); @@ -381,8 +400,8 @@ export class InlineChatWidget { // toolbars - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, _options.menuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, options.inputMenuId, { + telemetrySource: options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu hoverDelegate @@ -392,25 +411,17 @@ export class InlineChatWidget { this._store.add(this._progressBar); - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, _options.widgetMenuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, options.widgetMenuId, { + telemetrySource: options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hoverDelegate })); - const workbenchMenubarOptions: IWorkbenchButtonBarOptions = { - telemetrySource: 'interactiveEditorWidget-toolbar', - buttonConfigProvider: action => { - if (action.id === ACTION_REGENERATE_RESPONSE) { - return { showIcon: true, showLabel: false, isSecondary: true }; - } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { - return { isSecondary: false }; - } else { - return { isSecondary: true }; - } - } - }; - const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, _options.statusMenuId, workbenchMenubarOptions); + + const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; + const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; + + const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); @@ -423,7 +434,7 @@ export class InlineChatWidget { } }; - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, _options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); + const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(feedbackToolbar); @@ -432,24 +443,19 @@ export class InlineChatWidget { useInlineViewWhenSpaceIsLimited: false, ..._previewEditorEditorOptions, onlyShowAccessibleDiffViewer: this._accessibilityService.isScreenReaderOptimized(), - }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor))); + }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, _parentEditor))); this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); - this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor))); + this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, _parentEditor))); - this._elements.chatMessageContent.tabIndex = 0; - this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.followUps.tabIndex = 0; this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.statusLabel.tabIndex = 0; - const markdownMessageToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.messageActions, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, workbenchToolbarOptions); - this._store.add(markdownMessageToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - this._store.add(markdownMessageToolbar); this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); } })); @@ -475,7 +481,6 @@ export class InlineChatWidget { dispose(): void { this._store.dispose(); this._ctxInputEmpty.reset(); - this._ctxMessageCropState.reset(); } get domNode(): HTMLElement { @@ -488,6 +493,9 @@ export class InlineChatWidget { if (this._accessibleViewer.value) { this._accessibleViewer.value.width = _dim.width - 12; } + this._chatMessageContents.style.width = `${_dim.width - 10}px`; + this._chatMessageContents.style.maxHeight = `270px`; + const widgetToolbarWidth = getTotalWidth(this._elements.widgetToolbar); const editorToolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; const innerEditorWidth = _dim.width - editorToolbarWidth - widgetToolbarWidth; @@ -510,12 +518,6 @@ export class InlineChatWidget { this._elements.previewCreate.style.height = `${previewCreateDim.height}px`; } - - const lineHeight = this.parentEditor.getOption(EditorOption.lineHeight); - const editorHeight = this.parentEditor.getLayoutInfo().height; - const editorHeightInLines = Math.floor(editorHeight / lineHeight); - this._elements.root.style.setProperty('--vscode-inline-chat-cropped', String(Math.floor(editorHeightInLines / 5))); - this._elements.root.style.setProperty('--vscode-inline-chat-expanded', String(Math.floor(editorHeightInLines / 3))); this._onDidChangeLayout.fire(); } } finally { @@ -524,16 +526,17 @@ export class InlineChatWidget { } getHeight(): number { - const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status); const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */; + const progressHeight = getTotalHeight(this._elements.progress); const detectedIntentHeight = getTotalHeight(this._elements.detectedIntent); + const chatResponseHeight = getTotalHeight(this._chatMessageContents) + 16 /*padding*/; const followUpsHeight = getTotalHeight(this._elements.followUps); - const chatResponseHeight = getTotalHeight(this._elements.chatMessage); const previewDiffHeight = this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel() ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight())) : 0; const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle); const previewCreateHeight = this._previewCreateEditor.hasValue && this._previewCreateEditor.value.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight())) : 0; const accessibleViewHeight = this._accessibleViewer.value?.height ?? 0; - return base + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + 18 /* padding */ + 8 /*shadow*/; + const statusHeight = getTotalHeight(this._elements.status); + return progressHeight + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + statusHeight + 18 /* padding */ + 8 /*shadow*/; } updateProgress(show: boolean) { @@ -589,14 +592,6 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - get expansionState(): ExpansionState { - return this._expansionState; - } - - set preferredExpansionState(expansionState: ExpansionState | undefined) { - this._preferredExpansionState = expansionState; - } - get responseContent(): string | undefined { return this._chatMessage?.value; } @@ -604,42 +599,31 @@ export class InlineChatWidget { updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender; updateChatMessage(message: IInlineChatMessage | undefined): void; updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean): IInlineChatMessageAppender | undefined { - let expansionState: ExpansionState; + this._chatMessageDisposables.clear(); this._chatMessage = message ? new MarkdownString(message.message.value) : undefined; const hasMessage = message?.message.value; this._elements.chatMessage.classList.toggle('hidden', !hasMessage); - reset(this._elements.chatMessageContent); + reset(this._chatMessageContents); let resultingAppender: IInlineChatMessageAppender | undefined; - if (!hasMessage) { - this._ctxMessageCropState.reset(); - expansionState = ExpansionState.NOT_CROPPED; - } else { + if (hasMessage) { const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService)); const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } }; const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, this._codeBlockModelCollection, undefined)); - renderer.layout(this._elements.chatMessageContent.clientWidth - 4); // 2 for the padding used for the tab index border + renderer.layout(this._chatMessageContents.clientWidth - 4); // 2 for the padding used for the tab index border this._chatMessageDisposables.add(this._onDidChangeLayout.event(() => { - renderer.layout(this._elements.chatMessageContent.clientWidth - 4); + renderer.layout(this._chatMessageContents.clientWidth - 4); + this._chatMessageScrollable.scanDomNode(); })); - const template = renderer.renderTemplate(this._elements.chatMessageContent); + const template = renderer.renderTemplate(this._chatMessageContents); this._chatMessageDisposables.add(template.elementDisposables); this._chatMessageDisposables.add(template.templateDisposables); renderer.renderChatTreeItem(viewModel, 0, template); this._chatMessageDisposables.add(renderer.onDidChangeItemHeight(() => this._onDidChangeHeight.fire())); - if (this._preferredExpansionState) { - expansionState = this._preferredExpansionState; - this._preferredExpansionState = undefined; - } else { - this._updateLineClamp(ExpansionState.CROPPED); - expansionState = template.value.scrollHeight > template.value.clientHeight ? ExpansionState.CROPPED : ExpansionState.NOT_CROPPED; - } - this._ctxMessageCropState.set(expansionState); - this._updateLineClamp(expansionState); resultingAppender = isIncomplete ? { cancel: () => responseModel.cancel(), complete: () => responseModel.complete(), @@ -649,7 +633,6 @@ export class InlineChatWidget { } } : undefined; } - this._expansionState = expansionState; this._onDidChangeHeight.fire(); return resultingAppender; } @@ -667,21 +650,6 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - updateChatMessageExpansionState(expansionState: ExpansionState) { - this._ctxMessageCropState.set(expansionState); - const heightBefore = this._elements.chatMessageContent.scrollHeight; - this._updateLineClamp(expansionState); - const heightAfter = this._elements.chatMessageContent.scrollHeight; - if (heightBefore === heightAfter) { - this._ctxMessageCropState.set(ExpansionState.NOT_CROPPED); - } - this._onDidChangeHeight.fire(); - } - - private _updateLineClamp(expansionState: ExpansionState) { - this._elements.chatMessageContent.setAttribute('state', expansionState); - } - updateSlashCommandUsed(command: string): void { const details = this._slashCommandDetails.find(candidate => candidate.command === command); if (!details) { @@ -948,7 +916,7 @@ export class InlineChatWidget { this._elements.accessibleViewer, session, hunkData, - new AccessibleHunk(this.parentEditor, session, hunkData) + new AccessibleHunk(this._parentEditor, session, hunkData) ); this._onDidChangeHeight.fire(); @@ -956,160 +924,6 @@ export class InlineChatWidget { } } -export class InlineChatZoneWidget extends ZoneWidget { - - readonly widget: InlineChatWidget; - - private readonly _ctxVisible: IContextKey; - private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; - private _dimension?: Dimension; - private _indentationWidth: number | undefined; - - constructor( - editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); - - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); - - this._disposables.add(toDisposable(() => { - this._ctxVisible.reset(); - this._ctxCursorPosition.reset(); - })); - - this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, { - menuId: MENU_INLINE_CHAT_INPUT, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, - statusMenuId: MENU_INLINE_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK - }); - this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); - this._disposables.add(this.widget); - this.create(); - - - this._disposables.add(addDisposableListener(this.domNode, 'click', e => { - if (!this.widget.hasFocus()) { - this.widget.focus(); - } - }, true)); - - // todo@jrieken listen ONLY when showing - const updateCursorIsAboveContextKey = () => { - if (!this.position || !this.editor.hasModel()) { - this._ctxCursorPosition.reset(); - } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('above'); - } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('below'); - } else { - this._ctxCursorPosition.reset(); - } - }; - this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); - this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); - updateCursorIsAboveContextKey(); - } - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this.widget.domNode); - } - - - protected override _doLayout(heightInPixel: number): void { - - const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; - const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth)); - this._dimension = new Dimension(width, heightInPixel); - this.widget.domNode.style.width = `${width}px`; - this.widget.layout(this._dimension); - } - - private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { - const info = this.editor.getLayoutInfo(); - return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); - } - - private _computeHeightInLines(): number { - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - return this.widget.getHeight() / lineHeight; - } - - protected override _relayout() { - if (this._dimension) { - this._doLayout(this._dimension.height); - } - super._relayout(this._computeHeightInLines()); - } - - override show(position: Position): void { - super.show(position, this._computeHeightInLines()); - this.widget.focus(); - this._ctxVisible.set(true); - } - - protected override _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimap.minimapWidth; - } - - updateBackgroundColor(newPosition: Position, wholeRange: IRange) { - assertType(this.container); - const widgetLineNumber = newPosition.lineNumber; - this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); - } - - private _calculateIndentationWidth(position: Position): number { - const viewModel = this.editor._getViewModel(); - if (!viewModel) { - return 0; - } - const visibleRange = viewModel.getCompletelyVisibleViewRange(); - const startLineVisibleRange = visibleRange.startLineNumber; - const positionLine = position.lineNumber; - let indentationLineNumber: number | undefined; - let indentationLevel: number | undefined; - for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) { - const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); - if (currentIndentationLevel !== 0) { - indentationLineNumber = lineNumber; - indentationLevel = currentIndentationLevel; - break; - } - } - return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); - } - - setContainerMargins(): void { - assertType(this.container); - - const info = this.editor.getLayoutInfo(); - const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; - this.container.style.marginLeft = `${marginWithoutIndentation}px`; - } - - setWidgetMargins(position: Position): void { - const indentationWidth = this._calculateIndentationWidth(position); - if (this._indentationWidth === indentationWidth) { - return; - } - this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; - this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; - this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; - } - - override hide(): void { - this.container!.classList.remove('inside-selection'); - this._ctxVisible.reset(); - this._ctxCursorPosition.reset(); - this.widget.reset(); - super.hide(); - aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); - } -} - class HunkAccessibleDiffViewer extends AccessibleDiffViewer { readonly height: number; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts new file mode 100644 index 00000000000..f9853b40261 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Dimension, addDisposableListener } from 'vs/base/browser/dom'; +import * as aria from 'vs/base/browser/ui/aria/aria'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { assertType } from 'vs/base/common/types'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange } from 'vs/editor/common/core/range'; +import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; +import { localize } from 'vs/nls'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { InlineChatWidget } from './inlineChatWidget'; + + +export class InlineChatZoneWidget extends ZoneWidget { + + readonly widget: InlineChatWidget; + + private readonly _ctxVisible: IContextKey; + private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; + private _dimension?: Dimension; + private _indentationWidth: number | undefined; + + constructor( + editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); + + this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); + + this._disposables.add(toDisposable(() => { + this._ctxVisible.reset(); + this._ctxCursorPosition.reset(); + })); + + this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, { + telemetrySource: 'interactiveEditorWidget-toolbar', + inputMenuId: MENU_INLINE_CHAT_INPUT, + widgetMenuId: MENU_INLINE_CHAT_WIDGET, + feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK, + statusMenuId: { + menu: MENU_INLINE_CHAT_WIDGET_STATUS, + options: { + buttonConfigProvider: action => { + if (action.id === ACTION_REGENERATE_RESPONSE) { + return { showIcon: true, showLabel: false, isSecondary: true }; + } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { + return { isSecondary: false }; + } else { + return { isSecondary: true }; + } + } + } + } + }); + this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); + this._disposables.add(this.widget); + this.create(); + + + this._disposables.add(addDisposableListener(this.domNode, 'click', e => { + if (!this.widget.hasFocus()) { + this.widget.focus(); + } + }, true)); + + // todo@jrieken listen ONLY when showing + const updateCursorIsAboveContextKey = () => { + if (!this.position || !this.editor.hasModel()) { + this._ctxCursorPosition.reset(); + } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { + this._ctxCursorPosition.set('above'); + } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { + this._ctxCursorPosition.set('below'); + } else { + this._ctxCursorPosition.reset(); + } + }; + this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); + this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); + updateCursorIsAboveContextKey(); + } + + protected override _fillContainer(container: HTMLElement): void { + container.appendChild(this.widget.domNode); + } + + + protected override _doLayout(heightInPixel: number): void { + + const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; + const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth)); + this._dimension = new Dimension(width, heightInPixel); + this.widget.domNode.style.width = `${width}px`; + this.widget.layout(this._dimension); + } + + private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { + const info = this.editor.getLayoutInfo(); + return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); + } + + private _computeHeightInLines(): number { + const lineHeight = this.editor.getOption(EditorOption.lineHeight); + return this.widget.getHeight() / lineHeight; + } + + protected override _relayout() { + if (this._dimension) { + this._doLayout(this._dimension.height); + } + super._relayout(this._computeHeightInLines()); + } + + override show(position: Position): void { + super.show(position, this._computeHeightInLines()); + this.widget.focus(); + this._ctxVisible.set(true); + } + + protected override _getWidth(info: EditorLayoutInfo): number { + return info.width - info.minimap.minimapWidth; + } + + updateBackgroundColor(newPosition: Position, wholeRange: IRange) { + assertType(this.container); + const widgetLineNumber = newPosition.lineNumber; + this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); + } + + private _calculateIndentationWidth(position: Position): number { + const viewModel = this.editor._getViewModel(); + if (!viewModel) { + return 0; + } + const visibleRange = viewModel.getCompletelyVisibleViewRange(); + const startLineVisibleRange = visibleRange.startLineNumber; + const positionLine = position.lineNumber; + let indentationLineNumber: number | undefined; + let indentationLevel: number | undefined; + for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) { + const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); + if (currentIndentationLevel !== 0) { + indentationLineNumber = lineNumber; + indentationLevel = currentIndentationLevel; + break; + } + } + return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); + } + + setContainerMargins(): void { + assertType(this.container); + + const info = this.editor.getLayoutInfo(); + const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; + this.container.style.marginLeft = `${marginWithoutIndentation}px`; + } + + setWidgetMargins(position: Position): void { + const indentationWidth = this._calculateIndentationWidth(position); + if (this._indentationWidth === indentationWidth) { + return; + } + this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; + this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; + this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; + } + + override hide(): void { + this.container!.classList.remove('inside-selection'); + this._ctxVisible.reset(); + this._ctxCursorPosition.reset(); + this.widget.reset(); + super.hide(); + aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 10e6097adea..d44c8e11d1e 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -156,7 +156,6 @@ export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('in export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); -export const CTX_INLINE_CHAT_MESSAGE_CROP_STATE = new RawContextKey<'cropped' | 'not_cropped' | 'expanded'>('inlineChatMarkdownMessageCropState', 'not_cropped', localize('inlineChatMarkdownMessageCropState', "Whether the interactive editor message is cropped, not cropped or expanded")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); export const CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('inlineChatHasActiveRequest', false, localize('inlineChatHasActiveRequest', "Whether interactive editor has an active request")); export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); @@ -181,7 +180,6 @@ export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; export const MENU_INLINE_CHAT_INPUT = MenuId.for('inlineChatInput'); export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget'); -export const MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE = MenuId.for('inlineChatWidget.markdownMessage'); export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); export const MENU_INLINE_CHAT_WIDGET_FEEDBACK = MenuId.for('inlineChatWidget.feedback'); export const MENU_INLINE_CHAT_WIDGET_DISCARD = MenuId.for('inlineChatWidget.undo'); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index d4b59189404..d97a8f09aa7 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -292,7 +292,8 @@ export class NotebookChatController extends Disposable implements INotebookEdito InlineChatWidget, fakeParentEditor, { - menuId: MENU_CELL_CHAT_INPUT, + telemetrySource: 'notebook-generate-cell', + inputMenuId: MENU_CELL_CHAT_INPUT, widgetMenuId: MENU_CELL_CHAT_WIDGET, statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK @@ -895,4 +896,3 @@ export class EditStrategy { registerNotebookContribution(NotebookChatController.id, NotebookChatController); -