diff --git a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts b/src/vs/workbench/contrib/void/browser/_markerCheckService.ts similarity index 100% rename from src/vs/workbench/contrib/void/browser/MarkerCheckService.ts rename to src/vs/workbench/contrib/void/browser/_markerCheckService.ts diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 799d065e..7d3b20f9 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -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 | undefined> = {}; // uri -> diffareaId diffAreaOfId: Record = {}; // diffareaId -> diffArea diffOfId: Record = {}; // 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 = 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() + const registeredModelURIs = new Set() 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 = {} @@ -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((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((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 = { - // 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(); - } -} - - - diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 1acae2c7..2e0239e4 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -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('editCodeService'); @@ -40,32 +40,28 @@ export const IEditCodeService = createDecorator('editCodeServi export interface IEditCodeService { readonly _serviceBrand: undefined; - // main entrypoints (initialize things for the functions below to be called): startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null>; - _sortedUrisWithDiffs: URI[]; - _sortedDiffsOfFspath: { [fsPath: string]: Diff[] | undefined }; + addCtrlKZone(opts: AddCtrlKOpts): number | undefined; + removeCtrlKZone(opts: { diffareaid: number }): void; diffAreaOfId: Record; + diffAreasOfURI: Record | undefined>; diffOfId: Record; - - 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; } diff --git a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts index 3165e57f..ba906ff5 100644 --- a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts +++ b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts @@ -25,7 +25,7 @@ export interface IConsistentItemService { export const IConsistentItemService = createDecorator('ConsistentItemService'); -export class ConsistentItemService extends Disposable { +export class ConsistentItemService extends Disposable implements IConsistentItemService { readonly _serviceBrand: undefined diff --git a/src/vs/workbench/contrib/void/browser/metricsPollService.ts b/src/vs/workbench/contrib/void/browser/metricsPollService.ts index 92bbd16c..037aa1d6 100644 --- a/src/vs/workbench/contrib/void/browser/metricsPollService.ts +++ b/src/vs/workbench/contrib/void/browser/metricsPollService.ts @@ -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) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 62a20997..a4fe34bd 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -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 = <> {copyButton} @@ -258,7 +267,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri } - if (currStreamState === 'acceptRejectAll') { + if (currStreamState === 'idle-has-changes') { buttonsHTML = <> {reapplyButton} @@ -270,9 +279,9 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const statusIndicatorHTML =
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 }) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 0443e5c5..205ef3a9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -66,6 +66,7 @@ export const QuickEditChat = ({ editCodeService.startApplying({ from: 'QuickEdit', diffareaid, + startBehavior: 'keep-conflicts', }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 671e4571..c913f6f6 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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([]) 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([]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx index 5eba467d..6114c8ff 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx @@ -19,7 +19,20 @@ export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => const disposables = _registerServices(accessor) const root = ReactDOM.createRoot(rootElement) - root.render(); // tailwind dark theme indicator - return disposables + const rerender = (props?: any) => { + root.render(); // tailwind dark theme indicator + } + const dispose = () => { + root.unmount(); + disposables.forEach(d => d.dispose()); + } + + rerender(props) + + const returnVal = { + rerender, + dispose, + } + return returnVal } diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 08a52acf..95060cb2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -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 } +} + + + diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 6783be26..e97aa26a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -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
- +
} -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>({}) - // 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(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
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 =
setIsFocused(true)} + onBlur={() => setIsFocused(false)} >
-
File {(editCodeService._sortedUrisWithDiffs.findIndex(u => u.fsPath === currentUri?.fsPath) ?? 0) + 1} of {editCodeService._sortedUrisWithDiffs.length}
-
Diff {(diffIdxOfFspath[currentUri?.fsPath ?? ''] ?? 0) + 1} of {editCodeService._sortedDiffsOfFspath[currentUri?.fsPath ?? '']?.length ?? 0}
+
+ File {(currUriIdx ?? 0) + 1} of {sortedCommandBarURIs.length} +
+
+ Diff {(currDiffIdx ?? 0) + 1} of {sortedDiffIds?.length ?? 0} +
+ + + 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 &&
+ + +
+ + + return <> + {navPanel} + {acceptRejectButtons} + } diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 16a3be18..a6da7283 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -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); diff --git a/src/vs/workbench/contrib/void/browser/sidebarPane.ts b/src/vs/workbench/contrib/void/browser/sidebarPane.ts index 9b5fff8e..1fe2e88a 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarPane.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarPane.ts @@ -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?.())) }); } diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index ba7a5201..030085ca 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -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 {} diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 630cccf2..9054450b 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -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) ---------- diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts new file mode 100644 index 00000000..9fee88d8 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -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('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 = { + 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() // 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() + 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, oldDiffZones: Iterable) { + // Find the added or deleted diffZones by comparing diffareaids + const addedDiffZoneIds = new Set(); + const deletedDiffZoneIds = new Set(); + + // 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) { + 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) { + 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(); + } +} + + diff --git a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts index 71b37461..b70aef91 100644 --- a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts +++ b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts @@ -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)); }); } diff --git a/src/vs/workbench/contrib/void/browser/voidUriStateService.ts b/src/vs/workbench/contrib/void/browser/voidUriStateService.ts deleted file mode 100644 index 1a89c29b..00000000 --- a/src/vs/workbench/contrib/void/browser/voidUriStateService.ts +++ /dev/null @@ -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): void; - onDidChangeState: Event; -} - -export const IVoidUriStateService = createDecorator('voidUriStateService'); -class VoidUriStateService extends Disposable implements IVoidUriStateService { - _serviceBrand: undefined; - - static readonly ID = 'voidUriStateService'; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - - - // state - state: VoidUriState - - constructor( - ) { - super() - - // initial state - this.state = { currentUri: undefined } - } - - setState(newState: Partial) { - - this.state = { ...this.state, ...newState } - this._onDidChangeState.fire() - } - - -} - -registerSingleton(IVoidUriStateService, VoidUriStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/common/helpers/colors.ts b/src/vs/workbench/contrib/void/common/helpers/colors.ts new file mode 100644 index 00000000..1a20ea9b --- /dev/null +++ b/src/vs/workbench/contrib/void/common/helpers/colors.ts @@ -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'