diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
index 06a1c4ae..e534a324 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
@@ -6,7 +6,7 @@
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
-import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js';
+import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js';
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../chatThreadService.js';
import { BlockCode } from '../markdown/BlockCode.js';
@@ -259,9 +259,9 @@ const getBasename = (pathStr: string) => {
}
export const SelectedFiles = (
- { type, selections, setStaging }:
- | { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined }
- | { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) }
+ { type, selections, setSelections }:
+ | { type: 'past', selections: CodeSelection[]; setSelections?: undefined }
+ | { type: 'staging', selections: CodeStagingSelection[]; setSelections: ((newSelections: CodeStagingSelection[]) => void) }
) => {
// index -> isOpened
@@ -273,85 +273,104 @@ export const SelectedFiles = (
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
+ const { currentUri } = useUriState()
+
+ let prospectiveSelections: CodeStagingSelection[] = []
+ if ( // add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet
+ type === 'staging'
+ && currentUri
+ && !selections.find(s => s.range === null && s.fileURI.fsPath === currentUri.fsPath)
+ ) {
+ prospectiveSelections = [{
+ type: 'File',
+ fileURI: currentUri,
+ selectionStr: null,
+ range: null,
+ }]
+ }
+
+ const allSelections = [...selections, ...prospectiveSelections]
+
return (
- !!selections && selections.length !== 0 && (
-
- {selections.map((selection, i) => {
+
- const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
- const isThisSelectionAFile = selection.selectionStr === null
+ {allSelections.map((selection, i) => {
- return
selections.length - 1
+
+ return
+ {/* selection summary */}
+
- {/* selection summary */}
-
-
{
- // open the file if it is a file
- if (isThisSelectionAFile) {
- commandService.executeCommand('vscode.open', selection.fileURI, {
- preview: true,
- // preserveFocus: false,
- });
- } else {
- // open the selection if it is a text-selection
- setSelectionIsOpened(s => {
- const newS = [...s]
- newS[i] = !newS[i]
- return newS
- });
- }
- }}
- >
-
- {/* file name */}
- {getBasename(selection.fileURI.fsPath)}
- {/* selection range */}
- {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
-
+ onClick={() => {
+ if (isThisSelectionProspective) { // add prospective selection to selections
+ if (type !== 'staging') return; // (never)
+ setSelections([...selections, selection as CodeStagingSelection])
- {/* X button */}
- {type === 'staging' &&
- {
- e.stopPropagation(); // don't open/close selection
- if (type !== 'staging') return;
- setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
- setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
- }}
- >
-
- }
+ } else if (isThisSelectionAFile) { // open files
+ commandService.executeCommand('vscode.open', selection.fileURI, {
+ preview: true,
+ // preserveFocus: false,
+ });
+ } else { // show text
+ setSelectionIsOpened(s => {
+ const newS = [...s]
+ newS[i] = !newS[i]
+ return newS
+ });
+ }
+ }}
+ >
+
+ {/* file name */}
+ {getBasename(selection.fileURI.fsPath)}
+ {/* selection range */}
+ {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
+
+
+ {/* X button */}
+ {type === 'staging' &&
+ {
+ e.stopPropagation(); // don't open/close selection
+ if (type !== 'staging') return;
+ setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
+ setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
+ }}
+ >
+
+ }
-
+
- {/* clear all selections button */}
- {type !== 'staging' || selections.length === 0 || i !== selections.length - 1
- ? null
- :
-
setIsClearHovered(true)}
- onMouseLeave={() => setIsClearHovered(false)}
- >
-
+ setIsClearHovered(true)}
+ onMouseLeave={() => setIsClearHovered(false)}
+ >
+ { setStaging([]) }}
- />
-
+ onClick={() => { setSelections([]) }}
+ />
- }
-
- {/* selection text */}
- {isThisSelectionOpened &&
-
{
- e.stopPropagation(); // don't focus input box
- }}
- >
-
}
+ {/* selection text */}
+ {isThisSelectionOpened &&
+
{
+ e.stopPropagation(); // don't focus input box
+ }}
+ >
+
+
+ }
+
- })}
+ })}
-
- )
+
+
)
}
@@ -407,7 +426,7 @@ const ChatBubble = ({ chatMessage, isLoading }: {
if (role === 'user') {
chatbubbleContents = <>
-
+
{chatMessage.displayContent}
{/* {!isEditMode ? chatMessage.displayContent : <>>} */}
@@ -604,9 +623,8 @@ export const SidebarChat = () => {
{/* top row */}
<>
{/* selections */}
- {(selections && selections.length !== 0) &&
-
- }
+
+
{/* error message */}
{latestError === undefined ? null :
diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
index b5613ab6..b15cd2a6 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx
@@ -10,6 +10,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
+import { VoidUriState } from '../../../voidUriStateService.js';
import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
@@ -28,6 +29,7 @@ import { ILLMMessageService } from '../../../../../../../platform/void/common/ll
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
+import { IVoidUriStateService } from '../../../voidUriStateService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IChatThreadService } from '../../../chatThreadService.js';
@@ -47,10 +49,14 @@ import { IPathService } from '../../../../../../../workbench/services/path/commo
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
+
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
+let uriState: VoidUriState
+const uriStateListeners: Set<(s: VoidUriState) => void> = new Set()
+
let quickEditState: VoidQuickEditState
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
@@ -90,6 +96,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
_registerAccessor(accessor)
const stateServices = {
+ uriStateService: accessor.get(IVoidUriStateService),
quickEditStateService: accessor.get(IQuickEditStateService),
sidebarStateService: accessor.get(ISidebarStateService),
chatThreadsStateService: accessor.get(IChatThreadService),
@@ -99,7 +106,15 @@ export const _registerServices = (accessor: ServicesAccessor) => {
inlineDiffsService: accessor.get(IInlineDiffsService),
}
- const { sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
+ const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
+
+ uriState = uriStateService.state
+ disposables.push(
+ uriStateService.onDidChangeState(() => {
+ uriState = uriStateService.state
+ uriStateListeners.forEach(l => l(uriState))
+ })
+ )
quickEditState = quickEditStateService.state
disposables.push(
@@ -178,6 +193,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IInlineDiffsService: accessor.get(IInlineDiffsService),
+ IVoidUriStateService: accessor.get(IVoidUriStateService),
IQuickEditStateService: accessor.get(IQuickEditStateService),
ISidebarStateService: accessor.get(ISidebarStateService),
IChatThreadService: accessor.get(IChatThreadService),
@@ -224,6 +240,16 @@ export const useAccessor = () => {
// -- state of services --
+export const useUriState = () => {
+ const [s, ss] = useState(uriState)
+ useEffect(() => {
+ ss(uriState)
+ uriStateListeners.add(ss)
+ return () => { uriStateListeners.delete(ss) }
+ }, [ss])
+ return s
+}
+
export const useQuickEditState = () => {
const [s, ss] = useState(quickEditState)
useEffect(() => {
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts
index 3b260158..5787fc4d 100644
--- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts
+++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts
@@ -26,10 +26,9 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
-import { URI } from '../../../../base/common/uri.js';
import { localize2 } from '../../../../nls.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
-
+import { IVoidUriStateService } from './voidUriStateService.js';
// ---------- Register commands and keybindings ----------
@@ -241,7 +240,7 @@ registerAction2(class extends Action2 {
export class TabSwitchListener extends Disposable {
constructor(
- onSwitchTab: (uri: URI) => void,
+ onSwitchTab: () => void,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
@@ -250,7 +249,7 @@ export class TabSwitchListener extends Disposable {
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(editor.onDidChangeModel(e => {
if (e.newModelUrl?.scheme !== 'file') return
- onSwitchTab(e.newModelUrl)
+ onSwitchTab()
}))
}
@@ -270,8 +269,10 @@ class TabSwitchContribution extends Disposable implements IWorkbenchContribution
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
- @ICommandService private readonly commandService: ICommandService,
@IViewsService private readonly viewsService: IViewsService,
+ @IVoidUriStateService private readonly uriStateService: IVoidUriStateService,
+ @ICodeEditorService private readonly codeEditorService: ICodeEditorService,
+ // @ICommandService private readonly commandService: ICommandService,
) {
super()
@@ -281,18 +282,22 @@ class TabSwitchContribution extends Disposable implements IWorkbenchContribution
sidebarIsVisible = e.visible
}))
- const addCurrentFileIfVisible = () => {
- if (sidebarIsVisible)
- this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
+ const onSwitchTab = () => { // update state
+ if (sidebarIsVisible) {
+ const currentUri = this.codeEditorService.getActiveCodeEditor()?.getModel()?.uri
+ if (!currentUri) return;
+ this.uriStateService.setState({ currentUri })
+ // this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
+ }
}
// when sidebar becomes visible, add current file
this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible }))
// run on current tab if it exists, and listen for tab switches and visibility changes
- addCurrentFileIfVisible()
- this._register(this.viewsService.onDidChangeViewVisibility(() => { addCurrentFileIfVisible() }))
- this._register(this.instantiationService.createInstance(TabSwitchListener, () => { addCurrentFileIfVisible() }))
+ onSwitchTab()
+ this._register(this.viewsService.onDidChangeViewVisibility(() => { onSwitchTab() }))
+ this._register(this.instantiationService.createInstance(TabSwitchListener, () => { onSwitchTab() }))
}
}
diff --git a/src/vs/workbench/contrib/void/browser/voidUriStateService.ts b/src/vs/workbench/contrib/void/browser/voidUriStateService.ts
new file mode 100644
index 00000000..1a89c29b
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/voidUriStateService.ts
@@ -0,0 +1,57 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import { Emitter, Event } from '../../../../base/common/event.js';
+import { Disposable } from '../../../../base/common/lifecycle.js';
+import { URI } from '../../../../base/common/uri.js';
+import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
+import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
+
+
+
+// service that manages state
+export type VoidUriState = {
+ currentUri?: URI
+}
+
+export interface IVoidUriStateService {
+ readonly _serviceBrand: undefined;
+
+ readonly state: VoidUriState; // readonly to the user
+ setState(newState: Partial
): void;
+ onDidChangeState: Event;
+}
+
+export const IVoidUriStateService = createDecorator('voidUriStateService');
+class VoidUriStateService extends Disposable implements IVoidUriStateService {
+ _serviceBrand: undefined;
+
+ static readonly ID = 'voidUriStateService';
+
+ private readonly _onDidChangeState = new Emitter();
+ readonly onDidChangeState: Event = this._onDidChangeState.event;
+
+
+ // state
+ state: VoidUriState
+
+ constructor(
+ ) {
+ super()
+
+ // initial state
+ this.state = { currentUri: undefined }
+ }
+
+ setState(newState: Partial) {
+
+ this.state = { ...this.state, ...newState }
+ this._onDidChangeState.fire()
+ }
+
+
+}
+
+registerSingleton(IVoidUriStateService, VoidUriStateService, InstantiationType.Eager);