From d3b61867f00538f262be2ad79c6dd97ccbffaca8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 31 Dec 2024 15:41:24 -0800 Subject: [PATCH] ctrlK now switches tabs + UI + remove async --- .../inlineDiffService/inlineDiffService.ts | 5 +- .../helperServices/consistentItemService.ts | 243 +++++++++++++++++- .../void/browser/inlineDiffsService.ts | 122 ++++----- .../contrib/void/browser/prompt/prompts.ts | 3 +- .../react/src/ctrl-k-tsx/CtrlKChat.tsx | 41 ++- 5 files changed, 338 insertions(+), 76 deletions(-) diff --git a/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts b/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts index 56637a06..ece69c58 100644 --- a/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts +++ b/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts @@ -7,7 +7,10 @@ import { IRange } from '../../../common/core/range.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; -// THIS FILE IS OLD!!! +// THIS FILE IS OLD + UNUSED!!! + +// SEE inlineDiffsService.ts INSTEAD. + export interface IInlineDiffService { readonly _serviceBrand: undefined; addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void; diff --git a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts index be4e679e..e5cbc2e8 100644 --- a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts +++ b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts @@ -9,7 +9,6 @@ import { createDecorator } from '../../../../../platform/instantiation/common/in // lets you add a "consistent" item to a Model (aka URI), instead of just to a single editor - type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); } export interface IConsistentItemService { @@ -56,6 +55,7 @@ export class ConsistentItemService extends Disposable { } + // when editor switches tabs (models) const addTabSwitchListeners = (editor: ICodeEditor) => { this._register( editor.onDidChangeModel(e => { @@ -65,6 +65,7 @@ export class ConsistentItemService extends Disposable { ) } + // when editor is disposed const addDisposeListener = (editor: ICodeEditor) => { this._register(editor.onDidDispose(() => { // anything on the editor has been disposed already @@ -176,3 +177,243 @@ export class ConsistentItemService extends Disposable { registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager); + + + + + + + + + + + + + + + +// mostly generated by o1 (almost the same as above, but just for 1 editor) +export interface IConsistentEditorItemService { + readonly _serviceBrand: undefined; + addToEditor(editor: ICodeEditor, fn: () => () => void): string; + removeFromEditor(itemId: string): void; +} +export const IConsistentEditorItemService = createDecorator('ConsistentEditorItemService'); + + +export class ConsistentEditorItemService extends Disposable { + readonly _serviceBrand: undefined; + + /** + * For each editorId, we track the set of itemIds that have been "added" to that editor. + * This does *not* necessarily mean they're currently mounted (the user may have switched models). + */ + private readonly itemIdsByEditorId: Record> = {}; + + /** + * For each itemId, we store relevant info (the fn to call on the editor, the editorId, the uri, and the current dispose function). + */ + private readonly itemInfoById: Record< + string, + { + editorId: string; + uriFsPath: string; + fn: (editor: ICodeEditor) => () => void; + disposeFn?: () => void; + } + > = {}; + + constructor( + @ICodeEditorService private readonly _editorService: ICodeEditorService, + ) { + super(); + + // + // Wire up listeners to watch for new editors, removed editors, etc. + // + + // Initialize any already-existing editors + for (const editor of this._editorService.listCodeEditors()) { + this._initializeEditor(editor); + } + + // When an editor is added, track it + this._register( + this._editorService.onCodeEditorAdd((editor) => { + this._initializeEditor(editor); + }) + ); + + // When an editor is removed, remove all items associated with that editor + this._register( + this._editorService.onCodeEditorRemove((editor) => { + this._removeAllItemsFromEditor(editor); + }) + ); + } + + /** + * Sets up listeners on the provided editor so that: + * - If the editor changes models, we remove items and re-mount only if the new model matches. + * - If the editor is disposed, we do the needed cleanup. + */ + private _initializeEditor(editor: ICodeEditor) { + const editorId = editor.getId(); + + // + // Listen for model changes + // + this._register( + editor.onDidChangeModel((e) => { + this._removeAllItemsFromEditor(editor); + if (!e.newModelUrl) { + return; + } + // Re-mount any items that belong to this editor and match the new URI + const itemsForEditor = this.itemIdsByEditorId[editorId]; + if (itemsForEditor) { + for (const itemId of itemsForEditor) { + const itemInfo = this.itemInfoById[itemId]; + if (itemInfo && itemInfo.uriFsPath === e.newModelUrl.fsPath) { + this._mountItemOnEditor(editor, itemId); + } + } + } + }) + ); + + // + // When the editor is disposed, remove all items from it + // + this._register( + editor.onDidDispose(() => { + this._removeAllItemsFromEditor(editor); + }) + ); + + // + // If the editor already has a model (e.g. on initial load), try mounting items + // + const uri = editor.getModel()?.uri; + if (!uri) { + return; + } + + const itemsForEditor = this.itemIdsByEditorId[editorId]; + if (itemsForEditor) { + for (const itemId of itemsForEditor) { + const itemInfo = this.itemInfoById[itemId]; + if (itemInfo && itemInfo.uriFsPath === uri.fsPath) { + this._mountItemOnEditor(editor, itemId); + } + } + } + } + + /** + * Actually calls the item-creation function `fn(editor)` and saves the resulting disposeFn + * so we can later clean it up. + */ + private _mountItemOnEditor(editor: ICodeEditor, itemId: string) { + const info = this.itemInfoById[itemId]; + if (!info) { + return; + } + const { fn } = info; + const disposeFn = fn(editor); + info.disposeFn = disposeFn; + } + + /** + * Removes a single item from an editor (calling its `disposeFn` if present). + */ + private _removeItemFromEditor(editor: ICodeEditor, itemId: string) { + const info = this.itemInfoById[itemId]; + if (info?.disposeFn) { + info.disposeFn(); + info.disposeFn = undefined; + } + } + + /** + * Removes *all* items from the given editor. Typically called when the editor changes model or is disposed. + */ + private _removeAllItemsFromEditor(editor: ICodeEditor) { + const editorId = editor.getId(); + const itemsForEditor = this.itemIdsByEditorId[editorId]; + if (!itemsForEditor) { + return; + } + + for (const itemId of itemsForEditor) { + this._removeItemFromEditor(editor, itemId); + } + } + + /** + * Public API: Adds an item to an *individual* editor (determined by editor ID), + * but only when that editor is showing the same model (uri.fsPath). + */ + addToEditor(editor: ICodeEditor, fn: () => () => void): string { + const uri = editor.getModel()?.uri + if (!uri) { + throw new Error('No URI on the provided editor or in AddItemInputs.'); + } + + const editorId = editor.getId(); + + // Create an ID for this item + const itemId = generateUuid(); + + // Record the info + this.itemInfoById[itemId] = { + editorId, + uriFsPath: uri.fsPath, + fn, + }; + + // Add to the editor's known items + if (!this.itemIdsByEditorId[editorId]) { + this.itemIdsByEditorId[editorId] = new Set(); + } + this.itemIdsByEditorId[editorId].add(itemId); + + // If the editor's current URI matches, mount it now + if (editor.getModel()?.uri.fsPath === uri.fsPath) { + this._mountItemOnEditor(editor, itemId); + } + + return itemId; + } + + /** + * Public API: Removes an item from the *specific* editor. We look up which editor + * had this item and remove it from that editor. + */ + removeFromEditor(itemId: string): void { + const info = this.itemInfoById[itemId]; + if (!info) { + // Nothing to remove + return; + } + + const { editorId } = info; + + // Find the editor in question + const editor = this._editorService.listCodeEditors().find( + (ed) => ed.getId() === editorId + ); + if (editor) { + // Dispose on that editor + this._removeItemFromEditor(editor, itemId); + } + + // Clean up references + this.itemIdsByEditorId[editorId]?.delete(itemId); + delete this.itemInfoById[itemId]; + } +} + +registerSingleton(IConsistentEditorItemService, ConsistentEditorItemService, InstantiationType.Eager); + + diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index fca8f316..941bfc82 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -26,7 +26,7 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; -import { IConsistentItemService } from './helperServices/consistentItemService.js'; +import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; import { ctrlKStream_prefixAndSuffix, ctrlKStream_prompt, ctrlKStream_systemMessage, ctrlLStream_prompt, ctrlLStream_systemMessage } from './prompt/prompts.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; import { IPosition } from '../../../../editor/common/core/position.js'; @@ -117,7 +117,7 @@ type CtrlKZone = { editorId: string; // the editor the input lives on _mountInfo: null | { - inputBox: InputBox; // the input box that lives in the zone + inputBox: InputBox | null; // the input box that lives in the zone dispose: () => void; refresh: () => void; } @@ -195,6 +195,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IConsistentItemService private readonly _consistentItemService: IConsistentItemService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService, ) { super(); @@ -319,74 +320,78 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { mostRecentTextOfCtrlKZoneId: Record = {} - private _addCtrlKZoneInput = async (ctrlKZone: CtrlKZone) => { + private _addCtrlKZoneInput = (ctrlKZone: CtrlKZone) => { + const { editorId } = ctrlKZone const editor = this._editorService.listCodeEditors().find(e => e.getId() === editorId) - if (!editor) { - console.error('editor not found') - return null - } + if (!editor) { return null } - const domNode = document.createElement('div'); - domNode.style.zIndex = '1' - const viewZone: IViewZone = { - afterLineNumber: ctrlKZone.startLine - 1, - domNode: domNode, - heightInPx: 0, - suppressMouseDown: false, - }; - - - // mount zone let zoneId: string | null = null - editor.changeViewZones(accessor => { - zoneId = accessor.addZone(viewZone) - }) + let viewZone_: IViewZone | null = null + let inputBox_: InputBox | null = null + const itemId = this._consistentEditorItemService.addToEditor(editor, () => { + const domNode = document.createElement('div'); + domNode.style.zIndex = '1' + const viewZone: IViewZone = { + afterLineNumber: ctrlKZone.startLine - 1, + domNode: domNode, + heightInPx: 52, + suppressMouseDown: false, + }; + viewZone_ = viewZone - let res_: (inputBox: InputBox) => void - const inputBoxPromise: Promise = new Promise((res, rej) => { res_ = res }) + // mount zone + editor.changeViewZones(accessor => { + zoneId = accessor.addZone(viewZone) + }) - // mount react - this._instantiationService.invokeFunction(accessor => { - const props: QuickEditPropsType = { - diffareaid: ctrlKZone.diffareaid, - onGetInputBox(inputBox) { - res_(inputBox) - // not sure why this requries a timeout - setTimeout(() => inputBox.focus(), 0) - }, - onChangeHeight(height) { - if (height === undefined) return - viewZone.heightInPx = height - // re-render with this new height - editor.changeViewZones(accessor => { - if (zoneId) { - accessor.layoutZone(zoneId) + // mount react + this._instantiationService.invokeFunction(accessor => { + mountCtrlK(domNode, accessor, { + diffareaid: ctrlKZone.diffareaid, + onGetInputBox: (inputBox) => { + inputBox_ = inputBox + // if it's mounting for the first time, focus it + if (!(ctrlKZone.diffareaid in this.mostRecentTextOfCtrlKZoneId)) { // detect first mount this way (a hack) + this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = undefined + setTimeout(() => inputBox.focus(), 0) } - }) - }, - onUserUpdateText: (text) => { this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text; }, - initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null, - } - mountCtrlK(domNode, accessor, props) + }, + onChangeHeight(height) { + if (height === undefined) return + viewZone.heightInPx = height + // re-render with this new height + editor.changeViewZones(accessor => { + if (zoneId) { + accessor.layoutZone(zoneId) + } + }) + }, + onUserUpdateText: (text) => { this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text; }, + initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null, + } satisfies QuickEditPropsType) + + }) + + return () => editor.changeViewZones(accessor => { + if (zoneId) + accessor.removeZone(zoneId) + }) }) - const inputBox = await inputBoxPromise + return { - inputBox, + inputBox: inputBox_, refresh: () => editor.changeViewZones(accessor => { - if (zoneId && viewZone) { - viewZone.afterLineNumber = ctrlKZone.startLine - 1 + if (zoneId && viewZone_) { + viewZone_.afterLineNumber = ctrlKZone.startLine - 1 accessor.layoutZone(zoneId) } }), dispose: () => { - editor.changeViewZones(accessor => { - if (zoneId) - accessor.removeZone(zoneId) - }) + this._consistentEditorItemService.removeFromEditor(itemId) }, } } @@ -398,12 +403,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type !== 'CtrlKZone') continue if (!diffArea._mountInfo) { - diffArea._mountInfo = await this._addCtrlKZoneInput(diffArea) + diffArea._mountInfo = this._addCtrlKZoneInput(diffArea) } else { diffArea._mountInfo.refresh() } - } } @@ -882,7 +886,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { if (diffArea.type !== 'CtrlKZone') continue const noOverlap = diffArea.startLine > endLine || diffArea.endLine < startLine if (!noOverlap) { - diffArea._mountInfo?.inputBox.focus() + setTimeout(() => diffArea._mountInfo?.inputBox?.focus(), 0) return } } @@ -967,8 +971,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { startLine = startLine_ endLine = endLine_ - if (!_mountInfo) return - userMessage = _mountInfo.inputBox.value + if (!_mountInfo?.inputBox) return + userMessage = _mountInfo.inputBox?.value } else { throw new Error(`Void: diff.type not recognized on: ${featureName}`) @@ -1101,7 +1105,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - // call this outside undo/redo (it calls undo) + // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream interruptStreaming(diffareaid: number) { const diffArea = this.diffAreaOfId[diffareaid] diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index f95c0165..0190619f 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -334,7 +334,8 @@ export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: { <${preTag}> /* Original Selection: ${selection}*/ -/* Instructions: ${userMessage}*/ +/* Instructions: +${userMessage}*/ ${prefix} <${sufTag}>${suffix} <${midTag}>` diff --git a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx index 2cb76f82..ffffd374 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx @@ -11,6 +11,7 @@ import { getCmdKey } from '../../../helpers/getCmdKey.js'; import { VoidInputBox } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js'; +import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => { @@ -60,6 +61,7 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang const onInterrupt = useCallback(() => { if (currentlyStreamingIdRef.current !== undefined) inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current) + inputBoxRef.current?.enable() setIsStreaming(false) }, [inlineDiffsService]) @@ -126,22 +128,33 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang /> - {/* right (button) */} -
- {/* submit / stop button */} - {isStreaming ? - // stop button - - : - // submit button (up arrow) - - } +
+ + + {/* bottom row */} +
+ {/* submit options */} +
+
+ {/* submit / stop button */} + {isStreaming ? + // stop button + + : + // submit button (up arrow) + + }