mirror of
https://github.com/voideditor/void
synced 2026-05-23 01:18:25 +00:00
ctrlK now switches tabs + UI + remove async
This commit is contained in:
parent
954e611fda
commit
d3b61867f0
5 changed files with 338 additions and 76 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<IConsistentEditorItemService>('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<string, Set<string>> = {};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string | undefined> = {}
|
||||
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<InputBox> = 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]
|
||||
|
||||
|
|
|
|||
|
|
@ -334,7 +334,8 @@ export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: {
|
|||
<${preTag}>
|
||||
/* Original Selection:
|
||||
${selection}*/
|
||||
/* Instructions: ${userMessage}*/
|
||||
/* Instructions:
|
||||
${userMessage}*/
|
||||
${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* right (button) */}
|
||||
<div className='flex flex-row items-end'>
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
{/* bottom row */}
|
||||
<div
|
||||
className='flex flex-row justify-between items-end gap-1'
|
||||
>
|
||||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue