diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 91489c78..537da98e 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -594,6 +594,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const errorMessage = this.errMsgs.rejected this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, }) + this._setStreamState(threadId, {}, 'set') } stopRunning(threadId: string) { const thread = this.state.allThreads[threadId] diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 01bb2b0c..fe7b962f 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -44,6 +44,8 @@ import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } fro 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'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -277,6 +279,7 @@ class EditCodeService extends Disposable implements IEditCodeService { @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, // @IFileService private readonly _fileService: IFileService, @IVoidModelService private readonly _voidModelService: IVoidModelService, + @ITextFileService private readonly _textFileService: ITextFileService, ) { super(); @@ -784,7 +787,10 @@ class EditCodeService extends Disposable implements IEditCodeService { weAreWriting = false private _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) { const { model } = this._voidModelService.getModel(uri) - if (!model) return // TODO!!!! make sure this works + if (!model) { + 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 + } const range: IRange = range_ === 'wholeFileRange' ? { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER } // whole file @@ -817,8 +823,8 @@ class EditCodeService extends Disposable implements IEditCodeService { private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) { - const getCurrentSnapshot = async (): Promise => { - await this._voidModelService.initializeModel(uri) + const getCurrentSnapshot = (): HistorySnapshot => { + const { model } = this._voidModelService.getModel(uri) const snapshottedDiffAreaOfId: Record = {} @@ -827,7 +833,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea._URI.fsPath !== uri.fsPath) continue - snapshottedDiffAreaOfId[diffareaid] = structuredClone( // a structured clone must be on a JSON object + snapshottedDiffAreaOfId[diffareaid] = deepClone( Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]])) ) as DiffAreaSnapshot } @@ -841,8 +847,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } } - const restoreDiffAreas = async (snapshotPromise: Promise) => { - const snapshot = await snapshotPromise + const restoreDiffAreas = async (snapshot: HistorySnapshot) => { // for each diffarea in this uri, stop streaming if currently streaming for (const diffareaid in this.diffAreaOfId) { @@ -855,7 +860,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._deleteAllDiffAreas(uri) this.diffAreasOfURI[uri.fsPath]?.clear() - const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = structuredClone(snapshot) // don't want to destroy the snapshot + const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot // restore diffAreaOfId and diffAreasOfModelId for (const diffareaid in snapshottedDiffAreaOfId) { @@ -885,7 +890,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._onDidAddOrDeleteDiffZones.fire({ uri }) - await this._voidModelService.initializeModel(uri) // restore file content this._writeURIText(uri, entireModelCode, 'wholeFileRange', @@ -894,8 +898,8 @@ class EditCodeService extends Disposable implements IEditCodeService { // this._noLongerNeedModelReference(uri) } - const beforeSnapshot: Promise = getCurrentSnapshot() - let afterSnapshot: Promise | null = null + const beforeSnapshot: HistorySnapshot = getCurrentSnapshot() + let afterSnapshot: HistorySnapshot | null = null const elt: IUndoRedoElement = { type: UndoRedoElementType.Resource, @@ -907,7 +911,12 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._undoRedoService.pushElement(elt) - const onFinishEdit = () => { afterSnapshot = getCurrentSnapshot() } + const onFinishEdit = async () => { + afterSnapshot = getCurrentSnapshot() + await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change. + skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack) + }) + } return { onFinishEdit } } @@ -1298,7 +1307,6 @@ class EditCodeService extends Disposable implements IEditCodeService { let currentFileStr: string if (from === 'ClickApply') { - const uri_ = this._getActiveEditorURI() if (!uri_) return uri = uri_ @@ -1492,8 +1500,6 @@ class EditCodeService extends Disposable implements IEditCodeService { { shouldRealignDiffAreas: true } ) - const { editorModel } = this._voidModelService.getModel(uri) - editorModel?.save() // save the file onDone() resMessageDonePromise() }, @@ -1534,6 +1540,9 @@ class EditCodeService extends Disposable implements IEditCodeService { else { uri = givenURI } + + await this._voidModelService.initializeModel(uri) + const { model } = this._voidModelService.getModel(uri) if (!model) return @@ -1823,8 +1832,6 @@ class EditCodeService extends Disposable implements IEditCodeService { { shouldRealignDiffAreas: true } ) - const { editorModel } = this._voidModelService.getModel(uri) - editorModel?.save() // save the file // TODO!!! make sure this works onDone() resMessageDonePromise() }, 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 8afd17f1..4c3adb20 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 @@ -138,6 +138,9 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri uri: uri, startBehavior: 'reject-conflicts', }) ?? [] + + if (!newApplyingUri) console.log('NOT new applying') + applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined rerender(c => c + 1) @@ -291,7 +294,6 @@ export const BlockCodeApplyWrapper = ({ uri: URI | 'current', }) => { - const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri }) const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -301,7 +303,6 @@ export const BlockCodeApplyWrapper = ({ name={{getBasename(uri.fsPath)}} isSmall={true} showDot={false} - // TODO!!! this uri is not correct, it is not recognized as an actual file for some stupid reason onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }} /> : {language} diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index c69e56af..630cccf2 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -44,7 +44,6 @@ import './chatThreadService.js' import './metricsPollService.js' - // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- // llmMessage @@ -62,3 +61,5 @@ import '../common/metricsService.js' // updates import '../common/voidUpdateService.js' +// model service +import '../common/voidModelService.js' diff --git a/src/vs/workbench/contrib/void/common/voidModelService.ts b/src/vs/workbench/contrib/void/common/voidModelService.ts index 0daa40f9..8cbf4ac9 100644 --- a/src/vs/workbench/contrib/void/common/voidModelService.ts +++ b/src/vs/workbench/contrib/void/common/voidModelService.ts @@ -1,68 +1,69 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, IReference } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { ITextModel } from '../../../../editor/common/model.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ITextFileEditorModel, ITextFileService } from '../../../services/textfile/common/textfiles.js'; +type VoidModelType = { + model: ITextModel | null; + editorModel: IResolvedTextEditorModel | null; +}; -type VoidModelType = { model: ITextModel | null, editorModel: ITextFileEditorModel | null } export interface IVoidModelService { readonly _serviceBrand: undefined; - - initializeModel(uri: URI): Promise - getModel(uri: URI): VoidModelType - getModelSafe(uri: URI): Promise + initializeModel(uri: URI): Promise; + getModel(uri: URI): VoidModelType; + getModelSafe(uri: URI): Promise; } export const IVoidModelService = createDecorator('voidVoidModelService'); + class VoidModelService extends Disposable implements IVoidModelService { _serviceBrand: undefined; - static readonly ID = 'voidVoidModelService'; - - private readonly _modelRefOfURI: Record = {} + private readonly _modelRefOfURI: Record> = {}; constructor( - @ITextFileService private readonly _textFileService: ITextFileService, + @ITextModelService private readonly _textModelService: ITextModelService, ) { - super() + super(); } initializeModel = async (uri: URI) => { - if (uri.fsPath in this._modelRefOfURI) return - if (uri.scheme !== 'file') return - const model = await this._textFileService.files.resolve(uri) + if (uri.fsPath in this._modelRefOfURI) return; + const editorModelRef = await this._textModelService.createModelReference(uri); + // Keep a strong reference to prevent disposal + this._modelRefOfURI[uri.fsPath] = editorModelRef; + }; - this._modelRefOfURI[uri.fsPath] = model - } - getModel = (uri: URI) => { - const editorModel = this._modelRefOfURI[uri.fsPath] - if (!editorModel) return { model: null, editorModel: null } - const model = editorModel.textEditorModel - if (!model) - return { model: null, editorModel } - return { model, editorModel } - } + getModel = (uri: URI): VoidModelType => { + const editorModelRef = this._modelRefOfURI[uri.fsPath]; + if (!editorModelRef) { + return { model: null, editorModel: null }; + } - getModelSafe = async (uri: URI) => { - if (!(uri.fsPath in this._modelRefOfURI)) await this.initializeModel(uri) - return this.getModel(uri) - } + const model = editorModelRef.object.textEditorModel; + + if (!model) { + return { model: null, editorModel: editorModelRef.object }; + } + + return { model, editorModel: editorModelRef.object }; + }; + + getModelSafe = async (uri: URI): Promise => { + if (!(uri.fsPath in this._modelRefOfURI)) await this.initializeModel(uri); + return this.getModel(uri); + + }; override dispose() { - super.dispose() - for (const [_, reference] of Object.entries(this._modelRefOfURI)) { - reference?.dispose() + super.dispose(); + for (const ref of Object.values(this._modelRefOfURI)) { + ref.dispose(); // release reference to allow disposal } } - } registerSingleton(IVoidModelService, VoidModelService, InstantiationType.Eager); -