From 64e43fa53e8d2bb2dea0cb5f59ad3b715275be4e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 14 Jan 2025 19:10:57 -0800 Subject: [PATCH] fix ctrlK state --- .../void/browser/inlineDiffsService.ts | 34 +++++++++++++++++-- .../contrib/void/browser/quickEditActions.ts | 7 +--- .../src/quick-edit-tsx/QuickEditChat.tsx | 26 ++++++++------ .../void/browser/react/src/util/helpers.tsx | 19 +++++++++++ .../void/browser/react/src/util/services.tsx | 29 ++++++++++++++-- 5 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 7b3063a6..ba5ff935 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -38,6 +38,7 @@ import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; // import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; // import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; // import { localize2 } from '../../../../nls.js'; @@ -146,6 +147,7 @@ type DiffZone = { line?: undefined; }; editorId?: undefined; + linkedStreamingDiffZone?: undefined; } & CommonZoneProps @@ -160,6 +162,7 @@ const diffAreaSnapshotKeys = [ 'startLine', 'endLine', 'editorId', + ] as const satisfies (keyof DiffArea)[] type DiffAreaSnapshot = Pick @@ -172,6 +175,7 @@ type HistorySnapshot = { } +export type StreamState = Set // set of streaming diffareaids, only applies to diffzones export interface IInlineDiffsService { readonly _serviceBrand: undefined; @@ -179,6 +183,9 @@ export interface IInlineDiffsService { interruptStreaming(diffareaid: number): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; + + onDidChangeStreamState: Event + streamingDiffZonesState: StreamState // testDiffs(): void; } @@ -195,6 +202,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { diffOfId: Record = {}; // redundant with diffArea._diffs + + // streaming state for React, reflection of the streaming value in the diffarea, only applies to diffZone + readonly streamingDiffZonesState: StreamState = new Set() + private readonly _onDidChangeStreamState = new Emitter(); + readonly onDidChangeStreamState: Event = this._onDidChangeStreamState.event; + + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @ICodeEditorService private readonly _editorService: ICodeEditorService, @@ -366,6 +380,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { mountCtrlK(domNode, accessor, { diffareaid: ctrlKZone.diffareaid, + initStreamingDiffZoneId: ctrlKZone._linkedStreamingDiffZone, textAreaRef: (r) => { textAreaRef.current = r @@ -1045,7 +1060,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // check if there's overlap with any other ctrlKZone and if so, focus it const overlappingCtrlKZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'CtrlKZone' }) if (overlappingCtrlKZone) { - (overlappingCtrlKZone as CtrlKZone)._mountInfo?.textAreaRef.current?.focus() + editor.revealLine(overlappingCtrlKZone.startLine) // important + setTimeout(() => (overlappingCtrlKZone as CtrlKZone)._mountInfo?.textAreaRef.current?.focus(), 100) return } @@ -1053,6 +1069,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { if (overlappingDiffZone) return + editor.revealLine(startLine) + editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 }) + const { onFinishEdit } = this._addToHistory(uri) const adding: Omit = { @@ -1184,10 +1203,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) + this.streamingDiffZonesState.add(diffZone.diffareaid) + this._onDidChangeStreamState.fire(diffZone.diffareaid) + if (featureName === 'Ctrl+K') { const { diffareaid } = opts const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone.type !== 'CtrlKZone') return + ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid } @@ -1220,9 +1243,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const onDone = (hadError: boolean) => { diffZone._streamState = { isStreaming: false, } + this.streamingDiffZonesState.delete(diffZone.diffareaid) + this._onDidChangeStreamState.fire(diffZone.diffareaid) + if (featureName === 'Ctrl+K') { const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone - ctrlKZone._linkedStreamingDiffZone = null // gets deleted next so not really needed... + + ctrlKZone._linkedStreamingDiffZone = null this._deleteCtrlKZone(ctrlKZone) } this._refreshStylesAndDiffsInURI(uri) @@ -1300,7 +1327,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { this._llmMessageService.abort(streamRequestId) diffZone._streamState = { isStreaming: false, } - + this.streamingDiffZonesState.delete(diffZone.diffareaid) + this._onDidChangeStreamState.fire(diffZone.diffareaid) } _undoHistory(uri: URI) { diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index 8818191d..125e983c 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -16,6 +16,7 @@ import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js'; export type QuickEditPropsType = { diffareaid: number, + initStreamingDiffZoneId: number | null, textAreaRef: (ref: HTMLTextAreaElement | null) => void; onChangeHeight: (height: number) => void; onChangeText: (text: string) => void; @@ -60,12 +61,6 @@ registerAction2(class extends Action2 { const { startLineNumber: startLine, endLineNumber: endLine } = selection - - // deselect - clear selection - editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 }) - - editor.revealLine(startLine) // important - const inlineDiffsService = accessor.get(IInlineDiffsService) inlineDiffsService.addCtrlKZone({ startLine, endLine, editor }) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index b037c71b..cb8d3e1c 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -4,15 +4,17 @@ *--------------------------------------------------------------------------------------*/ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; -import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } from '../util/services.js'; +import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor, useIsStreaming } from '../util/services.js'; import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js'; import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; +import { useRefState } from '../util/helpers.js'; export const QuickEditChat = ({ diffareaid, + initStreamingDiffZoneId, onChangeHeight, onChangeText: onChangeText_, textAreaRef: textAreaRef_, @@ -43,32 +45,34 @@ export const QuickEditChat = ({ const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions const isDisabled = instructionsAreEmpty - const currentlyStreamingIdRef = useRef(undefined) - const [isStreaming, setIsStreaming] = useState(false) + const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) + + const isStreaming = useIsStreaming({ diffareaid: currStreamingDiffZoneRef.current }) const onSubmit = useCallback((e: FormEvent) => { if (isDisabled) return - if (currentlyStreamingIdRef.current !== undefined) return + if (currStreamingDiffZoneRef.current !== null) return textAreaFnsRef.current?.disable() const instructions = textAreaRef.current?.value ?? '' - currentlyStreamingIdRef.current = inlineDiffsService.startApplying({ + const id = inlineDiffsService.startApplying({ featureName: 'Ctrl+K', diffareaid: diffareaid, userMessage: instructions, }) - setIsStreaming(true) - }, [isDisabled, inlineDiffsService, diffareaid]) + setCurrentlyStreamingDiffZone(id ?? null) + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid]) const onInterrupt = useCallback(() => { - if (currentlyStreamingIdRef.current !== undefined) - inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current) + if (currStreamingDiffZoneRef.current === null) return + inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current) + setCurrentlyStreamingDiffZone(null) textAreaFnsRef.current?.enable() - setIsStreaming(false) - }, [inlineDiffsService]) + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService]) const onX = useCallback(() => { + onInterrupt() inlineDiffsService.removeCtrlKZone({ diffareaid }) }, [inlineDiffsService, diffareaid]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx new file mode 100644 index 00000000..32ab1902 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx @@ -0,0 +1,19 @@ +import { useCallback, useRef, useState } from 'react' + + + +type ReturnType = [ + { readonly current: T }, + (t: T) => void +] + +// use this if state might be too slow to catch +export const useRefState = (initVal: T): ReturnType => { + const [_, _setState] = useState(false) + const ref = useRef(initVal) + const setState = useCallback((newVal: T) => { + _setState(n => !n) // call rerender + ref.current = newVal + }, []) + return [ref, setState] +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index beec21dd..baf895b7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -27,7 +27,7 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js'; import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js'; -import { IInlineDiffsService } from '../../../inlineDiffsService.js'; +import { IInlineDiffsService, StreamState } from '../../../inlineDiffsService.js'; import { IQuickEditStateService } from '../../../quickEditStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; import { IThreadHistoryService } from '../../../threadHistoryService.js'; @@ -70,6 +70,9 @@ const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: Refresh let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() +let streamState: StreamState +const diffareaStreamStateListeners: Set<(diffareaid: number) => void> = new Set() + // must call this before you can use any of the hooks below // this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it! let wasCalled = false @@ -93,9 +96,10 @@ export const _registerServices = (accessor: ServicesAccessor) => { settingsStateService: accessor.get(IVoidSettingsService), refreshModelService: accessor.get(IRefreshModelService), themeService: accessor.get(IThemeService), + inlineDiffsService: accessor.get(IInlineDiffsService), } - const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices + const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices quickEditState = quickEditStateService.state disposables.push( @@ -146,6 +150,13 @@ export const _registerServices = (accessor: ServicesAccessor) => { }) ) + disposables.push( + inlineDiffsService.onDidChangeStreamState((diffareaid) => { + streamState = inlineDiffsService.streamingDiffZonesState + diffareaStreamStateListeners.forEach(l => l(diffareaid)) + }) + ) + return disposables } @@ -283,3 +294,17 @@ export const useIsDark = () => { return isDark } + + +export const useIsStreaming = ({ diffareaid }: { diffareaid: number | null }) => { + console.log('difareaid', diffareaid) + const [s, ss] = useState(diffareaid === null ? false : streamState.has(diffareaid)) + useEffect(() => { ss(diffareaid === null ? false : streamState.has(diffareaid)) }, [diffareaid]) + useEffect(() => { + const listener = (diffareaid_: number) => { if (diffareaid === diffareaid_) ss(streamState.has(diffareaid)) } + diffareaStreamStateListeners.add(listener) + return () => { diffareaStreamStateListeners.delete(listener) } + }, [ss, diffareaid]) + + return s +}