misc state fixes and fix model background (add voidModelService, delete voidFileService)

This commit is contained in:
Andrew Pareles 2025-03-16 23:19:11 -07:00
parent 39e989d3cc
commit 9350c0dcdf
16 changed files with 295 additions and 339 deletions

View file

@ -14,7 +14,6 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IVoidFileService } from '../common/voidFileService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
@ -28,6 +27,7 @@ import { Position } from '../../../../editor/common/core/position.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IMetricsService } from '../common/metricsService.js';
import { shorten } from '../../../../base/common/labels.js';
import { IVoidModelService } from '../common/voidModelService.js';
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
for (let i = arr.length - 1; i >= 0; i--) {
@ -203,7 +203,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IVoidFileService private readonly _voidFileService: IVoidFileService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IToolsService private readonly _toolsService: IToolsService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@ -658,7 +658,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// define helper functions so we can tell what's going on
const getLatestMessages = async () => {
// recompute files in last message
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidModelService) // all the file CONTENTS or "selections" de-duped
const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files
// replace last userMessage with userMessageFullContent (which contains all the files too)

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 { IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
import { EndOfLinePreference, 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';
@ -40,14 +40,10 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
import { IMetricsService } from '../common/metricsService.js';
import { 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 { getFullLanguage, getLanguageFromModel } from '../common/helpers/getLanguage.js';
// import { IFileService } from '../../../../platform/files/common/files.js';
// import { VSBuffer } from '../../../../base/common/buffer.js';
import { IVoidModelService } from '../common/voidModelService.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@ -245,8 +241,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
diffAreaOfId: Record<string, DiffArea> = {}; // diffareaId -> diffArea
diffOfId: Record<string, Diff> = {}; // diffid -> diff (redundant with diffArea._diffOfId)
_sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId
_sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId
_sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId (computed from it)
_sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId (computed from it)
// only applies to diffZones
// streamingDiffZones: Set<number> = new Set()
@ -278,16 +274,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
@IMetricsService private readonly _metricsService: IMetricsService,
@INotificationService private readonly _notificationService: INotificationService,
@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,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
) {
super();
// this function initializes data structures and listens for changes
const registeredModelListeners = new Set<string>()
const initializeModel = (model: ITextModel) => {
const initializeModel = async (model: ITextModel) => {
await this._voidModelService.initializeModel(model.uri)
// do not add listeners to the same model twice - important, or will see duplicates
if (registeredModelListeners.has(model.uri.fsPath)) return
@ -442,7 +439,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _addDiffAreaStylesToURI = (uri: URI) => {
const model = this._getModel(uri)
const { model } = this._voidModelService.getModel(uri)
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
@ -471,7 +468,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _computeDiffsAndAddStylesToURI = (uri: URI) => {
const fullFileText = this._voidFileService.readModel(uri) ?? ''
const { model } = this._voidModelService.getModel(uri)
if (model === null) return
const fullFileText = model.getValue(EndOfLinePreference.LF)
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
@ -636,8 +635,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const disposeInThisEditorFns: (() => void)[] = []
const model = this._getModel(uri)
if (model === null) return
const { model } = this._voidModelService.getModel(uri)
// green decoration and minimap decoration
if (type !== 'deletion') {
@ -775,38 +773,6 @@ 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()) {
return null
}
return model
}
// not obvious at all, but if we want the model we can just create it. our listeners in the constructor handle it. call this the moment we know we have a uri that we want to make changes to
private async _ensureModelExists(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 = getFullLanguage(this._languageService, { uri, fileContents: fileStr })
const model = this._modelService.createModel(fileStr, lang, uri);
return model
}
private _getActiveEditorURI(): URI | null {
const editor = this._codeEditorService.getActiveCodeEditor()
if (!editor) return null
@ -817,8 +783,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
weAreWriting = false
private _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) {
const model = this._getModel(uri)
if (model === null) return
const { model } = this._voidModelService.getModel(uri)
if (!model) return // TODO!!!! make sure this works
const range: IRange = range_ === 'wholeFileRange' ?
{ startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER } // whole file
@ -831,7 +797,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._realignAllDiffAreasLines(uri, newText, oldRange)
}
const uriStr = model.getValue()
const uriStr = model.getValue(EndOfLinePreference.LF)
// heuristic check
const dontNeedToWrite = uriStr === text
@ -852,6 +818,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) {
const getCurrentSnapshot = async (): Promise<HistorySnapshot> => {
await this._voidModelService.initializeModel(uri)
const { model } = this._voidModelService.getModel(uri)
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
for (const diffareaid in this.diffAreaOfId) {
@ -864,7 +832,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
) as DiffAreaSnapshot
}
const entireFileCode = await this._voidFileService.readFile(uri) ?? ''
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : '' // TODO!!! make sure this isn't '' usually
// this._noLongerNeedModelReference(uri)
return {
snapshottedDiffAreaOfId,
entireFileCode, // the whole file's code
@ -915,11 +885,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
this._onDidAddOrDeleteDiffZones.fire({ uri })
await this._voidModelService.initializeModel(uri)
// restore file content
this._writeURIText(uri, entireModelCode,
'wholeFileRange',
{ shouldRealignDiffAreas: false }
)
// this._noLongerNeedModelReference(uri)
}
const beforeSnapshot: Promise<HistorySnapshot> = getCurrentSnapshot()
@ -1330,13 +1302,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
const uri_ = this._getActiveEditorURI()
if (!uri_) return
uri = uri_
await this._ensureModelExists(uri)
await this._voidModelService.initializeModel(uri)
const { model } = this._voidModelService.getModel(uri)
if (!model) return
const c_ = await this._voidFileService.readFile(uri)
if (c_ === null) return
currentFileStr = c_
const numLines = numLinesOfStr(currentFileStr)
currentFileStr = model.getValue(EndOfLinePreference.LF)
const numLines = model.getLineCount()
// 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'
@ -1353,11 +1324,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
uri = _URI
await this._ensureModelExists(uri)
await this._voidModelService.initializeModel(uri)
const { model } = this._voidModelService.getModel(uri)
if (!model) return
const c_ = await this._voidFileService.readFile(uri)
if (c_ === null) return
currentFileStr = c_
currentFileStr = model.getValue(EndOfLinePreference.LF)
startLine = startLine_
endLine = endLine_
@ -1366,8 +1337,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
throw new Error(`Void: diff.type not recognized on: ${from}`)
}
const { model } = this._voidModelService.getModel(uri)
if (!model) return
const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
const language = getLanguageFromModel(uri, this._modelService)
const language = model.getLanguageId()
let streamRequestIdRef: { current: string | null } = { current: null }
@ -1517,7 +1491,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
{ shouldRealignDiffAreas: true }
)
this._voidFileService.saveOrWriteFileAssumingModelExists(uri)
const { editorModel } = this._voidModelService.getModel(uri)
editorModel?.save() // save the file
onDone()
resMessageDonePromise()
},
@ -1535,7 +1511,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
console.log('done waiting')
}
writeover().then(() => resApplyPromise()).catch((e) => rejApplyPromise(e))
writeover().then(() => {
resApplyPromise()
// this._noLongerNeedModelReference(uri)
}).catch((e) => rejApplyPromise(e))
return [diffZone, applyPromise]
}
@ -1555,14 +1534,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
else {
uri = givenURI
}
await this._ensureModelExists(uri)
const { model } = this._voidModelService.getModel(uri)
if (!model) return
// generate search/replace block text
const originalFileCode = await this._voidFileService.readFile(uri)
if (originalFileCode === null) return
const numLines = numLinesOfStr(originalFileCode)
const originalFileCode = model.getValue(EndOfLinePreference.LF)
const numLines = model.getLineCount()
// reject all diffZones on this URI, adding to history (there can't possibly be overlap after this)
this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true })
@ -1738,7 +1715,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const originalBounds = findTextInCode(block.orig, originalFileCode)
// if error
if (typeof originalBounds === 'string') {
console.log('TEXT NOT FOUND')
console.log('TEXT NOT FOUND, RETRYING')
const content = errMsgOfInvalidStr(originalBounds, block.orig)
messages.push(
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
@ -1776,7 +1753,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const trackingZone = this._addDiffArea(adding)
addedTrackingZoneOfBlockNum.push(trackingZone)
latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
} // <-- done adding diffarea
} // end adding diffarea
// should always be in streaming state here
@ -1846,7 +1823,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
{ shouldRealignDiffAreas: true }
)
this._voidFileService.saveOrWriteFileAssumingModelExists(uri)
const { editorModel } = this._voidModelService.getModel(uri)
editorModel?.save() // save the file // TODO!!! make sure this works
onDone()
resMessageDonePromise()
},
@ -1868,7 +1846,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
} // end retryLoop
retryLoop().then(() => resApplyDonePromise()).catch((e) => rejApplyDonePromise(e))
retryLoop().then(() => {
resApplyDonePromise();
// this._noLongerNeedModelReference(uri)
}).catch((e) => rejApplyDonePromise(e))
return [diffZone, applyDonePromise]
}

View file

@ -1,9 +1,9 @@
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js'
import { useRefState } from '../util/helpers.js'
import { usePromise, useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { LucideIcon, RotateCw } from 'lucide-react'
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js'
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
@ -16,7 +16,7 @@ enum CopyButtonText {
type IconButtonProps = {
onClick: () => void
onClick: () => void;
title: string
Icon: LucideIcon
disabled?: boolean
@ -27,7 +27,11 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu
<button
title={title}
disabled={disabled}
onClick={onClick}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onClick?.();
}}
className={`
size-[22px]
p-[4px]
@ -106,6 +110,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const commandService = accessor.get('ICommandService')
const [_, rerender] = useState(0)
@ -212,17 +217,27 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
/>
)
const jumpToFileHTML = uri !== 'current' && (
<IconShell1
Icon={FileSymlink}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
title="Reject changes"
/>
)
let buttonsHTML = <></>
if (currStreamState === 'streaming') {
buttonsHTML = <>
{jumpToFileHTML}
{stopButton}
</>
}
if (currStreamState === 'idle') {
buttonsHTML = <>
{jumpToFileHTML}
{copyButton}
{playButton}
</>
@ -230,6 +245,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
if (currStreamState === 'acceptRejectAll') {
buttonsHTML = <>
{jumpToFileHTML}
{reapplyButton}
{rejectButton}
{acceptButton}
@ -250,7 +266,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
return {
statusIndicatorHTML,
buttonsHTML
buttonsHTML,
}
}
@ -259,7 +275,6 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
export const BlockCodeApplyWrapper = ({
children,
initValue,
@ -272,7 +287,7 @@ export const BlockCodeApplyWrapper = ({
children: React.ReactNode;
applyBoxId: string;
canApply: boolean;
language: string;
language:string;
uri: URI | 'current',
}) => {

View file

@ -1,12 +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 { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
export const BlockCode = ({ ...codeEditorProps }: VoidCodeEditorProps) => {
const isSingleLine = !codeEditorProps.initValue.includes('\n')
return <VoidCodeEditor {...codeEditorProps} />
}

View file

@ -5,14 +5,16 @@
import React, { JSX, useMemo, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
import { convertToVscodeLang, getFirstLine, getLanguage } from '../../../../common/helpers/getLanguage.js'
import { BlockCodeApplyWrapper, useApplyButtonHTML } from './ApplyBlockHoverButtons.js'
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
import { useAccessor } from '../util/services.js'
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { getBasename } from '../sidebar-tsx/SidebarChat.js'
import { isAbsolute } from '../../../../../../../base/common/path.js'
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
import { BlockCode } from '../util/inputs.js'
export type ChatMessageLocation = {
@ -27,7 +29,7 @@ export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocati
}
function isValidUri(s: string): boolean {
return s.includes('/') && s.length > 5 && isAbsolute(s)
return s.length > 5 && isAbsolute(s) && !s.startsWith('//') // common case that is a false positive is comments like //
}
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
@ -124,25 +126,28 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
}
if (t.type === "code") {
const [firstLine, remainingContents] = getFirstLine(t.text)
const [firstLine, remainingContents] = separateOutFirstLine(t.text)
const firstLineIsURI = isValidUri(firstLine)
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
// figure out langauge and URI
let uri: URI | null
let language: string | undefined = undefined
if (firstLineIsURI) { // get lang from the uri in the first line of the markdown
uri = codeURI ?? URI.from(URI.file(firstLine))
let language: string
if (codeURI) {
uri = codeURI
}
else if (firstLineIsURI) { // get lang from the uri in the first line of the markdown
uri = URI.file(firstLine)
}
else {
uri = codeURI || null
uri = null
}
if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined
language = convertToVscodeLang(languageService, t.lang) // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell)
}
else { // no language provided - fallback - get lang from the uri and contents
language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined })
language = detectLanguage(languageService, { uri, fileContents: contents })
}
if (options.isApplyEnabled && chatMessageLocation) {

View file

@ -12,12 +12,11 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js';
import { URI } from '../../../../../../../base/common/uri.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
@ -29,7 +28,6 @@ import { getModelSelectionState, getModelCapabilities } from '../../../../common
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react';
import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js';
import { useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
import { DiffZone } from '../../../editCodeService.js';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
@ -504,7 +502,7 @@ export const SelectedFiles = (
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const modelService = accessor.get('IModelService')
const modelReferenceService = accessor.get('IVoidModelService')
// state for tracking prospective files
const { currentUri } = useUriState()
@ -519,21 +517,39 @@ export const SelectedFiles = (
return withCurrent.slice(0, maxRecentUris)
})
}, [currentUri])
let prospectiveSelections: StagingSelectionItem[] = []
if (type === 'staging' && showProspectiveSelections) { // handle prospective files
const [prospectiveSelections, setProspectiveSelections] = useState<StagingSelectionItem[]>([])
// handle prospective files
useEffect(() => {
const computeRecents = async () => {
const prospectiveURIs = recentUris
.filter(uri => !selections.find(s => s.type === 'File' && s.fileURI.fsPath === uri.fsPath))
.slice(0, maxProspectiveFiles)
const answer: StagingSelectionItem[] = []
for (const uri of prospectiveURIs) {
answer.push({
type: 'File',
fileURI: uri,
language: (await modelReferenceService.getModelSafe(uri)).model?.getLanguageId() || 'plaintext',
selectionStr: null,
range: null,
state: { isOpened: false },
})
}
return answer
}
// add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet
prospectiveSelections = recentUris
.filter(uri => !selections.find(s => s.type === 'File' && s.fileURI.fsPath === uri.fsPath))
.slice(0, maxProspectiveFiles)
.map(uri => ({
type: 'File',
fileURI: uri,
language: getLanguageFromModel(uri, modelService),
selectionStr: null,
range: null,
state: { isOpened: false },
}))
}
if (type === 'staging' && showProspectiveSelections) {
computeRecents().then((a) => setProspectiveSelections(a))
}
else {
setProspectiveSelections([])
}
}, [recentUris, selections, type, showProspectiveSelections])
const allSelections = [...selections, ...prospectiveSelections]
@ -685,8 +701,8 @@ const ToolHeaderWrapper = ({
isRejected,
}: ToolHeaderParams) => {
const [isExpanded_, setIsExpanded] = useState(false);
const isExpanded = isOpen ? isOpen : isExpanded_
const [isOpen_, setIsOpen] = useState(false);
const isExpanded = isOpen !== undefined ? isOpen : isOpen_
const isDropdown = children !== undefined // null ALLOWS dropdown
const isClickable = !!(isDropdown || onClick)
@ -697,7 +713,7 @@ const ToolHeaderWrapper = ({
<div
className={`select-none flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`}
onClick={() => {
if (isDropdown) { setIsExpanded(v => !v); }
if (isDropdown) { setIsOpen(v => !v); }
if (onClick) { onClick(); }
}}
>
@ -742,7 +758,7 @@ const ToolHeaderWrapper = ({
};
const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'user' } }) => {
const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCommitted: boolean, }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@ -851,7 +867,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
}
}
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show).
if (!chatMessage.content && isCommitted) { // don't show if empty and not loading (if loading, want to show).
return null
}
@ -938,7 +954,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'assistant' } }) => {
const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@ -956,7 +972,7 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
}
const isEmpty = !chatMessage.content && !chatMessage.reasoning
const isLastAndLoading = isLoading && isLast
const isLastAndLoading = !isCommitted && isLast
if (isEmpty && !isLastAndLoading) return null
return <>
@ -988,7 +1004,7 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
>
{/* reasoning token */}
{hasReasoning && <ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!!isLoading}>
{hasReasoning && <ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!isCommitted}>
<ChatMarkdownRender
string={reasoningStr}
chatMessageLocation={chatMessageLocation}
@ -1007,7 +1023,7 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
/>
{/* loading indicator */}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
{!isCommitted && <IconLoading className='opacity-50 text-sm' />}
</div>
</>
@ -1066,7 +1082,6 @@ const ToolRequestAcceptRejectButtons = () => {
const chatThreadsService = accessor.get('IChatThreadService')
const metricsService = accessor.get('IMetricsService')
const onAccept = useCallback(() => {
try { // this doesn't need to be wrapped in try/catch anymore
const threadId = chatThreadsService.state.currentThreadId
@ -1154,15 +1169,7 @@ const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDes
const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
return <ToolContentsWrapper className='bg-void-bg-3'>
<ListableToolItem
showDot={false}
name={uri.fsPath}
className='w-full overflow-auto py-1'
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>
<div className='!select-text cursor-auto my-4'>
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
</div>
@ -1200,9 +1207,9 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe
const isWriting = !isDone
const [isOpen, setIsOpen] = useState(isWriting)
useEffect(() => {
if (!isWriting) setIsOpen(isWriting) // if just finished reasoning, close
if (!isWriting) setIsOpen(false) // if just finished reasoning, close
}, [isWriting])
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen}>
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}>
<ToolContentsWrapper className='bg-void-bg-3 prose-sm'>
<div className='!select-text cursor-auto'>
{children}
@ -1585,36 +1592,36 @@ const toolNameToComponent: { [T in ToolName]: {
type ChatBubbleMode = 'display' | 'edit'
type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isLoading: boolean, isLast: boolean }
const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps) => {
type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isCommitted: boolean, isLast: boolean, showApproveRejectButtons: boolean }
const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, showApproveRejectButtons }: ChatBubbleProps) => {
const role = chatMessage.role
if (role === 'user') {
return <UserMessageComponent
chatMessage={chatMessage}
messageIdx={messageIdx}
isLoading={isLoading}
isLast={isLast}
isCommitted={isCommitted}
/>
}
else if (role === 'assistant') {
return <AssistantMessageComponent
chatMessage={chatMessage}
messageIdx={messageIdx}
isLoading={isLoading}
isCommitted={isCommitted}
isLast={isLast}
/>
}
else if (role === 'tool_request') {
const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough...
if (!isLast) return null
if (!ToolRequestWrapper) return null
return <>
<ToolRequestWrapper toolRequest={chatMessage} />
<ToolRequestAcceptRejectButtons />
</>
if (ToolRequestWrapper) {
return <>
{isLast && <ToolRequestWrapper toolRequest={chatMessage} />}
{showApproveRejectButtons && <ToolRequestAcceptRejectButtons />}
</>
}
return null
}
else if (role === 'tool') {
const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any, messageIdx: number }> // ts isnt smart enough...
@ -1624,10 +1631,6 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
}
const VoidCommandBarNavButtonsShell = () => {
}
const VoidCommandBar = () => {
const accessor = useAccessor()
@ -1638,7 +1641,6 @@ const VoidCommandBar = () => {
const [_, rerender] = useState(0)
// Add a state variable to track focus
const [isFocused, setIsFocused] = useState(false)
console.log('rerender count: ', _)
// state for what the user is currently focused on (both URI and diff)
const [diffIdxOfFspath, setDiffIdxOfFspath] = useState<Record<string, number | undefined>>({})
@ -1843,6 +1845,7 @@ export const SidebarChat = () => {
// stream state
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
const isRunning = !!currThreadStreamState?.isRunning
const isStreaming = !!currThreadStreamState?.streamingToken // might be running but not streaming
const latestError = currThreadStreamState?.error
const messageSoFar = currThreadStreamState?.messageSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
@ -1906,23 +1909,32 @@ export const SidebarChat = () => {
const numMessages = previousMessages.length
const previousMessagesHTML = useMemo(() => {
return previousMessages.map((message, i) =>
<ChatBubble key={getChatBubbleId(currentThread.id, i)} chatMessage={message} messageIdx={i} isLast={i === numMessages - 1} isLoading={false} />
return previousMessages.map((message, i) => {
const isLast = i === numMessages - 1 && !isStreaming // last if there is no streaming assistant message currently
return <ChatBubble key={getChatBubbleId(currentThread.id, i)}
chatMessage={message}
messageIdx={i}
isLast={isLast}
isCommitted={true}
showApproveRejectButtons={isLast}
/>
}
)
}, [previousMessages, currentThread, numMessages])
}, [previousMessages, isStreaming, currentThread, numMessages])
const streamingChatIdx = previousMessagesHTML.length
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ?
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning || isStreaming) ?
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
messageIdx={streamingChatIdx}
chatMessage={{
role: 'assistant',
content: messageSoFar ?? '',
reasoning: reasoningSoFar ?? '',
anthropicReasoning: null,
}}
isLoading={isRunning}
messageIdx={streamingChatIdx}
isCommitted={!isRunning}
isLast={true}
showApproveRejectButtons={false}
/> : null
const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML]

View file

@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
@ -17,3 +17,12 @@ export const useRefState = <T,>(initVal: T): ReturnType<T> => {
}, [])
return [ref, setState]
}
export const usePromise = <T,>(promise: Promise<T>): T | undefined => {
const [val, setVal] = useState<T | undefined>(undefined)
useEffect(() => {
promise.then((v) => setVal(v))
}, [promise])
return val
}

View file

@ -485,7 +485,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
arrowTouchesText?: boolean;
matchInputWidth?: boolean;
gapPx?: number;
offsetPx?:number;
offsetPx?: number;
}) => {
const [isOpen, setIsOpen] = useState(false);
const measureRef = useRef<HTMLDivElement>(null);
@ -787,8 +787,8 @@ const normalizeIndentation = (code: string): string => {
const modelOfEditorId: { [id: string]: ITextModel | undefined } = {}
export type VoidCodeEditorProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }: VoidCodeEditorProps) => {
export type BlockCodeProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
export const BlockCode = ({ initValue, language, maxHeight, showScrollbars }: BlockCodeProps) => {
initValue = normalizeIndentation(initValue)
@ -803,7 +803,6 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
// const languageDetectionService = accessor.get('ILanguageDetectionService')
const modelService = accessor.get('IModelService')
const id = useId()
// these are used to pass to the model creation of modelRef

View file

@ -45,6 +45,8 @@ import { URI } from '../../../../../../../base/common/uri.js'
import { IChatThreadService, ThreadsState, ThreadStreamState } from '../../../chatThreadService.js'
import { ITerminalToolService } from '../../../terminalToolService.js'
import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'
import { IVoidModelService } from '../../../../common/voidModelService.js'
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
@ -213,6 +215,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IMetricsService: accessor.get(IMetricsService),
ITerminalToolService: accessor.get(ITerminalToolService),
ILanguageService: accessor.get(ILanguageService),
IVoidModelService: accessor.get(IVoidModelService),
IWorkspaceContextService: accessor.get(IWorkspaceContextService),
} as const
return reactAccessor

View file

@ -7,8 +7,6 @@ import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
// import { ILLMMessageService } from '../common/llmMessageService.js';
// import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
@ -17,7 +15,7 @@ export interface ISearchReplaceService {
}
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
class SearchReplaceService extends Disposable implements ISearchReplaceService {
export class SearchReplaceService extends Disposable implements ISearchReplaceService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();

View file

@ -7,9 +7,10 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
import { ISearchService } from '../../../services/search/common/search.js'
import { IEditCodeService } from './editCodeServiceInterface.js'
import { IVoidFileService } from '../common/voidFileService.js'
import { ITerminalToolService } from './terminalToolService.js'
import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
import { IVoidModelService } from '../common/voidModelService.js'
import { EndOfLinePreference } from '../../../../editor/common/model.js'
// tool use for AI
@ -183,7 +184,7 @@ export class ToolsService implements IToolsService {
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@ISearchService searchService: ISearchService,
@IInstantiationService instantiationService: IInstantiationService,
@IVoidFileService voidFileService: IVoidFileService,
@IVoidModelService voidModelService: IVoidModelService,
@IEditCodeService editCodeService: IEditCodeService,
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
) {
@ -271,8 +272,10 @@ 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 { model } = await voidModelService.getModelSafe(uri)
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
const readFileContents = model.getValue(EndOfLinePreference.LF)
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate

View file

@ -5,58 +5,26 @@
import { URI } from '../../../../../base/common/uri.js';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
export const getFirstLine = (content: string): [string, string] | [string, undefined] => {
const newLineIdx = content.indexOf('\r\n')
if (newLineIdx !== -1) {
const A = content.substring(0, newLineIdx + 2)
const B = content.substring(newLineIdx + 2, Infinity);
return [A, B]
}
const newLineIdx2 = content.indexOf('\n')
if (newLineIdx2 !== -1) {
const A = content.substring(0, newLineIdx2 + 1)
const B = content.substring(newLineIdx2 + 1, Infinity);
return [A, B]
}
return [content, undefined]
}
import { separateOutFirstLine } from './util.js';
export function getFullLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }) {
const firstLine = opts.fileContents ? getFirstLine(opts.fileContents)?.[0] : undefined
// this works better than model.getLanguageId()
export function detectLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }) {
const firstLine = opts.fileContents ? separateOutFirstLine(opts.fileContents)?.[0] : undefined
const fullLang = languageService.createByFilepathOrFirstLine(opts.uri, firstLine)
return fullLang
}
export function getLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }): string {
return getFullLanguage(languageService, opts).languageId
}
export function getLanguageFromModel(uri: URI, modelService: IModelService): string {
return modelService.getModel(uri)?.getLanguageId() || ''
}
// conversions
const convertToVoidLanguage = (languageService: ILanguageService, language: string) => {
const { languageId } = languageService.createById(language)
return languageId
return fullLang.languageId || 'plaintext'
}
// --- conversions
export const convertToVscodeLang = (languageService: ILanguageService, markdownLang: string) => {
if (markdownLang in markdownLangToVscodeLang)
return markdownLangToVscodeLang[markdownLang]
return convertToVoidLanguage(languageService, markdownLang)
const { languageId } = languageService.createById(markdownLang)
return languageId
}
// // eg "bash" -> "shell"
const markdownLangToVscodeLang: { [key: string]: string } = {
// Web Technologies

View file

@ -0,0 +1,18 @@
export const separateOutFirstLine = (content: string): [string, string] | [string, undefined] => {
const newLineIdx = content.indexOf('\r\n')
if (newLineIdx !== -1) {
const A = content.substring(0, newLineIdx)
const B = content.substring(newLineIdx + 2, Infinity);
return [A, B]
}
const newLineIdx2 = content.indexOf('\n')
if (newLineIdx2 !== -1) {
const A = content.substring(0, newLineIdx2)
const B = content.substring(newLineIdx2 + 1, Infinity);
return [A, B]
}
return [content, undefined]
}

View file

@ -6,9 +6,10 @@
import { URI } from '../../../../../base/common/uri.js';
import { os } from '../helpers/systemInfo.js';
import { IVoidFileService } from '../voidFileService.js';
import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
// this is just for ease of readability
@ -81,10 +82,11 @@ ${tripleTick[1]}
}
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
const stringifyFileSelections = async (fileSelections: FileSelection[], voidFileService: IVoidFileService) => {
const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => {
if (fileSelections.length === 0) return null
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
const content = await voidFileService.readFile(sel.fileURI) ?? failToReadStr
const { model } = await voidModelService.getModelSafe(sel.fileURI)
const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr
return { ...sel, content }
}))
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
@ -115,7 +117,7 @@ export const chat_userMessageContent = async (instructions: string, currSelns: S
export const chat_selectionsString = async (
prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null,
voidFileService: IVoidFileService,
voidModelService: IVoidModelService,
) => {
// ADD IN FILES AT TOP
@ -141,7 +143,7 @@ export const chat_selectionsString = async (
}
}
const filesStr = await stringifyFileSelections(fileSelections, voidFileService)
const filesStr = await stringifyFileSelections(fileSelections, voidModelService)
const selnsStr = stringifyCodeSelections(codeSelections)

View file

@ -1,114 +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 { 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 | null>;
readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null;
saveOrWriteFileAssumingModelExists(uri: URI): Promise<void>;
}
export const IVoidFileService = createDecorator<IVoidFileService>('VoidFileService');
// implemented by calling channel
export class VoidFileService implements IVoidFileService {
readonly _serviceBrand: undefined;
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 | null> => {
// attempt to read the model
const modelResult = this.readModel(uri, range);
if (modelResult) return modelResult;
// if no model, read the raw file
const fileResult = await this._readFileRaw(uri, range);
if (fileResult) return fileResult;
return null;
}
_readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null> => {
try { // this throws an error if no file exists (eg it was deleted)
const res = await this.fileService.readFile(uri);
const str = res.value.toString().replace(/\r\n/g, '\n'); // even if not on Windows, might read a file with \r\n
if (range) return str.split('\n').slice(range.startLineNumber - 1, range.endLineNumber).join('\n')
return str;
} catch (e) {
return null;
}
}
readModel = (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null => {
// read saved model (sometimes null if the user reloads application)
let model = this.modelService.getModel(uri);
// check all opened models for the same `fsPath`
if (!model) {
const models = this.modelService.getModels();
for (const m of models) {
if (m.uri.fsPath === uri.fsPath) {
model = m
break;
}
}
}
// if still not found, return
if (!model) { return null }
// if range, read it
if (range) {
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);

View file

@ -0,0 +1,68 @@
/*--------------------------------------------------------------------------------------
* 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 { URI } from '../../../../base/common/uri.js';
import { ITextModel } from '../../../../editor/common/model.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: ITextFileEditorModel | null }
export interface IVoidModelService {
readonly _serviceBrand: undefined;
initializeModel(uri: URI): Promise<void>
getModel(uri: URI): VoidModelType
getModelSafe(uri: URI): Promise<VoidModelType>
}
export const IVoidModelService = createDecorator<IVoidModelService>('voidVoidModelService');
class VoidModelService extends Disposable implements IVoidModelService {
_serviceBrand: undefined;
static readonly ID = 'voidVoidModelService';
private readonly _modelRefOfURI: Record<string, ITextFileEditorModel> = {}
constructor(
@ITextFileService private readonly _textFileService: ITextFileService,
) {
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)
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 }
}
getModelSafe = async (uri: URI) => {
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()
}
}
}
registerSingleton(IVoidModelService, VoidModelService, InstantiationType.Eager);