Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts
5241 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable } from '../../../../base/common/lifecycle.js';6import { autorun, debouncedObservable, derived, observableSignalFromEvent, observableValue, runOnChange, waitForState } from '../../../../base/common/observable.js';7import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';8import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';9import { ScrollType } from '../../../../editor/common/editorCommon.js';10import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';11import { InlineChatConfigKeys } from '../common/inlineChat.js';12import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';13import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';14import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js';15import { InlineChatEditorAffordance } from './inlineChatEditorAffordance.js';16import { InlineChatInputWidget } from './inlineChatOverlayWidget.js';17import { InlineChatGutterAffordance } from './inlineChatGutterAffordance.js';18import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js';19import { assertType } from '../../../../base/common/types.js';20import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js';21import { IInlineChatSessionService } from './inlineChatSessionService.js';2223export class InlineChatAffordance extends Disposable {2425private _menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>(this, undefined);2627constructor(28private readonly _editor: ICodeEditor,29private readonly _inputWidget: InlineChatInputWidget,30@IInstantiationService private readonly _instantiationService: IInstantiationService,31@IConfigurationService configurationService: IConfigurationService,32@IChatEntitlementService chatEntiteldService: IChatEntitlementService,33@IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService,34) {35super();3637const editorObs = observableCodeEditor(this._editor);38const affordance = observableConfigValue<'off' | 'gutter' | 'editor'>(InlineChatConfigKeys.Affordance, 'off', configurationService);39const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500);4041const selectionData = observableValue<Selection | undefined>(this, undefined);4243let explicitSelection = false;4445this._store.add(runOnChange(editorObs.selections, (value, _prev, events) => {46explicitSelection = events.every(e => e.reason === CursorChangeReason.Explicit);47if (!value || value.length !== 1 || value[0].isEmpty() || !explicitSelection) {48selectionData.set(undefined, undefined);49}50}));5152this._store.add(autorun(r => {53const value = debouncedSelection.read(r);54if (!value || value.isEmpty() || !explicitSelection || _editor.getModel()?.getValueInRange(value).match(/^\s+$/)) {55selectionData.set(undefined, undefined);56return;57}58selectionData.set(value, undefined);59}));6061this._store.add(autorun(r => {62if (chatEntiteldService.sentimentObs.read(r).hidden) {63selectionData.set(undefined, undefined);64}65}));6667const hasSessionObs = derived(r => {68observableSignalFromEvent(this, inlineChatSessionService.onDidChangeSessions).read(r);69const model = editorObs.model.read(r);70return model ? inlineChatSessionService.getSessionByTextModel(model.uri) !== undefined : false;71});7273this._store.add(autorun(r => {74if (hasSessionObs.read(r)) {75selectionData.set(undefined, undefined);76}77}));7879this._store.add(this._instantiationService.createInstance(80InlineChatGutterAffordance,81editorObs,82derived(r => affordance.read(r) === 'gutter' ? selectionData.read(r) : undefined),83this._menuData84));8586this._store.add(this._instantiationService.createInstance(87InlineChatEditorAffordance,88this._editor,89derived(r => affordance.read(r) === 'editor' ? selectionData.read(r) : undefined)90));9192this._store.add(autorun(r => {93const data = this._menuData.read(r);94if (!data) {95return;96}9798// Reveal the line in case it's outside the viewport (e.g., when triggered from sticky scroll)99this._editor.revealLineInCenterIfOutsideViewport(data.lineNumber, ScrollType.Immediate);100101const editorDomNode = this._editor.getDomNode()!;102const editorRect = editorDomNode.getBoundingClientRect();103const left = data.rect.left - editorRect.left;104105// Show the overlay widget106this._inputWidget.show(data.lineNumber, left, data.above);107}));108109this._store.add(autorun(r => {110const pos = this._inputWidget.position.read(r);111if (pos === null) {112this._menuData.set(undefined, undefined);113}114}));115}116117async showMenuAtSelection() {118assertType(this._editor.hasModel());119120const direction = this._editor.getSelection().getDirection();121const position = this._editor.getPosition();122const editorDomNode = this._editor.getDomNode();123const scrolledPosition = this._editor.getScrolledVisiblePosition(position);124const editorRect = editorDomNode.getBoundingClientRect();125const x = editorRect.left + scrolledPosition.left;126const y = editorRect.top + scrolledPosition.top;127128this._menuData.set({129rect: new DOMRect(x, y, 0, scrolledPosition.height),130above: direction === SelectionDirection.RTL,131lineNumber: position.lineNumber132}, undefined);133134await waitForState(this._inputWidget.position, pos => pos === null);135}136}137138139