diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 76b06723..04cef415 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -818,6 +818,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } } } finally { + modelRef.object.dispose(); modelRef.dispose(); } } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9bd03b56..f8e47af9 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -12,7 +12,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; // import { throttle } from '../../../../base/common/decorators.js'; import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; -import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; +import { IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; import { Color, RGBA } from '../../../../base/common/color.js'; @@ -45,6 +45,9 @@ import { IVoidFileService } from '../common/voidFileService.js'; import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { FeatureName } from '../common/voidSettingsTypes.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +// import { IFileService } from '../../../../platform/files/common/files.js'; +// import { VSBuffer } from '../../../../base/common/buffer.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -67,7 +70,7 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); - +const numLinesOfStr = (str: string) => str.split('\n').length const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { @@ -118,7 +121,7 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = text.split('\n').length + const numLines = numLinesOfStr(text) const endLine = startLine + numLines - 1 return [startLine, endLine] as const } @@ -237,7 +240,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // URI <--> model - diffAreasOfURI: Record> = {} + diffAreasOfURI: Record | undefined> = {} diffAreaOfId: Record = {}; diffOfId: Record = {}; // redundant with diffArea._diffs @@ -259,7 +262,7 @@ class EditCodeService extends Disposable implements IEditCodeService { constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right - @ICodeEditorService private readonly _editorService: ICodeEditorService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IModelService private readonly _modelService: IModelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @@ -271,15 +274,22 @@ class EditCodeService extends Disposable implements IEditCodeService { @ICommandService private readonly _commandService: ICommandService, @IVoidFileService private readonly _voidFileService: IVoidFileService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, + // @IFileService private readonly _fileService: IFileService, + @ILanguageService private readonly _languageService: ILanguageService, ) { super(); // this function initializes data structures and listens for changes + const registeredModelListeners = new Set() const initializeModel = (model: ITextModel) => { + + // 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 (!(model.uri.fsPath in this.diffAreasOfURI)) { this.diffAreasOfURI[model.uri.fsPath] = new Set(); } - else return // do not add listeners to the same model twice - important, or will see duplicates // when the user types, realign diff areas and re-render them this._register( @@ -292,7 +302,7 @@ class EditCodeService extends Disposable implements IEditCodeService { ) // when a stream starts or ends, fire the event for onDidChangeURIStreamState - let prevStreamState = this.getURIStreamState({ uri: model.uri }) + let prevStreamState = this.getURIStreamState({ uri: null }) const updateAcceptRejectAllUI = () => { const state = this.getURIStreamState({ uri: model.uri }) let prevStateActual = prevStreamState @@ -316,11 +326,12 @@ class EditCodeService extends Disposable implements IEditCodeService { 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) } // initialize all existing models + initialize when a new model mounts for (let model of this._modelService.getModels()) { initializeModel(model) } - this._register(this._modelService.onModelAdded(model => initializeModel(model))); + this._register(this._modelService.onModelAdded(model => { console.log('initialized model!!', model.uri.fsPath); initializeModel(model) })); // this function adds listeners to refresh styles when editor changes tab @@ -328,9 +339,10 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = editor.getModel()?.uri ?? null if (uri) this._refreshStylesAndDiffsInURI(uri) } + // add listeners for all existing editors + listen for editor being added - for (let editor of this._editorService.listCodeEditors()) { initializeEditor(editor) } - this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) + for (let editor of this._codeEditorService.listCodeEditors()) { initializeEditor(editor) } + this._register(this._codeEditorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) } @@ -342,16 +354,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._refreshStylesAndDiffsInURI(uri) } - private _onInternalChangeContent(uri: URI, { shouldRealign }: { shouldRealign: false | { newText: string, oldRange: IRange } }) { - if (shouldRealign) { - const { newText, oldRange } = shouldRealign - // console.log('realiging', newText, oldRange) - this._realignAllDiffAreasLines(uri, newText, oldRange) - } - this._refreshStylesAndDiffsInURI(uri) - - } - @@ -424,7 +426,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _computeDiffsAndAddStylesToURI = (uri: URI) => { - const fullFileText = this._readURI(uri) ?? '' + const fullFileText = this._voidFileService.readModel(uri) ?? '' for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] @@ -485,7 +487,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _addCtrlKZoneInput = (ctrlKZone: CtrlKZone) => { const { editorId } = ctrlKZone - const editor = this._editorService.listCodeEditors().find(e => e.getId() === editorId) + const editor = this._codeEditorService.listCodeEditors().find(e => e.getId() === editorId) if (!editor) { return null } let zoneId: string | null = null @@ -587,7 +589,8 @@ class EditCodeService extends Disposable implements IEditCodeService { const disposeInThisEditorFns: (() => void)[] = [] - const model = this._modelService.getModel(uri) + const model = this._getModel(uri) + if (model === null) return // green decoration and minimap decoration if (type !== 'deletion') { @@ -724,6 +727,18 @@ class EditCodeService extends Disposable implements IEditCodeService { + + + + // private _readURI(uri: URI, range?: IRange): string | null { + // if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null + // else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null + // } + // private _getNumLines(uri: URI): number | null { + // return this._getModel(uri)?.getLineCount() ?? null + // } + + private _getModel(uri: URI) { const model = this._modelService.getModel(uri) if (!model || model.isDisposed()) { @@ -731,15 +746,22 @@ class EditCodeService extends Disposable implements IEditCodeService { } return model } - private _readURI(uri: URI, range?: IRange): string | null { - if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null - else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null - } - private _getNumLines(uri: URI): number | null { - return this._getModel(uri)?.getLineCount() ?? null + // not obvious at all, but if we want the model we can just create it. our listeners in the constructor handle it + private async _forceModel(uri: URI) { + + const m = this._getModel(uri) + if (m !== null) return m + + const fileStr = await this._voidFileService.readFile(uri) + if (fileStr === null) return null + const lang = this._languageService.createByFilepathOrFirstLine(uri, fileStr?.split('\n')?.[0]) + const model = this._modelService.createModel(fileStr, lang, uri); + return model } + + private _getActiveEditorURI(): URI | null { - const editor = this._editorService.getActiveCodeEditor() + const editor = this._codeEditorService.getActiveCodeEditor() if (!editor) return null const uri = editor.getModel()?.uri if (!uri) return null @@ -747,38 +769,44 @@ class EditCodeService extends Disposable implements IEditCodeService { } weAreWriting = false - private _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { - const model = this._getModel(uri) - if (!model) return - const uriStr = this._readURI(uri, range) - if (uriStr === null) return + private async _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) { + let m = this._getModel(uri) + if (m === null) { m = await this._forceModel(uri) } + if (m === null) return // if still null, return + const model: ITextModel = m + const range: IRange = range_ === 'wholeFileRange' ? + { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER } // whole file + : range_ - // heuristic check if don't need to make edits + // realign is 100% independent from written text (diffareas are nonphysical), can do this first + if (shouldRealignDiffAreas) { + const newText = text + const oldRange = range + this._realignAllDiffAreasLines(uri, newText, oldRange) + } + + const uriStr = model.getValue() + + // heuristic check const dontNeedToWrite = uriStr === text if (dontNeedToWrite) { - // at the end of a write, we still expect to refresh all styles - // e.g. sometimes we expect to restore all the decorations even if no edits were made when _writeText is used - this._refreshStylesAndDiffsInURI(uri) + this._refreshStylesAndDiffsInURI(uri) // at the end of a write, we still expect to refresh all styles. e.g. sometimes we expect to restore all the decorations even if no edits were made when _writeText is used return } - // minimal edits so not so flashy - // const edits = this.worker.$Void_computeMoreMinimalEdits(uri.toString(), [{ range, text }], false) this.weAreWriting = true model.applyEdits([{ range, text }]) this.weAreWriting = false - this._onInternalChangeContent(uri, { shouldRealign: shouldRealignDiffAreas && { newText: text, oldRange: range } }) - + this._refreshStylesAndDiffsInURI(uri) } - private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) { - const getCurrentSnapshot = (): HistorySnapshot => { + const getCurrentSnapshot = async (): Promise => { const snapshottedDiffAreaOfId: Record = {} for (const diffareaid in this.diffAreaOfId) { @@ -790,13 +818,16 @@ class EditCodeService extends Disposable implements IEditCodeService { Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]])) ) as DiffAreaSnapshot } + + const entireFileCode = await this._voidFileService.readFile(uri) ?? '' return { snapshottedDiffAreaOfId, - entireFileCode: this._readURI(uri) ?? '', // the whole file's code + entireFileCode, // the whole file's code } } - const restoreDiffAreas = (snapshot: HistorySnapshot) => { + const restoreDiffAreas = async (snapshotPromise: Promise) => { + const snapshot = await snapshotPromise // for each diffarea in this uri, stop streaming if currently streaming for (const diffareaid in this.diffAreaOfId) { @@ -807,7 +838,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // delete all diffareas on this uri (clearing their styles) this._deleteAllDiffAreas(uri) - this.diffAreasOfURI[uri.fsPath].clear() + this.diffAreasOfURI[uri.fsPath]?.clear() const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = structuredClone(snapshot) // don't want to destroy the snapshot @@ -835,23 +866,19 @@ class EditCodeService extends Disposable implements IEditCodeService { _linkedStreamingDiffZone: null, // when restoring, we will never be streaming } } - this.diffAreasOfURI[uri.fsPath].add(diffareaid) + this._addOrInitializeDiffAreaAtURI(uri, diffareaid) } this._onDidAddOrDeleteDiffZones.fire({ uri }) // restore file content - const numLines = this._getNumLines(uri) - if (numLines === null) return - - - this._writeText(uri, entireModelCode, - { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, + this._writeURIText(uri, entireModelCode, + 'wholeFileRange', { shouldRealignDiffAreas: false } ) } - const beforeSnapshot: HistorySnapshot = getCurrentSnapshot() - let afterSnapshot: HistorySnapshot | null = null + const beforeSnapshot: Promise = getCurrentSnapshot() + let afterSnapshot: Promise | null = null const elt: IUndoRedoElement = { type: UndoRedoElementType.Resource, @@ -863,7 +890,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._undoRedoService.pushElement(elt) - const onFinishEdit = () => { afterSnapshot = getCurrentSnapshot() } + const onFinishEdit = async () => { afterSnapshot = getCurrentSnapshot() } return { onFinishEdit } } @@ -906,26 +933,26 @@ class EditCodeService extends Disposable implements IEditCodeService { private _deleteDiffZone(diffZone: DiffZone) { this._clearAllDiffAreaEffects(diffZone) delete this.diffAreaOfId[diffZone.diffareaid] - this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString()) + this.diffAreasOfURI[diffZone._URI.fsPath]?.delete(diffZone.diffareaid.toString()) this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) } private _deleteTrackingZone(trackingZone: TrackingZone) { delete this.diffAreaOfId[trackingZone.diffareaid] - this.diffAreasOfURI[trackingZone._URI.fsPath].delete(trackingZone.diffareaid.toString()) + this.diffAreasOfURI[trackingZone._URI.fsPath]?.delete(trackingZone.diffareaid.toString()) } private _deleteCtrlKZone(ctrlKZone: CtrlKZone) { this._clearAllEffects(ctrlKZone._URI) ctrlKZone._mountInfo?.dispose() delete this.diffAreaOfId[ctrlKZone.diffareaid] - this.diffAreasOfURI[ctrlKZone._URI.fsPath].delete(ctrlKZone.diffareaid.toString()) + this.diffAreasOfURI[ctrlKZone._URI.fsPath]?.delete(ctrlKZone.diffareaid.toString()) } private _deleteAllDiffAreas(uri: URI) { const diffAreas = this.diffAreasOfURI[uri.fsPath] - diffAreas.forEach(diffareaid => { + diffAreas?.forEach(diffareaid => { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type === 'DiffZone') this._deleteDiffZone(diffArea) @@ -934,13 +961,16 @@ class EditCodeService extends Disposable implements IEditCodeService { }) } - + private _addOrInitializeDiffAreaAtURI = (uri: URI, diffareaid: string | number) => { + if (!(uri.fsPath in this.diffAreasOfURI)) this.diffAreasOfURI[uri.fsPath] = new Set() + this.diffAreasOfURI[uri.fsPath]?.add(diffareaid.toString()) + } private _diffareaidPool = 0 // each diffarea has an id private _addDiffArea(diffArea: Omit): T { const diffareaid = this._diffareaidPool++ const diffArea2 = { ...diffArea, diffareaid } as T - this.diffAreasOfURI[diffArea2._URI.fsPath].add(diffareaid.toString()) + this._addOrInitializeDiffAreaAtURI(diffArea._URI, diffareaid) this.diffAreaOfId[diffareaid] = diffArea2 return diffArea2 } @@ -958,7 +988,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } const fn = this._addDiffStylesToURI(uri, newDiff) - diffZone._removeStylesFns.add(fn) + if (fn) diffZone._removeStylesFns.add(fn) this.diffOfId[diffid] = newDiff diffZone._diffOfId[diffid] = newDiff @@ -974,9 +1004,6 @@ class EditCodeService extends Disposable implements IEditCodeService { // console.log('recent change', recentChange) - const model = this._getModel(uri) - if (!model) return - // compute net number of newlines lines that were added/removed const startLine = recentChange.startLineNumber const endLine = recentChange.endLineNumber @@ -984,7 +1011,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const newTextHeight = (text.match(/\n/g) || []).length + 1 // number of newlines is number of \n's + 1, e.g. "ab\ncd" // compute overlap with each diffArea and shrink/elongate each diffArea accordingly - for (const diffareaid of this.diffAreasOfURI[model.uri.fsPath] || []) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] // if the diffArea is entirely above the range, it is not affected @@ -1085,7 +1112,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // at the start, add a newline between the stream and originalCode to make reasoning easier if (!latestMutable.addedSplitYet) { - this._writeText(uri, '\n', + this._writeURIText(uri, '\n', { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col, }, { shouldRealignDiffAreas: true } ) @@ -1094,7 +1121,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // insert deltaText at latest line and col - this._writeText(uri, deltaText, + this._writeURIText(uri, deltaText, { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) @@ -1108,7 +1135,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (latestMutable.originalCodeStartLine < startLineInOriginalCode) { // moved up, delete const numLinesDeleted = startLineInOriginalCode - latestMutable.originalCodeStartLine - this._writeText(uri, '', + this._writeURIText(uri, '', { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, { shouldRealignDiffAreas: true } ) @@ -1116,7 +1143,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } else if (latestMutable.originalCodeStartLine > startLineInOriginalCode) { const newText = '\n' + originalCode.split('\n').slice((startLineInOriginalCode - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n') - this._writeText(uri, newText, + this._writeURIText(uri, newText, { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) @@ -1189,14 +1216,21 @@ class EditCodeService extends Disposable implements IEditCodeService { - // throws if there's an error - public startApplying(opts: StartApplyingOpts): [URI, Promise] | null { + // the applyDonePromise this returns can throw an error (reject) + public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null> { let res: [DiffZone, Promise] | undefined = undefined - if (opts.type === 'rewrite') res = this._initializeWriteoverStream(opts) - else if (opts.type === 'searchReplace') res = this._initializeSearchAndReplaceStream(opts) + if (opts.type === 'rewrite') res = await this._initializeWriteoverStream(opts) + else if (opts.type === 'searchReplace') res = await this._initializeSearchAndReplaceStream(opts) if (!res) return null const [diffZone, applyDonePromise] = res + applyDonePromise.then(() => { + const diffareaids = this.diffAreasOfURI[diffZone._URI.fsPath] + for (const diffareaid of diffareaids || []) { + const diffArea = this.diffAreaOfId[diffareaid] + console.log('DA!!', diffArea) + } + }) return [diffZone._URI, applyDonePromise] } @@ -1222,29 +1256,33 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise] | undefined { + private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise] | undefined> { const { from, } = opts let startLine: number let endLine: number let uri: URI + let currentFileStr: string if (from === 'ClickApply') { const uri_ = this._getActiveEditorURI() if (!uri_) return uri = uri_ + const c_ = await this._voidFileService.readFile(uri) + if (c_ === null) return + currentFileStr = c_ - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + const numLines = numLinesOfStr(currentFileStr) + + // remove all diffZones on this URI, adding to history (there can't possibly be overlap after this) + const behavior: 'accept' | 'reject' = opts.startBehavior === 'accept-conflicts' ? 'accept' : 'reject' + this.removeDiffAreas({ uri, behavior, removeCtrlKs: true }) // in ctrl+L the start and end lines are the full document - const numLines = this._getNumLines(uri) - if (numLines === null) return startLine = 1 endLine = numLines - } else if (from === 'QuickEdit') { const { diffareaid } = opts @@ -1253,6 +1291,11 @@ class EditCodeService extends Disposable implements IEditCodeService { const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone uri = _URI + + const c_ = await this._voidFileService.readFile(uri) + if (c_ === null) return + currentFileStr = c_ + startLine = startLine_ endLine = endLine_ } @@ -1260,14 +1303,9 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error(`Void: diff.type not recognized on: ${from}`) } - const currentFileStr = this._readURI(uri) - if (currentFileStr === null) return const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - - let streamRequestIdRef: { current: string | null } = { current: null } - // promise that resolves when the apply is done let resApplyPromise: () => void let rejApplyPromise: (e: any) => void @@ -1408,10 +1446,11 @@ class EditCodeService extends Disposable implements IEditCodeService { // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) // at the end, re-write whole thing to make sure no sync errors const [croppedText, _1, _2] = extractText(fullText, 0) - this._writeText(uri, croppedText, + this._writeURIText(uri, croppedText, { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) + this._voidFileService.saveOrWriteFileAssumingModelExists(uri) onDone() resMessageDonePromise() }, @@ -1434,7 +1473,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise] | undefined { + private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise] | undefined> { const { from, applyStr, uri: givenURI, } = opts let uri: URI @@ -1447,13 +1486,11 @@ class EditCodeService extends Disposable implements IEditCodeService { uri = givenURI } - // generate search/replace block text - const originalFileCode = this._voidFileService.readModel(uri) + const originalFileCode = await this._voidFileService.readFile(uri) if (originalFileCode === null) return - const numLines = this._getNumLines(uri) - if (numLines === null) return + const numLines = numLinesOfStr(originalFileCode) // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) @@ -1507,6 +1544,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) + console.log('b', uri) const convertOriginalRangeToFinalRange = (originalRange: readonly [number, number]): [number, number] => { @@ -1608,9 +1646,8 @@ class EditCodeService extends Disposable implements IEditCodeService { } } else { - // TODO!!! test this // starting line is at least the number of lines in the generated code minus 1 - const numLinesInOrig = block.orig.split('\n').length - 1 + const numLinesInOrig = numLinesOfStr(block.orig) diffZone._streamState.line = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1) } // must be done writing original to move on to writing streamed content @@ -1670,12 +1707,11 @@ class EditCodeService extends Disposable implements IEditCodeService { console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') continue } - if (!latestStreamLocationMutable) continue // if a block is done, finish it by writing all if (block.state === 'done') { const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum] - this._writeText(uri, block.final, + this._writeURIText(uri, block.final, { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) @@ -1685,6 +1721,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // write the added text to the file + if (!latestStreamLocationMutable) continue const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) oldBlocks = blocks // oldblocks is only used if writingFinal @@ -1726,14 +1763,13 @@ class EditCodeService extends Disposable implements IEditCodeService { ...lines.slice((originalEnd - 1) + 1, Infinity) ].join('\n') } - const numLines = this._getNumLines(uri) - if (numLines !== null) { - this._writeText(uri, newCode, - { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, - { shouldRealignDiffAreas: true } - ) - } + this._writeURIText(uri, newCode, + 'wholeFileRange', + { shouldRealignDiffAreas: true } + ) + + this._voidFileService.saveOrWriteFileAssumingModelExists(uri) onDone() resMessageDonePromise() }, @@ -1816,7 +1852,7 @@ class EditCodeService extends Disposable implements IEditCodeService { getURIStreamState = ({ uri }: { uri: URI | null }) => { if (uri === null) return 'idle' - const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] + const diffZones = [...this.diffAreasOfURI[uri.fsPath]?.values() ?? []] .map(diffareaid => this.diffAreaOfId[diffareaid]) .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) @@ -1853,7 +1889,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const writeText = diffZone.originalCode const toRange: IRange = { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER } - this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) + this._writeURIText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) this._deleteDiffZone(diffZone) } @@ -1863,11 +1899,11 @@ class EditCodeService extends Disposable implements IEditCodeService { public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { const diffareaids = this.diffAreasOfURI[uri.fsPath] - if (diffareaids.size === 0) return // do nothing + if ((diffareaids?.size ?? 0) === 0) return // do nothing const { onFinishEdit } = this._addToHistory(uri) - for (const diffareaid of diffareaids) { + for (const diffareaid of diffareaids ?? []) { const diffArea = this.diffAreaOfId[diffareaid] if (!diffArea) continue @@ -2024,7 +2060,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // update the file - this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) + this._writeURIText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) // originalCode does not change! diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 0ffbf047..99e0c937 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -20,6 +20,7 @@ export type StartApplyingOpts = ({ type: 'searchReplace' | 'rewrite'; applyStr: string; uri: 'current' | URI; + startBehavior: 'accept-conflicts' | 'reject-conflicts'; }) @@ -37,7 +38,7 @@ export const IEditCodeService = createDecorator('editCodeServi export interface IEditCodeService { readonly _serviceBrand: undefined; - startApplying(opts: StartApplyingOpts): [URI, Promise] | null; + startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null>; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts deleted file mode 100644 index f7afa5dd..00000000 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ /dev/null @@ -1,52 +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 { URI } from '../../../../../base/common/uri.js' -import { EndOfLinePreference } from '../../../../../editor/common/model.js' -import { IModelService } from '../../../../../editor/common/services/model.js' -import { IFileService } from '../../../../../platform/files/common/files.js' - - -// attempts to read URI of currently opened model, then of raw file -export const VSReadFile = async (uri: URI, modelService: IModelService, fileService: IFileService) => { - - const modelResult = await _VSReadModel(modelService, uri) - if (modelResult) return modelResult - - const fileResult = await _VSReadFileRaw(fileService, uri) - if (fileResult) return fileResult - - return '' - -} - -// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.) -const _VSReadModel = async (modelService: IModelService, uri: URI): Promise => { - - // attempt to read saved model (doesn't work if application was reloaded...) - const model = modelService.getModel(uri) - if (model) { - return model.getValue(EndOfLinePreference.LF) - } - - // backup logic - look at all opened models and check if they have the same `fsPath` - const models = modelService.getModels() - for (const model of models) { - if (model.uri.fsPath === uri.fsPath) - return model.getValue(EndOfLinePreference.LF); - } - - return null -} - -const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => { - try { - const res = await fileService.readFile(uri) - const str = res.value.toString() - return str - } catch (e) { - return null - } -} 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 d44dfcf7..a0e0ec4d 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 @@ -118,16 +118,18 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a }, [applyBoxId, editCodeService, getUriBeingApplied]) ) - const onSubmit = useCallback(() => { + const onClickSubmit = useCallback(async () => { if (isDisabled) return if (getStreamState() === 'streaming') return - const [newApplyingUri, _] = editCodeService.startApplying({ + const [newApplyingUri, _] = await editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, uri: 'current', + startBehavior: 'reject-conflicts', }) ?? [] applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined + rerender(c => c + 1) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only }, [isDisabled, getStreamState, editCodeService, codeStr, applyBoxId, metricsService]) @@ -154,8 +156,8 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a const onReapply = useCallback(() => { onReject() - onSubmit() - }, [onReject, onSubmit]) + onClickSubmit() + }, [onReject, onClickSubmit]) const currStreamState = getStreamState() @@ -166,7 +168,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a const playButton = ( ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index baa0620e..4eed66fe 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -315,9 +315,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options // inline code if (t.type === "codespan") { - console.log('isLinkDetectionEnabled', options.isLinkDetectionEnabled) if (options.isLinkDetectionEnabled && chatMessageLocation) { - return -
- {children || '(no results)'} -
+ {children || '(no results)'} } @@ -1382,8 +1381,14 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest - componentParams.children = - componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } + componentParams.children =
+
{ commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}> + {getBasename(params.uri.fsPath)} +
+ +
return }, @@ -1452,8 +1457,7 @@ const toolNameToComponent: { [T in ToolName]: { const { command } = toolMessage.result.params const { terminalId, resolveReason, result } = toolMessage.result.value - componentParams.children =
- + componentParams.children =
terminalToolsService.openTerminal(terminalId)} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index f4042a2e..bf752bc2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -883,8 +883,8 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars onCreateInstance={useCallback((editor: CodeEditorWidget) => { const model = modelOfEditorId[id] ?? modelService.createModel( - initValueRef.current + '\n', { - languageId: languageRef.current ? languageRef.current : 'typescript', + initValueRef.current, { + languageId: languageRef.current ? languageRef.current : 'plaintext', onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this }) modelRef.current = model diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index cf4b4c50..fb061bc1 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -272,7 +272,7 @@ export class ToolsService implements IToolsService { this.callTool = { read_file: async ({ uri, pageNumber }) => { const readFileContents = await voidFileService.readFile(uri) - + if (readFileContents === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) } const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate @@ -326,11 +326,12 @@ export class ToolsService implements IToolsService { }, edit: async ({ uri, changeDescription }) => { - const [_, applyDonePromise] = editCodeService.startApplying({ + const [_, applyDonePromise] = await editCodeService.startApplying({ // throws error if error uri, applyStr: changeDescription, from: 'ClickApply', type: 'searchReplace', + startBehavior: 'accept-conflicts', }) ?? [] await applyDonePromise return {} diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index c12752b1..2d956379 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -25,7 +25,7 @@ Do NOT output the whole file here if possible, and try to write as LITTLE as nee export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\ You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase'}. You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. -Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (do not be lazy)` : ''}. The user's query is never invalid. +Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (make all necessary changes, and do not be lazy)` : ''}. The user's query is never invalid. The user's system information is as follows: - ${os} diff --git a/src/vs/workbench/contrib/void/common/voidFileService.ts b/src/vs/workbench/contrib/void/common/voidFileService.ts index cebad454..c276d3c1 100644 --- a/src/vs/workbench/contrib/void/common/voidFileService.ts +++ b/src/vs/workbench/contrib/void/common/voidFileService.ts @@ -3,18 +3,22 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../base/common/buffer.js'; import { URI } from '../../../../base/common/uri.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; export interface IVoidFileService { readonly _serviceBrand: undefined; - readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise; + readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise; readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null; + + saveOrWriteFileAssumingModelExists(uri: URI): Promise; } export const IVoidFileService = createDecorator('VoidFileService'); @@ -26,11 +30,12 @@ export class VoidFileService implements IVoidFileService { constructor( @IModelService private readonly modelService: IModelService, @IFileService private readonly fileService: IFileService, + @IEditorService private readonly _editorService: IEditorService, ) { } - readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { // attempt to read the model const modelResult = this.readModel(uri, range); @@ -40,7 +45,7 @@ export class VoidFileService implements IVoidFileService { const fileResult = await this._readFileRaw(uri, range); if (fileResult) return fileResult; - return ''; + return null; } _readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { @@ -77,18 +82,33 @@ export class VoidFileService implements IVoidFileService { // if range, read it if (range) { - return model.getValueInRange({ - startLineNumber: range.startLineNumber, - endLineNumber: range.endLineNumber, - startColumn: 1, - endColumn: Number.MAX_VALUE - }, EndOfLinePreference.LF); + return model.getValueInRange({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, startColumn: 1, endColumn: Number.MAX_VALUE }, EndOfLinePreference.LF); } else { return model.getValue(EndOfLinePreference.LF) } } + + + saveOrWriteFileAssumingModelExists = async (uri: URI): Promise => { + + const editorsOpen = [...this._editorService.findEditors(uri)] + if (editorsOpen.length !== 0) { + this._editorService.save(editorsOpen) + } + else { + // write the file using the contents of the existing model + const fileStr = this.modelService.getModel(uri)?.getValue() + if (fileStr === undefined) { + console.error('model not found for uri', uri.fsPath) + return + } + const buffer = VSBuffer.fromString(fileStr) + await this.fileService.writeFile(uri, buffer); + } + } + } registerSingleton(IVoidFileService, VoidFileService, InstantiationType.Eager);