changes in the BG work!! + autosave

This commit is contained in:
Andrew Pareles 2025-03-13 16:54:29 -07:00
parent cd512402d9
commit edefe841d5
11 changed files with 204 additions and 193 deletions

View file

@ -818,6 +818,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
}
} finally {
modelRef.object.dispose();
modelRef.dispose();
}
}

View file

@ -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<string, Set<string>> = {}
diffAreasOfURI: Record<string, Set<string> | undefined> = {}
diffAreaOfId: Record<string, DiffArea> = {};
diffOfId: Record<string, Diff> = {}; // 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<string>()
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<HistorySnapshot> => {
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
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<HistorySnapshot>) => {
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<HistorySnapshot> = getCurrentSnapshot()
let afterSnapshot: Promise<HistorySnapshot> | 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<unknown>) {
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<T extends DiffArea>(diffArea: Omit<T, 'diffareaid'>): 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<void>] | null {
// the applyDonePromise this returns can throw an error (reject)
public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null> {
let res: [DiffZone, Promise<void>] | 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<void>] | undefined {
private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise<void>] | 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<void>] | undefined {
private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise<void>] | 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!

View file

@ -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<IEditCodeService>('editCodeServi
export interface IEditCodeService {
readonly _serviceBrand: undefined;
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null>;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;

View file

@ -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<string | null> => {
// 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
}
}

View file

@ -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 = (
<IconShell1
Icon={Play}
onClick={onSubmit}
onClick={onClickSubmit}
title="Apply changes"
/>
)

View file

@ -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 <CodespanWithLink
text={t.text}
rawText={t.raw}

View file

@ -728,11 +728,10 @@ const ToolHeaderComponent = ({
{/* children */}
{<div
// the py-1 here makes sure all elements in the container have py-2 total. this makes a nice animation effect during transition.
className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100 py-1' : 'max-h-0 opacity-0'}`}
className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100' : 'max-h-0 opacity-0'}
text-void-fg-4 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm`}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{children || '(no results)'}
</div>
{children || '(no results)'}
</div>}
</div>
</div>
@ -1382,8 +1381,14 @@ const toolNameToComponent: { [T in ToolName]: {
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, }
const { params } = toolRequest
componentParams.children = <ChatMarkdownRender string={params.changeDescription} chatMessageLocation={undefined} />
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.children = <div>
<div
className=''
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}>
{getBasename(params.uri.fsPath)}
</div>
<ChatMarkdownRender string={params.changeDescription} chatMessageLocation={undefined} />
</div>
return <ToolHeaderComponent {...componentParams} />
},
@ -1452,8 +1457,7 @@ const toolNameToComponent: { [T in ToolName]: {
const { command } = toolMessage.result.params
const { terminalId, resolveReason, result } = toolMessage.result.value
componentParams.children = <div className='font-mono whitespace-pre text-nowrap text-xs overflow-auto bg-void-bg-1'>
componentParams.children = <div className='font-mono whitespace-pre text-nowrap text-xs overflow-auto bg-void-bg-3'>
<div
className='cursor-pointer'
onClick={() => terminalToolsService.openTerminal(terminalId)}

View file

@ -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

View file

@ -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 {}

View file

@ -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}

View file

@ -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<string>;
readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null>;
readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null;
saveOrWriteFileAssumingModelExists(uri: URI): Promise<void>;
}
export const IVoidFileService = createDecorator<IVoidFileService>('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<string> => {
readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null> => {
// 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<string | null> => {
@ -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<void> => {
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);