editCodeService and voidCommandBar service!!! + VoidCommandBar.tsx

This commit is contained in:
Andrew Pareles 2025-03-19 16:14:59 -07:00
parent ec505b653b
commit 744d387fe1
20 changed files with 864 additions and 674 deletions

View file

@ -3,10 +3,10 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/browser/editorBrowser.js';
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
@ -40,12 +40,13 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
import { IMetricsService } from '../common/metricsService.js';
import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@ -236,32 +237,29 @@ type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean
class EditCodeService extends Disposable implements IEditCodeService {
_serviceBrand: undefined;
// URI <--> model
diffAreasOfURI: Record<string, Set<string> | undefined> = {}; // uri -> diffareaId
diffAreaOfId: Record<string, DiffArea> = {}; // diffareaId -> diffArea
diffOfId: Record<string, Diff> = {}; // diffid -> diff (redundant with diffArea._diffOfId)
_sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId (computed from it)
_sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId (computed from it)
// only applies to diffZones
// streamingDiffZones: Set<number> = new Set()
private readonly _onDidChangeDiffZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>();
// events
// uri: diffZones // listen on change diffZones
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
private readonly _onDidChangeURIStreamState = new Emitter<{ uri: URI; state: URIStreamState }>();
onDidChangeURIStreamState = this._onDidChangeURIStreamState.event
// diffZone: [uri], diffs, isStreaming // listen on change diffs, change streaming (uri is const)
private readonly _onDidChangeDiffsInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
private readonly _onDidChangeStreamingInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
onDidChangeDiffsInDiffZone = this._onDidChangeDiffsInDiffZone.event;
onDidChangeStreamingInDiffZone = this._onDidChangeStreamingInDiffZone.event;
// ctrlKZone: [uri], isStreaming // listen on change streaming
private readonly _onDidChangeStreamingInCtrlKZone = new Emitter<{ uri: URI; diffareaid: number }>();
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event
constructor(
@ -284,14 +282,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
super();
// this function initializes data structures and listens for changes
const registeredModelListeners = new Set<string>()
const registeredModelURIs = new Set<string>()
const initializeModel = async (model: ITextModel) => {
await this._voidModelService.initializeModel(model.uri)
// do not add listeners to the same model twice - important, or will see duplicates
if (registeredModelListeners.has(model.uri.fsPath)) return
registeredModelListeners.add(model.uri.fsPath)
if (registeredModelURIs.has(model.uri.fsPath)) return
registeredModelURIs.add(model.uri.fsPath)
if (!(model.uri.fsPath in this.diffAreasOfURI)) {
this.diffAreasOfURI[model.uri.fsPath] = new Set();
@ -307,30 +305,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
})
)
// when a stream starts or ends, fire the event for onDidChangeURIStreamState
let prevStreamState = this.getURIStreamState({ uri: null })
const updateAcceptRejectAllUI = () => {
const state = this.getURIStreamState({ uri: model.uri })
let prevStateActual = prevStreamState
prevStreamState = state
if (state === prevStateActual) return
this._onDidChangeURIStreamState.fire({ uri: model.uri, state })
}
let _removeAcceptRejectAllUI: (() => void) | null = null
this._register(this._onDidChangeURIStreamState.event(({ uri, state }) => {
if (uri.fsPath !== model.uri.fsPath) return
if (state === 'acceptRejectAll') {
if (!_removeAcceptRejectAllUI)
_removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null
} else {
_removeAcceptRejectAllUI?.()
_removeAcceptRejectAllUI = null
}
}))
this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() }))
this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() }))
// when the model first mounts, refresh any diffs that might be on it (happens if diffs were added in the BG)
this._refreshStylesAndDiffsInURI(model.uri)
}
@ -350,44 +324,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
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);
} else {
delete this._sortedDiffsOfFspath[uri.fsPath];
}
}));
}
@ -494,40 +430,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
}
private _addAcceptRejectAllUI(uri: URI) {
// find all diffzones that aren't streaming
const diffZones: DiffZone[] = []
for (let diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea.type !== 'DiffZone') continue
if (diffArea._streamState.isStreaming) continue
diffZones.push(diffArea)
}
if (diffZones.length === 0) return
const consistentItemId = this._consistentItemService.addConsistentItemToURI({
uri,
fn: (editor) => {
const buttonsWidget = new AcceptAllRejectAllWidget({
editor,
onAcceptAll: () => {
this.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true })
this._metricsService.capture('Accept All', {})
},
onRejectAll: () => {
this.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true })
this._metricsService.capture('Reject All', {})
},
instantiationService: this._instantiationService,
})
return () => { buttonsWidget.dispose() }
}
})
return () => { this._consistentItemService.removeConsistentItemFromURI(consistentItemId) }
}
mostRecentTextOfCtrlKZoneId: Record<string, string | undefined> = {}
@ -564,9 +466,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
})
// mount react
let disposablesRef: IDisposable[] | undefined = undefined
let disposeFn: (() => void) | undefined = undefined
this._instantiationService.invokeFunction(accessor => {
disposablesRef = mountCtrlK(domNode, accessor, {
disposeFn = mountCtrlK(domNode, accessor, {
diffareaid: ctrlKZone.diffareaid,
@ -591,14 +493,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text;
},
initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null,
} satisfies QuickEditPropsType)
} satisfies QuickEditPropsType)?.dispose
})
// cleanup
return () => {
editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) })
disposablesRef?.forEach(d => d.dispose())
disposeFn?.()
}
})
@ -624,7 +525,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (diffArea.type !== 'CtrlKZone') continue
if (!diffArea._mountInfo) {
diffArea._mountInfo = this._addCtrlKZoneInput(diffArea)
// console.log('MOUNTED', diffArea.diffareaid)
console.log('MOUNTED CTRLK', diffArea.diffareaid)
}
else {
diffArea._mountInfo.refresh()
@ -748,15 +649,15 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
else { throw new Error('Void 1') }
const buttonsWidget = new AcceptRejectWidget({
const buttonsWidget = new AcceptRejectInlineWidget({
editor,
onAccept: () => {
this.acceptDiff({ diffid })
this._metricsService.capture('Accept Diff', {})
this._metricsService.capture('Accept Diff', { diffid })
},
onReject: () => {
this.rejectDiff({ diffid })
this._metricsService.capture('Reject Diff', {})
this._metricsService.capture('Reject Diff', { diffid })
},
diffid: diffid.toString(),
startLine,
@ -927,10 +828,6 @@ 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) {
@ -1023,10 +920,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
this.diffOfId[diffid] = newDiff
diffZone._diffOfId[diffid] = newDiff
if (!diffZone._streamState.isStreaming) {
this._onDidFinishAddOrDeleteDiffInDiffZone.fire({ uri })
}
return newDiff
}
@ -1094,6 +987,19 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private _fireChangeDiffsIfNotStreaming(uri: URI) {
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea?.type !== 'DiffZone') continue
// fire changed diffs (this is the only place Diffs are added)
if (!diffArea._streamState.isStreaming) {
this._onDidChangeDiffsInDiffZone.fire({ uri, diffareaid: diffArea.diffareaid })
}
}
}
private _refreshStylesAndDiffsInURI(uri: URI) {
// 1. clear DiffArea styles and Diffs
@ -1107,6 +1013,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
// 4. refresh ctrlK zones
this._refreshCtrlKInputs(uri)
// 5. this is the only place where diffs are changed, so can fire here only
this._fireChangeDiffsIfNotStreaming(uri)
}
@ -1301,34 +1210,48 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _startStreamingDiffZone({
uri,
startRange,
startBehavior,
streamRequestIdRef,
onUndo,
linkedCtrlKZone,
}: {
uri: URI,
startRange: 'fullFile' | [number, number],
startBehavior: 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts',
streamRequestIdRef: { current: string | null },
linkedCtrlKZone: CtrlKZone | null,
onUndo: () => void,
}) {
const { model } = this._voidModelService.getModel(uri)
if (!model) return
const startLine = startRange === 'fullFile' ? 1 : startRange[0]
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
let originalCode = startRange === 'fullFile' ? model.getValue(EndOfLinePreference.LF) : model.getValue(EndOfLinePreference.LF).split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
// treat like full file, unless linkedCtrlKZone was provided in which case use its diff's range
const startLine = linkedCtrlKZone ? linkedCtrlKZone.startLine : 1
const endLine = linkedCtrlKZone ? linkedCtrlKZone.endLine : model.getLineCount()
const range = { startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER }
const originalFileStr = model.getValue(EndOfLinePreference.LF)
let originalCode = model.getValueInRange(range, EndOfLinePreference.LF)
// add to history as a checkpoint, before we start modifying
const { onFinishEdit } = this._addToHistory(uri, { onUndo })
// clear diffZones so no conflict
if (startBehavior === 'keep-conflicts') {
// delete them then re-apply their change
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false })
const originalCodeReplacement = model.getValue(EndOfLinePreference.LF) // use this as original code
this._writeURIText(uri, originalCode, 'wholeFileRange', { shouldRealignDiffAreas: false }) // un-revert
originalCode = originalCodeReplacement
if (linkedCtrlKZone) {
// ctrlkzone should never have any conflicts
}
else {
// keep conflict on whole file - to keep conflict, revert the change and use those contents as original, then un-revert the change
const currentFileStr = originalFileStr
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false })
const oldFileStr = model.getValue(EndOfLinePreference.LF) // use this as original code
this._writeURIText(uri, currentFileStr, 'wholeFileRange', { shouldRealignDiffAreas: false }) // un-revert
originalCode = oldFileStr
}
}
else if (startBehavior === 'accept-conflicts' || startBehavior === 'reject-conflicts') {
const behavior = startBehavior === 'accept-conflicts' ? 'accept' : 'reject'
@ -1351,11 +1274,18 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
const diffZone = this._addDiffArea(adding)
this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
// a few items related to the ctrlKZone that started streaming this diffZone
if (linkedCtrlKZone) {
const ctrlKZone = linkedCtrlKZone
ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid
this._onDidChangeStreamingInCtrlKZone.fire({ uri, diffareaid: ctrlKZone.diffareaid })
}
console.log('DONE _STREAMING', diffZone)
console.log('DONE WITH _STARTSTREAMING', diffZone)
return { diffZone, onFinishEdit }
}
@ -1372,6 +1302,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
let uri: URI
let startRange: 'fullFile' | [number, number]
let ctrlKZoneIfQuickEdit: CtrlKZone | null = null
if (from === 'ClickApply') {
const uri_ = this._getActiveEditorURI()
if (!uri_) return
@ -1381,7 +1313,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
else if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
if (ctrlKZone?.type !== 'CtrlKZone') return
ctrlKZoneIfQuickEdit = ctrlKZone
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
uri = _URI
startRange = [startLine_, endLine_]
@ -1390,10 +1323,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
throw new Error(`Void: diff.type not recognized on: ${from}`)
}
console.log('q1', this.diffAreaOfId)
await this._voidModelService.initializeModel(uri)
console.log('q2', this.diffAreaOfId)
const { model } = this._voidModelService.getModel(uri)
if (!model) return
console.log('q3', this.diffAreaOfId)
// promise that resolves when the apply is done
let resApplyPromise: () => void
@ -1401,16 +1337,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const applyPromise = new Promise<void>((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ })
let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId
// start diffzone
const res = this._startStreamingDiffZone({
uri,
streamRequestIdRef,
startRange,
startBehavior: opts.startBehavior,
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) },
})
if (!res) return
const { diffZone, onFinishEdit } = res
// build messages
const quickEditFIMTags = defaultQuickEditFimTags // TODO can eventually let users customize modelFimTags
@ -1426,11 +1352,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
]
}
else if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
const { _mountInfo } = ctrlKZone
console.log('aaa')
if (!ctrlKZoneIfQuickEdit) return
console.log('bbb', ctrlKZoneIfQuickEdit)
const { _mountInfo } = ctrlKZoneIfQuickEdit
console.log('ccc', _mountInfo)
const instructions = _mountInfo?.textAreaRef.current?.value ?? ''
console.log('ddd', instructions)
const startLine = startRange === 'fullFile' ? 1 : startRange[0]
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
@ -1445,27 +1373,30 @@ class EditCodeService extends Disposable implements IEditCodeService {
else { throw new Error(`featureName ${from} is invalid`) }
// a few items related to writeover streams and quickEdits
if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
// start diffzone
const res = this._startStreamingDiffZone({
uri,
streamRequestIdRef,
startBehavior: opts.startBehavior,
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) },
linkedCtrlKZone: ctrlKZoneIfQuickEdit,
})
if (!res) return
const { diffZone, onFinishEdit } = res
ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid
this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid })
}
// helpers
const onDone = () => {
console.log('called onDone')
diffZone._streamState = { isStreaming: false, }
this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
if (from === 'QuickEdit') {
const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
if (ctrlKZoneIfQuickEdit) {
const ctrlKZone = ctrlKZoneIfQuickEdit
ctrlKZone._linkedStreamingDiffZone = null
this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid })
this._onDidChangeStreamingInCtrlKZone.fire({ uri, diffareaid: ctrlKZone.diffareaid })
this._deleteCtrlKZone(ctrlKZone)
}
this._refreshStylesAndDiffsInURI(uri)
@ -1593,16 +1524,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const applyDonePromise = new Promise<void>((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ })
let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId
// start diffzone
const res = this._startStreamingDiffZone({
uri,
streamRequestIdRef,
startRange: 'fullFile',
startBehavior: opts.startBehavior,
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by user pressing undo.')) },
})
if (!res) return
const { diffZone, onFinishEdit } = res
// build messages - ask LLM to generate search/replace block text
const originalFileCode = model.getValue(EndOfLinePreference.LF)
@ -1612,6 +1533,18 @@ class EditCodeService extends Disposable implements IEditCodeService {
{ role: 'user', content: userMessageContent },
]
// start diffzone
const res = this._startStreamingDiffZone({
uri,
streamRequestIdRef,
startBehavior: opts.startBehavior,
linkedCtrlKZone: null,
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by user pressing undo.')) },
})
if (!res) return
const { diffZone, onFinishEdit } = res
// helpers
type SearchReplaceDiffAreaMetadata = {
originalBounds: [number, number], // 1-indexed
@ -1656,7 +1589,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const onDone = () => {
diffZone._streamState = { isStreaming: false, }
this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
this._refreshStylesAndDiffsInURI(uri)
// delete the tracking zones
@ -1929,7 +1862,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._llmMessageService.abort(streamRequestId)
diffZone._streamState = { isStreaming: false, }
this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
}
_undoHistory(uri: URI) {
@ -1972,24 +1905,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private _getDiffZonesOnURI(uri: URI) {
const diffZones = [...this.diffAreasOfURI[uri.fsPath]?.values() ?? []]
.map(diffareaid => this.diffAreaOfId[diffareaid])
.filter(diffArea => !!diffArea && diffArea.type === 'DiffZone')
return diffZones
}
getURIStreamState = ({ uri }: { uri: URI | null }) => {
if (uri === null) return 'idle'
const diffZones = this._getDiffZonesOnURI(uri)
const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming)
const state: URIStreamState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll')
return state
}
interruptURIStreaming({ uri }: { uri: URI }) {
// brute force for now is OK
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
@ -2013,14 +1928,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
// onFinishEdit()
// }
private _revertAndDeleteDiffZone(diffZone: DiffZone) {
private _revertDiffZone(diffZone: DiffZone) {
const uri = diffZone._URI
const writeText = diffZone.originalCode
const toRange: IRange = { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }
this._writeURIText(uri, writeText, toRange, { shouldRealignDiffAreas: true })
this._deleteDiffZone(diffZone)
}
@ -2037,7 +1950,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (!diffArea) continue
if (diffArea.type === 'DiffZone') {
if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea)
if (behavior === 'reject') {
this._revertDiffZone(diffArea)
this._deleteDiffZone(diffArea)
}
else if (behavior === 'accept') this._deleteDiffZone(diffArea)
}
else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) {
@ -2209,62 +2125,18 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
// testDiffs(): DiffZone | undefined {
// const uri = this._getActiveEditorURI()
// if (!uri) return
// const startLine = 1
// const endLine = 4
// const currentFileStr = this._readURI(uri)
// if (currentFileStr === null) return
// const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
// const { onFinishEdit } = this._addToHistory(uri)
// const adding: Omit<DiffZone, 'diffareaid'> = {
// type: 'DiffZone',
// originalCode,
// startLine,
// endLine,
// _URI: uri,
// _streamState: { isStreaming: false, },
// _diffOfId: {}, // added later
// _removeStylesFns: new Set(),
// }
// const diffZone = this._addDiffArea(adding)
// const endResult = `\
// const x = 1;
// if (x > 0) {
// console.log('hi!')
// }`
// this._writeText(uri, endResult,
// { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
// { shouldRealignDiffAreas: true }
// )
// diffZone._streamState = { isStreaming: false, }
// this._refreshStylesAndDiffsInURI(uri)
// onFinishEdit()
// return diffZone
// }
}
registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager);
const acceptBg = '#1a7431'
const acceptAllBg = '#1e8538'
const acceptBorder = '1px solid #145626'
const rejectBg = '#b42331'
const rejectAllBg = '#cf2838'
const rejectBorder = '1px solid #8e1c27'
const buttonFontSize = '11px'
const buttonTextColor = 'white'
class AcceptRejectWidget extends Widget implements IOverlayWidget {
class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
public getId() { return this.ID }
public getDomNode() { return this._domNode; }
@ -2380,97 +2252,3 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget {
class AcceptAllRejectAllWidget extends Widget implements IOverlayWidget {
private readonly _domNode: HTMLElement;
private readonly editor: ICodeEditor;
private readonly ID: string;
private readonly _instantiationService: IInstantiationService;
constructor({ editor, onAcceptAll, onRejectAll, instantiationService }: {
editor: ICodeEditor,
onAcceptAll: () => void,
onRejectAll: () => void,
instantiationService: IInstantiationService
}) {
super();
this.ID = editor.getModel()?.uri.fsPath + '';
this.editor = editor;
this._instantiationService = instantiationService;
// Create container div with buttons
const { voidCommandBar, acceptButton, rejectButton, buttons } = dom.h('div@buttons', [
dom.h('div@voidCommandBar', []),
dom.h('button@acceptButton', []),
dom.h('button@rejectButton', [])
]);
// Style the container
buttons.style.zIndex = '2';
buttons.style.padding = '4px';
buttons.style.display = 'flex';
buttons.style.gap = '4px';
buttons.style.alignItems = 'center';
// Mount command bar using mountVoidCommandBar
this._instantiationService.invokeFunction(accessor => {
console.log(voidCommandBar)
if (voidCommandBar) { // remove this
Math.random()
}
// mountVoidCommandBar(voidCommandBar, accessor, {})
});
// Style accept button
acceptButton.addEventListener('click', onAcceptAll)
acceptButton.textContent = 'Accept All';
acceptButton.style.backgroundColor = acceptAllBg;
acceptButton.style.border = acceptBorder;
acceptButton.style.color = buttonTextColor;
acceptButton.style.fontSize = buttonFontSize;
acceptButton.style.padding = '4px 8px';
acceptButton.style.borderRadius = '6px';
acceptButton.style.cursor = 'pointer';
// Style reject button
rejectButton.addEventListener('click', onRejectAll)
rejectButton.textContent = 'Reject All';
rejectButton.style.backgroundColor = rejectAllBg;
rejectButton.style.border = rejectBorder;
rejectButton.style.color = buttonTextColor;
rejectButton.style.fontSize = buttonFontSize;
rejectButton.style.color = 'white';
rejectButton.style.padding = '4px 8px';
rejectButton.style.borderRadius = '6px';
rejectButton.style.cursor = 'pointer';
this._domNode = buttons;
// Mount the widget
editor.addOverlayWidget(this);
}
public getId(): string {
return this.ID;
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getPosition() {
return {
preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER,
}
}
public override dispose(): void {
this.editor.removeOverlayWidget(this);
super.dispose();
}
}

View file

@ -32,7 +32,7 @@ export type AddCtrlKOpts = {
editor: ICodeEditor,
}
export type URIStreamState = 'idle' | 'acceptRejectAll' | 'streaming'
export type URIAcceptRejectState = 'idle' | 'acceptRejectAll' | 'streaming'
export const IEditCodeService = createDecorator<IEditCodeService>('editCodeService');
@ -40,32 +40,28 @@ 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 };
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
diffAreaOfId: Record<string, DiffArea>;
diffAreasOfURI: Record<string, Set<string> | undefined>;
diffOfId: Record<string, Diff>;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void;
// events
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
onDidAddOrDeleteDiffInDiffZone: Event<{ uri: URI }>;
onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>;
onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>;
// CtrlKZone streaming state
isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean;
interruptCtrlKStreaming(opts: { diffareaid: number }): void;
onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>;
// // DiffZone codeBoxId streaming state
getURIStreamState(opts: { uri: URI | null }): URIStreamState;
interruptURIStreaming(opts: { uri: URI }): void;
onDidChangeURIStreamState: Event<{ uri: URI; state: URIStreamState }>;
// testDiffs(): void;
}

View file

@ -25,7 +25,7 @@ export interface IConsistentItemService {
export const IConsistentItemService = createDecorator<IConsistentItemService>('ConsistentItemService');
export class ConsistentItemService extends Disposable {
export class ConsistentItemService extends Disposable implements IConsistentItemService {
readonly _serviceBrand: undefined

View file

@ -33,12 +33,11 @@ class MetricsPollService extends Disposable implements IMetricsPollService {
// initial state
const { window } = dom.getActiveWindow()
let i = 1
let i = 0
this.intervalID = window.setInterval(() => {
this.metricsService.capture('Alive', { i })
this.metricsService.capture('Alive', { iv1: i })
i += 1
console.log('ping', i)
}, PING_EVERY_MS)

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js'
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
import { usePromise, useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
@ -128,28 +128,37 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const metricsService = accessor.get('IMetricsService')
const [_, rerender] = useState(0)
const getUriBeingApplied = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
const getStreamState = useCallback(() => editCodeService.getURIStreamState({ uri: getUriBeingApplied() }), [editCodeService, getUriBeingApplied])
const getUriBeingApplied = useCallback(() => {
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}, [applyBoxId])
const getStreamState = useCallback(() => {
const uri = getUriBeingApplied()
if (!uri) return 'idle-no-changes'
return voidCommandBarService.getStreamState(uri)
}, [voidCommandBarService, getUriBeingApplied])
// listen for stream updates on this box
useURIStreamState(
useCallback((uri_, newStreamState) => {
const shouldUpdate = (
getUriBeingApplied()?.fsPath === uri_.fsPath
|| (uri === 'current' ? false : uri.fsPath === uri_.fsPath)
)
if (!shouldUpdate) return
rerender(c => c + 1)
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = (
getUriBeingApplied()?.fsPath === uri_.fsPath
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
)
if (!shouldUpdate) return
rerender(c => c + 1)
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
)
const onClickSubmit = useCallback(async () => {
if (isDisabled) return
if (getStreamState() === 'streaming') return
if (getStreamState()) return
const [newApplyingUri, _] = await editCodeService.startApplying({
from: 'ClickApply',
applyStr: codeStr,
@ -167,7 +176,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
const onInterrupt = useCallback(() => {
if (getStreamState() !== 'streaming') return
if (!getStreamState()) return
const uri = getUriBeingApplied()
if (!uri) return
@ -250,7 +259,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
</>
}
if (currStreamState === 'idle') {
if (currStreamState === 'idle-no-changes') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{copyButton}
@ -258,7 +267,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
</>
}
if (currStreamState === 'acceptRejectAll') {
if (currStreamState === 'idle-has-changes') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{reapplyButton}
@ -270,9 +279,9 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
const statusIndicatorHTML = <div className='flex flex-row items-center size-4'>
<div
className={` size-1.5 rounded-full border
${currStreamState === 'idle' ? 'bg-void-bg-3 border-void-border-1' :
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
currStreamState === 'acceptRejectAll' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
'bg-void-border-1 border-void-border-1'
}`
}

View file

@ -28,7 +28,7 @@ export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocati
}
function isValidUri(s: string): boolean {
return s.length > 5 && isAbsolute(s) && !s.startsWith('//') // common case that is a false positive is comments like //
return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like //
}
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {

View file

@ -66,6 +66,7 @@ export const QuickEditChat = ({
editCodeService.startApplying({
from: 'QuickEdit',
diffareaid,
startBehavior: 'keep-conflicts',
})
}, [isStreamingRef, isDisabled, editCodeService, diffareaid])

View file

@ -10,7 +10,7 @@
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI } from '../util/services.js';
import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js';
import { URI } from '../../../../../../../base/common/uri.js';
@ -208,7 +208,7 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) =>
const nameOfChatMode = {
'normal': 'Normal',
'normal': 'Chat',
'gather': 'Gather',
'agent': 'Agent',
}
@ -488,18 +488,18 @@ export const SelectedFiles = (
const modelReferenceService = accessor.get('IVoidModelService')
// state for tracking prospective files
const { currentUri } = useUriState()
const { uri: currentURI } = useActiveURI()
const [recentUris, setRecentUris] = useState<URI[]>([])
const maxRecentUris = 10
const maxProspectiveFiles = 3
useEffect(() => { // handle recent files
if (!currentUri) return
if (!currentURI) return
setRecentUris(prev => {
const withoutCurrent = prev.filter(uri => uri.fsPath !== currentUri.fsPath) // remove duplicates
const withCurrent = [currentUri, ...withoutCurrent]
const withoutCurrent = prev.filter(uri => uri.fsPath !== currentURI.fsPath) // remove duplicates
const withCurrent = [currentURI, ...withoutCurrent]
return withCurrent.slice(0, maxRecentUris)
})
}, [currentUri])
}, [currentURI])
const [prospectiveSelections, setProspectiveSelections] = useState<StagingSelectionItem[]>([])

View file

@ -19,7 +19,20 @@ export const mountFnGenerator = (Component: (params: any) => React.ReactNode) =>
const disposables = _registerServices(accessor)
const root = ReactDOM.createRoot(rootElement)
root.render(<Component {...props} />); // tailwind dark theme indicator
return disposables
const rerender = (props?: any) => {
root.render(<Component {...props} />); // tailwind dark theme indicator
}
const dispose = () => {
root.unmount();
disposables.forEach(d => d.dispose());
}
rerender(props)
const returnVal = {
rerender,
dispose,
}
return returnVal
}

View file

@ -9,7 +9,6 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { VoidUriState } from '../../../voidUriStateService.js';
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
@ -23,9 +22,8 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
import { IEditCodeService, URIStreamState } from '../../../editCodeServiceInterface.js'
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
import { IVoidUriStateService } from '../../../voidUriStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
@ -47,15 +45,13 @@ import { ITerminalToolService } from '../../../terminalToolService.js'
import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'
import { IVoidModelService } from '../../../../common/voidModelService.js'
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
let uriState: VoidUriState
const uriStateListeners: Set<(s: VoidUriState) => void> = new Set()
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
@ -77,8 +73,8 @@ let colorThemeState: ColorScheme
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set()
const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set()
const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set();
const activeURIListeners: Set<(uri: URI | null) => void> = new Set();
// must call this before you can use any of the hooks below
@ -90,24 +86,17 @@ export const _registerServices = (accessor: ServicesAccessor) => {
_registerAccessor(accessor)
const stateServices = {
uriStateService: accessor.get(IVoidUriStateService),
sidebarStateService: accessor.get(ISidebarStateService),
chatThreadsStateService: accessor.get(IChatThreadService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
themeService: accessor.get(IThemeService),
editCodeService: accessor.get(IEditCodeService),
voidCommandBarService: accessor.get(IVoidCommandBarService),
modelService: accessor.get(IModelService),
}
const { uriStateService, sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService } = stateServices
uriState = uriStateService.state
disposables.push(
uriStateService.onDidChangeState(() => {
uriState = uriStateService.state
uriStateListeners.forEach(l => l(uriState))
})
)
const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
sidebarState = sidebarStateService.state
disposables.push(
@ -161,15 +150,21 @@ export const _registerServices = (accessor: ServicesAccessor) => {
// no state
disposables.push(
editCodeService.onDidChangeCtrlKZoneStreaming(({ diffareaid }) => {
editCodeService.onDidChangeStreamingInCtrlKZone(({ diffareaid }) => {
const isStreaming = editCodeService.isCtrlKZoneStreaming({ diffareaid })
ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming))
})
)
disposables.push(
editCodeService.onDidChangeURIStreamState(({ uri }) => {
const isStreaming = editCodeService.getURIStreamState({ uri })
uriStreamingStateListeners.forEach(l => l(uri, isStreaming))
voidCommandBarService.onDidChangeState(({ uri }) => {
commandBarURIStateListeners.forEach(l => l(uri));
})
)
disposables.push(
voidCommandBarService.onDidChangeActiveURI(({ uri }) => {
activeURIListeners.forEach(l => l(uri));
})
)
@ -193,7 +188,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IEditCodeService: accessor.get(IEditCodeService),
IVoidUriStateService: accessor.get(IVoidUriStateService),
ISidebarStateService: accessor.get(ISidebarStateService),
IChatThreadService: accessor.get(IChatThreadService),
@ -218,6 +212,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IVoidModelService: accessor.get(IVoidModelService),
IWorkspaceContextService: accessor.get(IWorkspaceContextService),
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
} as const
return reactAccessor
}
@ -244,16 +240,6 @@ export const useAccessor = () => {
// -- state of services --
export const useUriState = () => {
const [s, ss] = useState(uriState)
useEffect(() => {
ss(uriState)
uriStateListeners.add(ss)
return () => { uriStateListeners.delete(ss) }
}, [ss])
return s
}
export const useSidebarState = () => {
const [s, ss] = useState(sidebarState)
useEffect(() => {
@ -340,14 +326,6 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo
}, [listener, ctrlKZoneStreamingStateListeners])
}
export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => {
useEffect(() => {
uriStreamingStateListeners.add(listener)
return () => { uriStreamingStateListeners.delete(listener) }
}, [listener, uriStreamingStateListeners])
}
export const useIsDark = () => {
const [s, ss] = useState(colorThemeState)
useEffect(() => {
@ -359,6 +337,40 @@ export const useIsDark = () => {
// s is the theme, return isDark instead of s
const isDark = s === ColorScheme.DARK || s === ColorScheme.HIGH_CONTRAST_DARK
return isDark
}
export const useCommandBarURIListener = (listener: (uri: URI) => void) => {
useEffect(() => {
commandBarURIStateListeners.add(listener);
return () => { commandBarURIStateListeners.delete(listener) };
}, [listener]);
};
export const useCommandBarState = () => {
const accessor = useAccessor()
const commandBarService = accessor.get('IVoidCommandBarService')
const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
const listener = useCallback(() => {
ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
}, [commandBarService])
useCommandBarURIListener(listener)
return s;
}
// roughly gets the active URI - this is used to get the history of recent URIs
export const useActiveURI = () => {
const accessor = useAccessor()
const commandBarService = accessor.get('IVoidCommandBarService')
const [s, ss] = useState(commandBarService.activeURI)
useEffect(() => {
const listener = () => { ss(commandBarService.activeURI) }
activeURIListeners.add(listener);
return () => { activeURIListeners.delete(listener) };
}, [])
return { uri: s }
}

View file

@ -4,186 +4,241 @@
*--------------------------------------------------------------------------------------*/
import { useAccessor, useIsDark, useUriState } from '../util/services.js';
import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js';
import '../styles.css'
import { DiffZone } from '../../../editCodeService.js';
import { useCallback, useEffect, useState } from 'react';
import { URI } from '../../../../../../../base/common/uri.js';
import { ICodeEditor } from '../../../../../../../editor/browser/editorBrowser.js';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
import { getBasename } from '../sidebar-tsx/SidebarChat.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js';
export const VoidCommandBarMain = ({ className }: { className: string }) => {
export type VoidCommandBarProps = {
uri: URI | null;
editor: ICodeEditor;
}
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const isDark = useIsDark()
console.log('VoidCommandBarMain', uri?.fsPath)
return <div
className={`@@void-scope ${isDark ? 'dark' : ''}`}
>
<VoidCommandBar />
<VoidCommandBar uri={uri} editor={editor} />
</div>
}
const VoidCommandBar = () => {
const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => {
if (len === 0) return null
if (len === 1 && step === -1) return null // don't step backwards if just 1 element
return ((currIdx ?? 0) + step) % len
}
const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor }) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const editorService = accessor.get('ICodeEditorService')
const metricsService = accessor.get('IMetricsService')
const commandService = accessor.get('ICommandService')
const commandBarService = accessor.get('IVoidCommandBarService')
const voidModelService = accessor.get('IVoidModelService')
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
const [_, rerender] = useState(0)
// Add a state variable to track focus
const [isFocused, setIsFocused] = useState(false)
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 { currentUri } = useUriState()
// trigger rerender when diffzone is created (TODO need to also update when diff is accepted/rejected)
// changes if the user clicks left/right or if the user goes on a uri with changes
const [currUriIdx, setUriIdx] = useState<number | null>(null)
const [currUriHasChanges, setCurrUriHasChanges] = useState(false)
useEffect(() => {
const disposable = editCodeService.onDidAddOrDeleteDiffInDiffZone(() => {
rerender(c => c + 1) // rerender
})
return () => disposable.dispose()
}, [editCodeService, rerender])
const getNextDiff = useCallback(({ step }: { step: 1 | -1 }) => {
if (!currentUri) {
return;
const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath)
if (i !== -1) {
setUriIdx(i)
setCurrUriHasChanges(true)
}
const sortedDiffs = editCodeService._sortedDiffsOfFspath[currentUri.fsPath]
if (!sortedDiffs || sortedDiffs.length === 0) {
return;
else {
setCurrUriHasChanges(false)
}
}, [sortedCommandBarURIs, uri])
const currentDiffIdx = diffIdxOfFspath[currentUri.fsPath] || 0
const nextDiffIdx = (currentDiffIdx + step) % sortedDiffs.length
// just for style
const [isFocused, setIsFocused] = useState(false)
const nextDiff = sortedDiffs[nextDiffIdx]
return { nextDiff, nextDiffIdx }
}, [currentUri, editCodeService._sortedDiffsOfFspath, diffIdxOfFspath])
const getNextUri = useCallback(({ step }: { step: 1 | -1 }) => {
const sortedUris = editCodeService._sortedUrisWithDiffs
if (sortedUris.length === 0) {
return;
const getNextDiffIdx = (step: 1 | -1) => {
// check undefined
if (!uri) return null
const s = commandBarState[uri.fsPath]
if (!s) return null
const { diffIdx, sortedDiffIds } = s
// get next idx
const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step)
return nextDiffIdx
}
const goToDiffIdx = (idx: number | null) => {
// check undefined
if (!uri) return
const s = commandBarState[uri.fsPath]
if (!s) return
const { sortedDiffIds } = s
// reveal
if (idx) {
const diffid = sortedDiffIds[idx]
const diff = editCodeService.diffOfId[diffid]
const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 };
editor.revealRange(range, ScrollType.Immediate)
}
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 }
}, [currentUri, editCodeService._sortedUrisWithDiffs])
const gotoNextDiff = ({ step }: { step: 1 | -1 }) => {
// get the next diff
const res = getNextDiff({ step })
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 }))
}
const gotoNextUri = ({ step }: { step: 1 | -1 }) => {
// get the next uri
const res = getNextUri({ step })
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)
})
const getNextUriIdx = (step: 1 | -1) => {
return stepIdx(currUriIdx, sortedCommandBarURIs.length, step)
}
const goToURIIdx = async (idx: number | null) => {
if (idx === null) return
const nextURI = sortedCommandBarURIs[idx]
editCodeService.diffAreasOfURI
const { model } = await voidModelService.getModelSafe(nextURI)
if (model) { editor.setModel(model) } // switch to the URI
}
return <div
className={`flex items-center gap-2 p-2 ${isFocused ? 'ring-1 ring-[var(--vscode-focusBorder)]' : ''}`}
onFocusCapture={() => setIsFocused(true)}
onBlurCapture={() => setIsFocused(false)}
// when change URI, scroll to the proper spot
useEffect(() => {
setTimeout(() => {
// check undefined
if (!uri) return
const s = commandBarState[uri.fsPath]
if (!s) return
const { diffIdx } = s
goToDiffIdx(diffIdx)
}, 50)
}, [uri])
const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null
const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? null : null
const nextDiffIdx = getNextDiffIdx(1)
const prevDiffIdx = getNextDiffIdx(-1)
const nextURIIdx = getNextUriIdx(1)
const prevURIIdx = getNextUriIdx(-1)
if (sortedCommandBarURIs.length === 0) return null // if there are absolutely no changes
const navPanel = <div
className={`pointer-events-auto flex items-center gap-2 p-2 ${isFocused ? 'ring-1 ring-[var(--vscode-focusBorder)]' : ''}`}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
>
<div className="flex gap-1">
<button
className={`px-2 py-1 rounded hover:bg-[var(--vscode-button-hoverBackground)] ${!getNextDiff({ step: -1 }) ? 'opacity-50' : ''}`}
disabled={!getNextDiff({ step: -1 })}
onClick={() => gotoNextDiff({ step: -1 })}
className={`
px-2 py-1 rounded hover:bg-[var(--vscode-button-hoverBackground)]
${prevDiffIdx === null ? 'opacity-50' : ''}
`}
disabled={prevDiffIdx === null}
onClick={() => { goToDiffIdx(prevDiffIdx) }}
title="Previous diff"
></button>
<button
className={`px-2 py-1 rounded hover:bg-[var(--vscode-button-hoverBackground)] ${!getNextDiff({ step: 1 }) ? 'opacity-50' : ''}`}
disabled={!getNextDiff({ step: 1 })}
onClick={() => gotoNextDiff({ step: 1 })}
className={`
px-2 py-1 rounded hover:bg-[var(--vscode-button-hoverBackground)]
${nextDiffIdx === null ? 'opacity-50' : ''}
`}
disabled={nextDiffIdx === null}
onClick={() => { goToDiffIdx(nextDiffIdx) }}
title="Next diff"
></button>
<button
className={`px-2 py-1 rounded hover:bg-[var(--vscode-button-hoverBackground)] ${!getNextUri({ step: -1 }) ? 'opacity-50' : ''}`}
disabled={!getNextUri({ step: -1 })}
onClick={() => gotoNextUri({ step: -1 })}
className={`
px-2 py-1 rounded hover:bg-[var(--vscode-button-hoverBackground)]
${prevURIIdx === null ? 'opacity-50' : ''}
`}
disabled={prevURIIdx === null}
onClick={() => goToURIIdx(prevURIIdx)}
title="Previous file"
></button>
<button
className={`px-2 py-1 rounded hover:bg-[var(--vscode-button-hoverBackground)] ${!getNextUri({ step: 1 }) ? 'opacity-50' : ''}`}
disabled={!getNextUri({ step: 1 })}
onClick={() => gotoNextUri({ step: 1 })}
className={`
px-2 py-1 rounded hover:bg-[var(--vscode-button-hoverBackground)]
${nextURIIdx === null ? 'opacity-50' : ''}
`}
disabled={nextURIIdx === null}
onClick={() => goToURIIdx(nextURIIdx)}
title="Next file"
></button>
</div>
<div className="text-[var(--vscode-editor-foreground)] text-xs flex gap-4">
<div>File {(editCodeService._sortedUrisWithDiffs.findIndex(u => u.fsPath === currentUri?.fsPath) ?? 0) + 1} of {editCodeService._sortedUrisWithDiffs.length}</div>
<div>Diff {(diffIdxOfFspath[currentUri?.fsPath ?? ''] ?? 0) + 1} of {editCodeService._sortedDiffsOfFspath[currentUri?.fsPath ?? '']?.length ?? 0}</div>
<div>
File {(currUriIdx ?? 0) + 1} of {sortedCommandBarURIs.length}
</div>
<div>
Diff {(currDiffIdx ?? 0) + 1} of {sortedDiffIds?.length ?? 0}
</div>
</div>
</div>
const onAcceptAll = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Accept All', {})
}
const onRejectAll = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Reject All', {})
}
const acceptRejectButtons = currUriHasChanges && <div className="flex gap-2">
<button
className='pointer-events-auto'
onClick={onAcceptAll}
style={{
backgroundColor: acceptAllBg,
border: acceptBorder,
color: buttonTextColor,
fontSize: buttonFontSize,
padding: '4px 8px',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Accept All
</button>
<button
className='pointer-events-auto'
onClick={onRejectAll}
style={{
backgroundColor: rejectAllBg,
border: rejectBorder,
color: 'white',
fontSize: buttonFontSize,
padding: '4px 8px',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Reject All
</button>
</div>
return <>
{navPanel}
{acceptRejectButtons}
</>
}

View file

@ -15,19 +15,15 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
import { VOID_VIEW_ID } from './sidebarPane.js';
import { IMetricsService } from '../common/metricsService.js';
import { ISidebarStateService } from './sidebarStateService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { localize2 } from '../../../../nls.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { IVoidUriStateService } from './voidUriStateService.js';
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
import { IChatThreadService } from './chatThreadService.js';
@ -286,43 +282,3 @@ export class TabSwitchListener extends Disposable {
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
}
}
class TabSwitchContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.void.tabswitch'
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IViewsService private readonly viewsService: IViewsService,
@IVoidUriStateService private readonly uriStateService: IVoidUriStateService,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
// @ICommandService private readonly commandService: ICommandService,
) {
super()
// sidebarIsVisible state
let sidebarIsVisible = this.viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
this._register(this.viewsService.onDidChangeViewVisibility(e => {
sidebarIsVisible = e.visible
}))
const onSwitchTab = () => { // update state
if (sidebarIsVisible) {
const currentUri = this.codeEditorService.getActiveCodeEditor()?.getModel()?.uri
if (!currentUri) return;
this.uriStateService.setState({ currentUri })
// this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
}
}
// when sidebar becomes visible, add current file
this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible }))
// run on current tab if it exists, and listen for tab switches and visibility changes
onSwitchTab()
this._register(this.viewsService.onDidChangeViewVisibility(() => { onSwitchTab() }))
this._register(this.instantiationService.createInstance(TabSwitchListener, () => { onSwitchTab() }))
}
}
registerWorkbenchContribution2(TabSwitchContribution.ID, TabSwitchContribution, WorkbenchPhase.BlockRestore);

View file

@ -38,7 +38,7 @@ import { mountSidebar } from './react/out/sidebar-tsx/index.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
// import { IDisposable } from '../../../../base/common/lifecycle.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { toDisposable } from '../../../../base/common/lifecycle.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
@ -80,8 +80,8 @@ class SidebarViewPane extends ViewPane {
// gets set immediately
this.instantiationService.invokeFunction(accessor => {
// mount react
const disposables: IDisposable[] | undefined = mountSidebar(parent, accessor);
disposables?.forEach(d => this._register(d))
const disposeFn: (() => void) | undefined = mountSidebar(parent, accessor)?.dispose;
this._register(toDisposable(() => disposeFn?.()))
});
}

View file

@ -331,7 +331,6 @@ export class ToolsService implements IToolsService {
if (isFolder)
await fileService.createFolder(uri)
else {
await voidModelService.initializeModel(uri)
await fileService.createFile(uri)
}
return {}

View file

@ -43,6 +43,8 @@ import './chatThreadService.js'
// ping
import './metricsPollService.js'
// helper services
import './helperServices/consistentItemService.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------

View file

@ -0,0 +1,419 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { URI } from '../../../../base/common/uri.js';
import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { IOverlayWidget, ICodeEditor, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js'
import { deepClone } from '../../../../base/common/objects.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
export interface IVoidCommandBarService {
readonly _serviceBrand: undefined;
stateOfURI: { [uri: string]: CommandBarStateType };
sortedURIs: URI[];
activeURI: URI | null;
onDidChangeState: Event<{ uri: URI }>;
onDidChangeActiveURI: Event<{ uri: URI | null }>;
getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes';
setDiffIdx(uri: URI, newIdx: number | null): void
}
export const IVoidCommandBarService = createDecorator<IVoidCommandBarService>('VoidCommandBarService');
export type CommandBarStateType = undefined | {
sortedDiffZoneIds: string[]; // sorted by line number
sortedDiffIds: string[]; // sorted by line number (computed)
isStreaming: boolean; // is any diffZone streaming in this URI
diffIdx: number | null; // must refresh whenever sortedDiffIds does so it's valid
}
const defaultState: NonNullable<CommandBarStateType> = {
sortedDiffZoneIds: [],
sortedDiffIds: [],
isStreaming: false,
diffIdx: null,
}
export class VoidCommandBarService extends Disposable implements IVoidCommandBarService {
_serviceBrand: undefined;
static readonly ID: 'void.VoidCommandBarService'
// depends on uri -> diffZone -> {streaming, diffs}
public stateOfURI: { [uri: string]: CommandBarStateType } = {}
public sortedURIs: URI[] = [] // keys of state (depends on diffZones in the uri)
private readonly _hooks = new Set<URI>() // uriFsPaths
// Emits when a URI's stream state changes between idle, streaming, and acceptRejectAll
private readonly _onDidChangeState = new Emitter<{ uri: URI }>();
readonly onDidChangeState = this._onDidChangeState.event;
// active URI
activeURI: URI | null = null;
private readonly _onDidChangeActiveURI = new Emitter<{ uri: URI | null }>();
readonly onDidChangeActiveURI = this._onDidChangeActiveURI.event;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IModelService private readonly _modelService: IModelService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
) {
super();
const registeredModelURIs = new Set<string>()
const initializeModel = async (model: ITextModel) => {
// do not add listeners to the same model twice - important, or will see duplicates
if (registeredModelURIs.has(model.uri.fsPath)) return
registeredModelURIs.add(model.uri.fsPath)
this._hooks.add(model.uri)
}
// initialize all existing models + initialize when a new model mounts
this._modelService.getModels().forEach(model => { initializeModel(model) })
this._register(this._modelService.onModelAdded(model => { initializeModel(model) }));
const updateActiveURI = () => {
const currentUri = this._codeEditorService.getActiveCodeEditor()?.getModel()?.uri ?? null
this.activeURI = currentUri
if (!currentUri) return;
this._onDidChangeActiveURI.fire({ uri: currentUri })
}
// for every new editor, add the floating widget and update active URI
const disposablesOfEditorId: { [editorId: string]: IDisposable[] } = {};
const onCodeEditorAdd = (editor: ICodeEditor) => {
const id = editor.getId();
disposablesOfEditorId[id] = [];
const d1 = this._instantiationService.createInstance(AcceptRejectAllFloatingWidget, { editor });
disposablesOfEditorId[id].push(d1);
const d2 = editor.onDidChangeModel((e) => { if (e?.newModelUrl?.scheme === 'file') updateActiveURI() })
disposablesOfEditorId[id].push(d2);
}
const onCodeEditorRemove = (editor: ICodeEditor) => {
const id = editor.getId();
if (disposablesOfEditorId[id]) {
disposablesOfEditorId[id].forEach(d => d.dispose());
delete disposablesOfEditorId[id];
}
}
this._register(this._codeEditorService.onCodeEditorAdd((editor) => { onCodeEditorAdd(editor) }))
this._register(this._codeEditorService.onCodeEditorRemove((editor) => { onCodeEditorRemove(editor) }))
this._codeEditorService.listCodeEditors().forEach(editor => { onCodeEditorAdd(editor) })
// state updaters
this._register(this._editCodeService.onDidAddOrDeleteDiffZones(e => {
for (const uri of this._hooks) {
if (e.uri.fsPath !== uri.fsPath) return
// --- sortedURIs: delete if empty, add if not in state yet
const diffZones = this._getDiffZonesOnURI(uri)
if (diffZones.length === 0) {
this._deleteURIEntryFromState(uri)
this._onDidChangeState.fire({ uri })
continue // deleted, so done
}
if (!this.sortedURIs.find(uri2 => uri2.fsPath === uri.fsPath)) {
this._addURIEntryToState(uri)
}
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
// update state of the diffZones on this URI
const oldDiffZones = currState.sortedDiffZoneIds
const currentDiffZones = this._editCodeService.diffAreasOfURI[uri.fsPath] || [] // a Set
const { addedDiffZones, deletedDiffZones } = this._getDiffZoneChanges(oldDiffZones, currentDiffZones || [])
const diffZonesWithoutDeleted = oldDiffZones.filter(olddiffareaid => !deletedDiffZones.has(olddiffareaid))
// --- new state:
const newSortedDiffZoneIds = [
...diffZonesWithoutDeleted,
...addedDiffZones,
]
const newSortedDiffIds = this._computeSortedDiffs(newSortedDiffZoneIds)
const isStreaming = this._isAnyDiffZoneStreaming(currentDiffZones)
this._setState(uri, {
sortedDiffZoneIds: newSortedDiffZoneIds,
sortedDiffIds: newSortedDiffIds,
isStreaming: isStreaming
})
this._onDidChangeState.fire({ uri })
}
}))
this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => {
for (const uri of this._hooks) {
if (e.uri.fsPath !== uri.fsPath) continue
// --- sortedURIs: no change
// --- state:
// sortedDiffIds gets a change to it, so gets recomputed
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
const { sortedDiffZoneIds } = currState
const newSortedDiffIds = this._computeSortedDiffs(sortedDiffZoneIds)
this._setState(uri, {
sortedDiffIds: newSortedDiffIds,
// sortedDiffZoneIds, // no change
// isStreaming, // no change
})
this._onDidChangeState.fire({ uri })
}
}))
this._register(this._editCodeService.onDidChangeStreamingInDiffZone(e => {
for (const uri of this._hooks) {
if (e.uri.fsPath !== uri.fsPath) continue
// --- sortedURIs: no change
// --- state:
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
const { sortedDiffZoneIds } = currState
this._setState(uri, {
isStreaming: this._isAnyDiffZoneStreaming(sortedDiffZoneIds),
// sortedDiffIds, // no change
// sortedDiffZoneIds, // no change
})
this._onDidChangeState.fire({ uri })
}
}))
}
setDiffIdx(uri: URI, newIdx: number | null): void {
this._setState(uri, { diffIdx: newIdx });
this._onDidChangeState.fire({ uri });
}
getStreamState(uri: URI) {
const { isStreaming, sortedDiffZoneIds } = this.stateOfURI[uri.fsPath] ?? {}
if (isStreaming) {
return 'streaming'
}
if ((sortedDiffZoneIds?.length ?? 0) > 0) {
return 'idle-has-changes'
}
return 'idle-no-changes'
}
_computeSortedDiffs(diffareaids: string[]) {
const sortedDiffIds = [];
for (const diffareaid of diffareaids) {
const diffZone = this._editCodeService.diffAreaOfId[diffareaid];
if (!diffZone || diffZone.type !== 'DiffZone') {
continue;
}
// Add all diff ids from this diffzone
const diffIds = Object.keys(diffZone._diffOfId);
sortedDiffIds.push(...diffIds);
}
return sortedDiffIds;
}
_getDiffZoneChanges(currentDiffZones: Iterable<string>, oldDiffZones: Iterable<string>) {
// Find the added or deleted diffZones by comparing diffareaids
const addedDiffZoneIds = new Set<string>();
const deletedDiffZoneIds = new Set<string>();
// Convert the current diffZones to a set of ids for easy lookup
const currentDiffZoneIdSet = new Set(currentDiffZones);
// Find deleted diffZones (in old but not in current)
for (const oldDiffZoneId of oldDiffZones) {
if (!currentDiffZoneIdSet.has(oldDiffZoneId)) {
const diffZone = this._editCodeService.diffAreaOfId[oldDiffZoneId];
if (diffZone && diffZone.type === 'DiffZone') {
deletedDiffZoneIds.add(oldDiffZoneId);
}
}
}
// Find added diffZones (in current but not in old)
const oldDiffZoneIdSet = new Set(oldDiffZones);
for (const currentDiffZoneId of currentDiffZones) {
if (!oldDiffZoneIdSet.has(currentDiffZoneId)) {
const diffZone = this._editCodeService.diffAreaOfId[currentDiffZoneId];
if (diffZone && diffZone.type === 'DiffZone') {
addedDiffZoneIds.add(currentDiffZoneId);
}
}
}
return { addedDiffZones: addedDiffZoneIds, deletedDiffZones: deletedDiffZoneIds }
}
_isAnyDiffZoneStreaming(diffareaids: Iterable<string>) {
for (const diffareaid of diffareaids) {
const diffZone = this._editCodeService.diffAreaOfId[diffareaid];
if (!diffZone || diffZone.type !== 'DiffZone') {
continue;
}
if (diffZone._streamState.isStreaming) {
return true;
}
}
return false
}
_setState(uri: URI, opts: Partial<CommandBarStateType>) {
const newState = {
...this.stateOfURI[uri.fsPath] ?? deepClone(defaultState),
...opts
}
// make sure diffIdx is always correct
if (newState.diffIdx && newState.diffIdx > newState.sortedDiffIds.length) {
newState.diffIdx = newState.sortedDiffIds.length
if (newState.diffIdx < 0) newState.diffIdx = null
}
this.stateOfURI = {
...this.stateOfURI,
[uri.fsPath]: newState
}
}
_addURIEntryToState(uri: URI) {
// add to sortedURIs
this.sortedURIs = [
...this.sortedURIs,
uri
]
// add to state
this.stateOfURI[uri.fsPath] = deepClone(defaultState)
}
_deleteURIEntryFromState(uri: URI) {
// delete this from sortedURIs
const i = this.sortedURIs.findIndex(uri2 => uri2.fsPath === uri.fsPath)
if (i === -1) return
this.sortedURIs = [
...this.sortedURIs.slice(0, i),
...this.sortedURIs.slice(i, Infinity),
]
// delete from state
delete this.stateOfURI[uri.fsPath]
}
private _getDiffZonesOnURI(uri: URI) {
const diffZones = [...this._editCodeService.diffAreasOfURI[uri.fsPath]?.values() ?? []]
.map(diffareaid => this._editCodeService.diffAreaOfId[diffareaid])
.filter(diffArea => !!diffArea && diffArea.type === 'DiffZone')
return diffZones
}
}
registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationType.Delayed); // delayed is needed here :(
// registerWorkbenchContribution2(VoidCommandBarService.ID, VoidCommandBarService, WorkbenchPhase.BlockRestore);
class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
private readonly _domNode: HTMLElement;
private readonly editor: ICodeEditor;
private readonly ID: string;
constructor({ editor }: { editor: ICodeEditor, },
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.ID = editor.getId() + '-voidfloatingwidget';
this.editor = editor;
// Create container div
const { root } = dom.h('div@root');
// Style the container
root.style.zIndex = '2';
root.style.padding = '4px';
root.style.alignItems = 'center';
root.style.pointerEvents = 'none';
// Mount command bar using mountVoidCommandBar
this.instantiationService.invokeFunction(accessor => {
const uri = editor.getModel()?.uri || null
const res = mountVoidCommandBar(root, accessor, { uri, editor })
if (!res) return
const dispose = res.dispose
const rerender: (o: { uri: URI | null }) => void = res.rerender
this._register(toDisposable(() => dispose?.()))
this._register(editor.onDidChangeModel((model) => {
const uri = model.newModelUrl
rerender({ uri })
}))
});
this._domNode = root;
// Mount the widget
editor.addOverlayWidget(this);
}
public getId(): string {
return this.ID;
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getPosition() {
return {
preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER,
}
}
public override dispose(): void {
this.editor.removeOverlayWidget(this);
super.dispose();
}
}

View file

@ -25,7 +25,7 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { mountVoidSettings } from './react/out/void-settings-tsx/index.js'
import { Codicon } from '../../../../base/common/codicons.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { toDisposable } from '../../../../base/common/lifecycle.js';
// refer to preferences.contribution.ts keybindings editor
@ -90,12 +90,12 @@ class VoidSettingsPane extends EditorPane {
// Mount React into the scrollable content
this.instantiationService.invokeFunction(accessor => {
const disposables: IDisposable[] | undefined = mountVoidSettings(settingsElt, accessor);
const disposeFn = mountVoidSettings(settingsElt, accessor)?.dispose;
this._register(toDisposable(() => disposeFn?.()))
// setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
// this._scrollbar?.scanDomNode();
// }, 1000)
disposables?.forEach(d => this._register(d));
});
}

View file

@ -1,57 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
// service that manages state
export type VoidUriState = {
currentUri?: URI
}
export interface IVoidUriStateService {
readonly _serviceBrand: undefined;
readonly state: VoidUriState; // readonly to the user
setState(newState: Partial<VoidUriState>): void;
onDidChangeState: Event<void>;
}
export const IVoidUriStateService = createDecorator<IVoidUriStateService>('voidUriStateService');
class VoidUriStateService extends Disposable implements IVoidUriStateService {
_serviceBrand: undefined;
static readonly ID = 'voidUriStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
// state
state: VoidUriState
constructor(
) {
super()
// initial state
this.state = { currentUri: undefined }
}
setState(newState: Partial<VoidUriState>) {
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
}
registerSingleton(IVoidUriStateService, VoidUriStateService, InstantiationType.Eager);

View file

@ -0,0 +1,8 @@
export const acceptBg = '#1a7431'
export const acceptAllBg = '#1e8538'
export const acceptBorder = '1px solid #145626'
export const rejectBg = '#b42331'
export const rejectAllBg = '#cf2838'
export const rejectBorder = '1px solid #8e1c27'
export const buttonFontSize = '11px'
export const buttonTextColor = 'white'