commandbar draft

This commit is contained in:
Mathew Pareles 2025-03-15 01:19:52 -07:00
parent 47e93d1cf3
commit 3f7f2fb40c
4 changed files with 244 additions and 7 deletions

View file

@ -177,7 +177,7 @@ type CtrlKZone = {
} & CommonZoneProps
type DiffZone = {
export type DiffZone = {
type: 'DiffZone',
originalCode: string;
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
@ -207,7 +207,7 @@ type TrackingZone<T> = {
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
const diffAreaSnapshotKeys = [
'type',
@ -240,16 +240,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
// URI <--> model
diffAreasOfURI: Record<string, Set<string> | undefined> = {}
diffAreasOfURI: Record<string, Set<string> | undefined> = {}; // uri -> diffareaId
diffAreaOfId: Record<string, DiffArea> = {};
diffOfId: Record<string, Diff> = {}; // redundant with diffArea._diffs
diffAreaOfId: Record<string, DiffArea> = {}; // diffareaId -> diffArea
diffOfId: Record<string, Diff> = {}; // diffid -> diff (redundant with diffArea._diffOfId)
_sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId
_sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId
// only applies to diffZones
// streamingDiffZones: Set<number> = new Set()
private readonly _onDidChangeDiffZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>();
private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>();
onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event;
private readonly _onDidFinishAddOrDeleteDiffInDiffZone = new Emitter<{ uri: URI }>();
onDidAddOrDeleteDiffInDiffZone = this._onDidFinishAddOrDeleteDiffInDiffZone.event;
private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>();
onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event
@ -344,6 +350,46 @@ class EditCodeService extends Disposable implements IEditCodeService {
for (let editor of this._codeEditorService.listCodeEditors()) { initializeEditor(editor) }
this._register(this._codeEditorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// update `_sortedUrisWithDiffs` and `_sortedDiffsOfUri` on all changes to diffzones
this._register(this._onDidFinishAddOrDeleteDiffInDiffZone.event(({ uri }) => {
// 1. Update _sortedUrisWithDiffs
const hasDiffZones = Array.from(this.diffAreasOfURI[uri.fsPath] || [])
.some(diffAreaId => this.diffAreaOfId[diffAreaId]?.type === 'DiffZone');
// Add or remove this URI from _sortedUrisWithDiffs
const currentIndex = this._sortedUrisWithDiffs.findIndex(u => u.fsPath === uri.fsPath);
if (hasDiffZones && currentIndex === -1) {
// Add URI and maintain sort
this._sortedUrisWithDiffs.push(uri);
this._sortedUrisWithDiffs.sort((a, b) => a.fsPath.localeCompare(b.fsPath));
} else if (!hasDiffZones && currentIndex !== -1) {
// Remove URI
this._sortedUrisWithDiffs.splice(currentIndex, 1);
}
// 2. Update _sortedDiffsOfUri only for this URI
const diffsInUri: Diff[] = [];
// Collect all diffs from DiffZones in this URI
for (const diffAreaId of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffAreaId];
if (diffArea?.type === 'DiffZone') {
diffsInUri.push(...Object.values(diffArea._diffOfId));
}
}
// Update or remove the entry for this URI
if (diffsInUri.length > 0) {
this._sortedDiffsOfFspath[uri.fsPath] = diffsInUri.sort((a, b) => a.startLine - b.startLine);
console.log('_sortedDiffsOfFspath', this._sortedDiffsOfFspath)
} else {
delete this._sortedDiffsOfFspath[uri.fsPath];
}
}));
}
@ -901,6 +947,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (diffArea.type !== 'DiffZone') return
delete diffArea._diffOfId[diff.diffid]
delete this.diffOfId[diff.diffid]
if (!diffArea._streamState.isStreaming) {
this._onDidFinishAddOrDeleteDiffInDiffZone.fire({ uri: diffArea._URI })
}
}
private _deleteDiffs(diffZone: DiffZone) {
@ -993,6 +1043,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
this.diffOfId[diffid] = newDiff
diffZone._diffOfId[diffid] = newDiff
if (!diffZone._streamState.isStreaming) {
this._onDidFinishAddOrDeleteDiffInDiffZone.fire({ uri })
}
return newDiff
}

View file

@ -7,6 +7,7 @@ import { Event } from '../../../../base/common/event.js';
import { URI } from '../../../../base/common/uri.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Diff, DiffArea } from './editCodeService.js';
@ -36,13 +37,24 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
export interface IEditCodeService {
readonly _serviceBrand: undefined;
// main entrypoints (initialize things for the functions below to be called):
startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null>;
_sortedUrisWithDiffs: URI[];
_sortedDiffsOfFspath: { [fsPath: string]: Diff[] | undefined };
diffAreaOfId: Record<string, DiffArea>;
diffOfId: Record<string, Diff>;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void;
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
onDidAddOrDeleteDiffInDiffZone: Event<{ uri: URI }>;
// CtrlKZone streaming state
isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean;
interruptCtrlKStreaming(opts: { diffareaid: number }): void;

View file

@ -41,7 +41,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const accessor = useAccessor()
const chatThreadService = accessor.get('IChatThreadService')
const commandSerivce = accessor.get('ICommandService')
const commandService = accessor.get('ICommandService')
const editorService = accessor.get('ICodeEditorService')
const { messageIdx, threadId } = chatMessageLocation
@ -73,7 +73,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const selection = link.selection
// open the file
commandSerivce.executeCommand('vscode.open', link.uri).then(() => {
commandService.executeCommand('vscode.open', link.uri).then(() => {
// select the text
setTimeout(() => {

View file

@ -32,6 +32,8 @@ import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '.
import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js';
import { dirname } from '../../../../../../../base/common/resources.js';
import { useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
import { DiffZone } from '../../../editCodeService.js';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
@ -1609,6 +1611,173 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
}
const VoidCommandBar = () => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const editorService = accessor.get('ICodeEditorService')
const commandService = accessor.get('ICommandService')
const [_, rerender] = useState(0)
console.log('rerender count: ', _)
// state for what the user is currently focused on (both URI and diff)
const [diffIdxOfFspath, setDiffIdxOfFspath] = useState<Record<string, number | undefined>>({})
// const [currentUriIdx, setCurrentUriIdx] = useState(-1) // we are doing O(n) search for this
const getCurrentUri = useCallback(() => {
const editor = editorService.getActiveCodeEditor()
if (!editor) return null
const uri = editor.getModel()?.uri
if (!uri) return null
return uri
}, [editorService])
const diffZones: DiffZone[] = []
// trigger rerender when diffzone is created (TODO need to also update when diff is accepted/rejected)
useEffect(() => {
const disposable = editCodeService.onDidAddOrDeleteDiffInDiffZone(() => {
rerender(c => c + 1) // rerender
})
return () => disposable.dispose()
}, [editCodeService, rerender])
const getNextDiff = useCallback(({ step }: { step: 1 | -1 }) => {
const currentUri = getCurrentUri()
if (!currentUri) {
return;
}
const sortedDiffs = editCodeService._sortedDiffsOfFspath[currentUri.fsPath]
if (!sortedDiffs || sortedDiffs.length === 0) {
return;
}
const currentDiffIdx = diffIdxOfFspath[currentUri.fsPath] || 0
const nextDiffIdx = (currentDiffIdx + step) % sortedDiffs.length
const nextDiff = sortedDiffs[nextDiffIdx]
return { nextDiff, nextDiffIdx, }
}, [getCurrentUri, editCodeService._sortedDiffsOfFspath, diffIdxOfFspath])
const getNextUri = useCallback(({ step }: { step: 1 | -1 }) => {
const sortedUris = editCodeService._sortedUrisWithDiffs
if (sortedUris.length === 0) {
return;
}
const currentUri = getCurrentUri()
const defaultUriIdx = step === 1 ? -1 : 0 // defaults: if next, currentIdx = -1; if prev, currentIdx = 0
let currentUriIdx = -1
if (currentUri) {
currentUriIdx = sortedUris.findIndex(u => u.fsPath === currentUri.fsPath)
}
if (currentUriIdx === -1) { // not found
currentUriIdx = defaultUriIdx // set to default
}
const nextUriIdx = (currentUriIdx + step) % sortedUris.length
const nextUri = sortedUris[nextUriIdx]
return { nextUri, nextUriIdx, }
}, [getCurrentUri, editCodeService._sortedUrisWithDiffs])
return <div className='bg-red-500 min-h-4 min-w-4 flex gap-4'>
<button
className='bg-[var(--vscode-button-secondaryBackground)] text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)] px-4 py-1.5 rounded text-sm font-medium'
onClick={() => {
rerender(c => c + 1)
}}
>
rerender
</button>
<button
onClick={() => {
// get the next diff
const res = getNextDiff({ step: 1 })
if (!res) return;
// scroll to the next diff
const { nextDiff, nextDiffIdx } = res;
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
const range = { startLineNumber: nextDiff.startLine, endLineNumber: nextDiff.startLine, startColumn: 1, endColumn: 1 };
editor.revealRange(range, ScrollType.Immediate)
// update state
const diffArea = editCodeService.diffAreaOfId[nextDiff.diffareaid]
setDiffIdxOfFspath(v => ({ ...v, [diffArea._URI.fsPath]: nextDiffIdx }))
}}
>
next diff:
</button>
<button
onClick={() => {
// get the next uri
const res = getNextUri({ step: 1 })
if (!res) return;
const { nextUri, nextUriIdx } = res;
// open the uri and scroll to diff
const sortedDiffs = editCodeService._sortedDiffsOfFspath[nextUri.fsPath]
if (!sortedDiffs) return;
const diffIdx = diffIdxOfFspath[nextUri.fsPath] || 0
const diff = sortedDiffs[diffIdx]
const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 };
commandService.executeCommand('vscode.open', nextUri).then(() => {
// select the text
setTimeout(() => {
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
editor.revealRange(range, ScrollType.Immediate)
}, 50)
})
}}
>
next diff area
</button>
<div>
<div className='gap-2 text-[var(--vscode-editor-foreground)] flex'>
<div>numUris: {Object.keys(editCodeService._sortedDiffsOfFspath).length}</div>
<div>numDiffs: {Object.values(editCodeService._sortedDiffsOfFspath).reduce((acc, diffs) => acc + (diffs?.length || 0), 0)}</div>
</div>
</div>
{diffZones.map((area, index) => (
<>
<div key={index} className='bg-red-500 p-2 rounded-lg m-2 text-white'>{getBasename(area?._URI?.toString())}</div>
</>
))}
</div>
}
export const SidebarChat = () => {
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
@ -1805,6 +1974,8 @@ export const SidebarChat = () => {
fnsRef={textAreaFnsRef}
multiline={true}
/>
<VoidCommandBar />
</VoidChatArea>
</div>