dsh-vscode-workbench 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +17 -123
- package/README.md +17 -125
- package/lib/client.js +771 -375
- package/lib/client.js.map +1 -1
- package/lib/index.js +423 -26
- package/lib/index.js.map +1 -1
- package/lib/types/client/ActivityBar.d.ts +5 -1
- package/lib/types/client/DshConversationPanel.d.ts +1 -1
- package/lib/types/client/DshMark.d.ts +3 -0
- package/lib/types/client/MonacoEditor.d.ts +18 -2
- package/lib/types/client/Workbench.d.ts +9 -0
- package/lib/types/client/WorkspaceSwitcher.d.ts +3 -2
- package/lib/types/client/api.d.ts +2 -0
- package/lib/types/client/lsp.d.ts +6 -0
- package/lib/types/client/service.d.ts +1 -1
- package/lib/types/client/terminal.d.ts +2 -2
- package/lib/types/commit-message.d.ts +43 -0
- package/lib/types/host.d.ts +3 -0
- package/lib/types/lsp.d.ts +21 -0
- package/package.json +3 -1
- package/src/client/ActivityBar.tsx +71 -34
- package/src/client/DshConversationPanel.tsx +1 -1
- package/src/client/DshMark.tsx +4 -4
- package/src/client/MonacoEditor.tsx +88 -7
- package/src/client/TerminalPanel.tsx +3 -3
- package/src/client/Workbench.tsx +97 -40
- package/src/client/WorkspaceSwitcher.tsx +7 -3
- package/src/client/api.ts +6 -1
- package/src/client/lsp.ts +3 -0
- package/src/client/service.ts +1 -1
- package/src/client/styles.ts +125 -2
- package/src/client/terminal.ts +16 -10
- package/src/commit-message.ts +224 -0
- package/src/host.ts +47 -0
- package/src/index.ts +18 -1
- package/src/lsp.ts +85 -25
|
@@ -22,6 +22,20 @@ export const WORKBENCH_EDITOR_CHROME = {
|
|
|
22
22
|
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
|
|
23
23
|
} as const
|
|
24
24
|
|
|
25
|
+
/** macOS reports Control-click as a secondary click, so Monaco only navigates with Cmd-click there. */
|
|
26
|
+
export function isMacPlatform(userAgent: string = navigator.userAgent): boolean {
|
|
27
|
+
return /Macintosh|Mac OS X/.test(userAgent)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether a mouse press should jump to the definition. On macOS, Control-click is a common
|
|
32
|
+
* Windows/Linux habit but arrives as a context-menu gesture (sometimes as a right-button
|
|
33
|
+
* press); treat it as Cmd-click instead.
|
|
34
|
+
*/
|
|
35
|
+
export function isMacDefinitionClick(event: { button: number; ctrlKey: boolean; metaKey: boolean }): boolean {
|
|
36
|
+
return (event.button === 0 || event.button === 2) && event.ctrlKey && !event.metaKey
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
/** Monaco theme aligned with the workbench dark palette. */
|
|
26
40
|
export const DARK_THEME = 'dvw-dark'
|
|
27
41
|
const DARK_RULES = [
|
|
@@ -71,8 +85,43 @@ const DARK_COLORS = {
|
|
|
71
85
|
'scrollbarSlider.hoverBackground': '#646464b3',
|
|
72
86
|
'scrollbarSlider.activeBackground': '#bfbfbf66',
|
|
73
87
|
}
|
|
88
|
+
/** Eye-care palette: the editor shares the workbench's warm yellow surface. */
|
|
89
|
+
export const EYE_THEME = 'dvw-eyecare'
|
|
90
|
+
const EYE_COLORS = {
|
|
91
|
+
'editor.background': '#f7ebc8',
|
|
92
|
+
'editor.foreground': '#3b3324',
|
|
93
|
+
'editorLineNumber.foreground': '#92856a',
|
|
94
|
+
'editorLineNumber.activeForeground': '#5c4a2a',
|
|
95
|
+
'editor.selectionBackground': '#e6d294',
|
|
96
|
+
'editor.inactiveSelectionBackground': '#ecdcae',
|
|
97
|
+
'editor.lineHighlightBackground': '#efe1b4',
|
|
98
|
+
'editorCursor.foreground': '#6b5320',
|
|
99
|
+
'editorWidget.background': '#f7ebc8',
|
|
100
|
+
'editorWidget.border': '#e0cf9f',
|
|
101
|
+
'editorSuggestWidget.background': '#f7ebc8',
|
|
102
|
+
'editorSuggestWidget.border': '#e0cf9f',
|
|
103
|
+
'editorSuggestWidget.selectedBackground': '#f0e0ae',
|
|
104
|
+
'editorHoverWidget.background': '#f7ebc8',
|
|
105
|
+
'editorHoverWidget.border': '#e0cf9f',
|
|
106
|
+
'editorGutter.background': '#f7ebc8',
|
|
107
|
+
'diffEditor.insertedTextBackground': '#b7d7a8aa',
|
|
108
|
+
'diffEditor.removedTextBackground': '#e8b4ad88',
|
|
109
|
+
'diffEditor.border': '#e0cf9f',
|
|
110
|
+
'diffEditor.diagonalFill': '#eadcb6',
|
|
111
|
+
'scrollbarSlider.background': '#92856a55',
|
|
112
|
+
'scrollbarSlider.hoverBackground': '#92856a88',
|
|
113
|
+
'scrollbarSlider.activeBackground': '#5c4a2a66',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type MonacoColorScheme = 'dark' | 'light' | 'white' | 'eyecare'
|
|
117
|
+
|
|
118
|
+
export function monacoThemeFor(theme: MonacoColorScheme): string {
|
|
119
|
+
return theme === 'dark' ? DARK_THEME : theme === 'eyecare' ? EYE_THEME : 'vs'
|
|
120
|
+
}
|
|
121
|
+
|
|
74
122
|
function defineWorkbenchThemes(monaco: Monaco): void {
|
|
75
123
|
monaco.editor.defineTheme(DARK_THEME, { base: 'vs-dark', inherit: true, rules: DARK_RULES, colors: DARK_COLORS })
|
|
124
|
+
monaco.editor.defineTheme(EYE_THEME, { base: 'vs', inherit: true, rules: [], colors: EYE_COLORS })
|
|
76
125
|
}
|
|
77
126
|
|
|
78
127
|
/**
|
|
@@ -105,13 +154,13 @@ export function loadMonaco(): Promise<Monaco> {
|
|
|
105
154
|
return loading
|
|
106
155
|
}
|
|
107
156
|
|
|
108
|
-
export function MonacoDiffEditor(props: { uri: string; original: string; modified: string; mode: 'inline' | 'split'; theme:
|
|
157
|
+
export function MonacoDiffEditor(props: { uri: string; original: string; modified: string; mode: 'inline' | 'split'; theme: MonacoColorScheme; fontSize?: number }) {
|
|
109
158
|
const root=useRef<HTMLDivElement>(null);const editor=useRef<import('monaco-editor').editor.IStandaloneDiffEditor>();const models=useRef<{original:import('monaco-editor').editor.ITextModel;modified:import('monaco-editor').editor.ITextModel}>();const [error,setError]=useState<string>()
|
|
110
|
-
useEffect(()=>{let disposed=false;void loadMonaco().then(monaco=>{if(disposed||root.current===null)return;const safe=props.uri.replace(/^\/+/, '');const original=monaco.editor.createModel(props.original,undefined,monaco.Uri.parse(`dvw-diff-original:///${safe}`));const modified=monaco.editor.createModel(props.modified,undefined,monaco.Uri.parse(`dvw-diff-modified:///${safe}`));models.current={original,modified};const instance=monaco.editor.createDiffEditor(root.current,{automaticLayout:true,readOnly:true,renderSideBySide:props.mode==='split',theme:props.theme
|
|
159
|
+
useEffect(()=>{let disposed=false;void loadMonaco().then(monaco=>{if(disposed||root.current===null)return;const safe=props.uri.replace(/^\/+/, '');const original=monaco.editor.createModel(props.original,undefined,monaco.Uri.parse(`dvw-diff-original:///${safe}`));const modified=monaco.editor.createModel(props.modified,undefined,monaco.Uri.parse(`dvw-diff-modified:///${safe}`));models.current={original,modified};const instance=monaco.editor.createDiffEditor(root.current,{automaticLayout:true,readOnly:true,renderSideBySide:props.mode==='split',useInlineViewWhenSpaceIsLimited:false,theme:monacoThemeFor(props.theme),fontSize:props.fontSize??14,...WORKBENCH_EDITOR_CHROME,originalEditable:false,ignoreTrimWhitespace:false});instance.setModel({original,modified});editor.current=instance}).catch(error=>setError(error instanceof Error?error.message:String(error)));return()=>{disposed=true;editor.current?.dispose();editor.current=undefined;models.current?.original.dispose();models.current?.modified.dispose();models.current=undefined}},[props.uri])
|
|
111
160
|
useEffect(()=>{const value=models.current;if(value===undefined)return;if(value.original.getValue()!==props.original)value.original.setValue(props.original);if(value.modified.getValue()!==props.modified)value.modified.setValue(props.modified)},[props.original,props.modified])
|
|
112
|
-
useEffect(()=>{editor.current
|
|
161
|
+
useEffect(()=>{const value=editor.current;if(value===undefined)return;value.updateOptions({renderSideBySide:props.mode==='split'});const frame=requestAnimationFrame(()=>value.layout());return()=>cancelAnimationFrame(frame)},[props.mode])
|
|
113
162
|
useEffect(()=>{editor.current?.updateOptions({fontSize:props.fontSize??14})},[props.fontSize])
|
|
114
|
-
useEffect(()=>{if(window.monaco!==undefined)window.monaco.editor.setTheme(props.theme
|
|
163
|
+
useEffect(()=>{if(window.monaco!==undefined)window.monaco.editor.setTheme(monacoThemeFor(props.theme))},[props.theme])
|
|
115
164
|
return error===undefined?<div className="dvw-diff dvw-monaco-diff" data-mode={props.mode} data-render-side-by-side={props.mode==='split'} ref={root}/>:<div className="dvw-error">{error}</div>
|
|
116
165
|
}
|
|
117
166
|
|
|
@@ -152,10 +201,11 @@ function editorSelection(instance: import('monaco-editor').editor.IStandaloneCod
|
|
|
152
201
|
return { text: model.getValueInRange(range), startLine: range.startLineNumber, endLine }
|
|
153
202
|
}
|
|
154
203
|
|
|
155
|
-
export function MonacoEditor(props: { uri: string; value: string; theme:
|
|
204
|
+
export function MonacoEditor(props: { uri: string; value: string; theme: MonacoColorScheme; line?: number; column?: number; fontSize?: number; highlight?: string; onChange(value: string): void; onSave(): void; onAddToDsh?(selection?: EditorSelection): void }) {
|
|
156
205
|
const root = useRef<HTMLDivElement>(null)
|
|
157
206
|
const editor = useRef<import('monaco-editor').editor.IStandaloneCodeEditor>()
|
|
158
207
|
const decorations = useRef<import('monaco-editor').editor.IEditorDecorationsCollection>()
|
|
208
|
+
const cleanupListeners = useRef<(() => void) | undefined>(undefined)
|
|
159
209
|
const latest = useRef(props)
|
|
160
210
|
latest.current = props
|
|
161
211
|
const [error, setError] = useState<string>()
|
|
@@ -200,15 +250,46 @@ export function MonacoEditor(props: { uri: string; value: string; theme: 'dark'
|
|
|
200
250
|
void loadMonaco().then((monaco) => {
|
|
201
251
|
if (disposed || root.current === null) return
|
|
202
252
|
const props = latest.current
|
|
203
|
-
const instance = monaco.editor.create(root.current, { model: null, automaticLayout: true, theme: props.theme
|
|
253
|
+
const instance = monaco.editor.create(root.current, { model: null, automaticLayout: true, theme: monacoThemeFor(props.theme), fontSize: props.fontSize ?? 14, ...WORKBENCH_EDITOR_CHROME, wordWrap: 'off' })
|
|
204
254
|
editor.current = instance
|
|
205
255
|
instance.onDidChangeModelContent(() => latest.current.onChange(instance.getValue()))
|
|
206
256
|
instance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => latest.current.onSave())
|
|
207
257
|
instance.addAction({ id: 'dvw.addToDsh', label: '添加到 DSH 对话', contextMenuGroupId: 'navigation', contextMenuOrder: 0, keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KeyK], run: () => latest.current.onAddToDsh?.(editorSelection(instance)) })
|
|
258
|
+
// Monaco maps go-to-definition to Cmd-click on macOS; also honor the usual Ctrl-click.
|
|
259
|
+
const mac = isMacPlatform()
|
|
260
|
+
let jumpedAt = -Infinity
|
|
261
|
+
const jumpToDefinition = (clientX: number, clientY: number): boolean => {
|
|
262
|
+
const position = instance.getTargetAtClientPoint(clientX, clientY)?.position
|
|
263
|
+
if (position === null || position === undefined) return false
|
|
264
|
+
jumpedAt = performance.now()
|
|
265
|
+
instance.setPosition(position); instance.focus()
|
|
266
|
+
// Definition navigation is registered as a command (Action2); getAction() cannot reach it.
|
|
267
|
+
instance.trigger('dvw.definitionClick', 'editor.action.revealDefinition', null)
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
const onMouseDown = (event: MouseEvent) => {
|
|
271
|
+
if (!mac || !isMacDefinitionClick(event)) return
|
|
272
|
+
event.preventDefault(); event.stopPropagation()
|
|
273
|
+
jumpToDefinition(event.clientX, event.clientY)
|
|
274
|
+
}
|
|
275
|
+
// Chrome turns Control-click on macOS into a context menu; suppress it so the jump stands.
|
|
276
|
+
const onContextMenu = (event: MouseEvent) => {
|
|
277
|
+
if (!mac || !event.ctrlKey || event.metaKey) return
|
|
278
|
+
event.preventDefault(); event.stopPropagation()
|
|
279
|
+
// macOS may swallow the Control state in mousedown but keep it here; jump as a fallback.
|
|
280
|
+
if (performance.now() - jumpedAt > 500) jumpToDefinition(event.clientX, event.clientY)
|
|
281
|
+
}
|
|
282
|
+
root.current.addEventListener('mousedown', onMouseDown, true)
|
|
283
|
+
root.current.addEventListener('contextmenu', onContextMenu, true)
|
|
284
|
+
cleanupListeners.current = () => {
|
|
285
|
+
root.current?.removeEventListener('mousedown', onMouseDown, true)
|
|
286
|
+
root.current?.removeEventListener('contextmenu', onContextMenu, true)
|
|
287
|
+
}
|
|
208
288
|
attach(monaco, instance)
|
|
209
289
|
}).catch(error => setError(error instanceof Error ? error.message : String(error)))
|
|
210
290
|
return () => {
|
|
211
291
|
disposed = true
|
|
292
|
+
cleanupListeners.current?.(); cleanupListeners.current = undefined
|
|
212
293
|
const instance = editor.current
|
|
213
294
|
const model = instance?.getModel(); const state = instance?.saveViewState()
|
|
214
295
|
if (model && state) viewStates.set(model.uri.toString(), state)
|
|
@@ -222,7 +303,7 @@ export function MonacoEditor(props: { uri: string; value: string; theme: 'dark'
|
|
|
222
303
|
useEffect(() => {
|
|
223
304
|
const model = editor.current?.getModel(); if (model !== null && model !== undefined && model.getValue() !== props.value) model.setValue(props.value)
|
|
224
305
|
}, [props.value])
|
|
225
|
-
useEffect(() => { if (window.monaco !== undefined) window.monaco.editor.setTheme(props.theme
|
|
306
|
+
useEffect(() => { if (window.monaco !== undefined) window.monaco.editor.setTheme(monacoThemeFor(props.theme)) }, [props.theme])
|
|
226
307
|
useEffect(() => { editor.current?.updateOptions({ fontSize: props.fontSize ?? 14 }) }, [props.fontSize])
|
|
227
308
|
useEffect(() => {
|
|
228
309
|
const instance = editor.current; const monaco = window.monaco; const model = instance?.getModel()
|
|
@@ -7,9 +7,9 @@ import { isTerminalProtocolReply, TerminalWorkspaceController } from './terminal
|
|
|
7
7
|
|
|
8
8
|
// VS Code Light+ ANSI palette: the default xterm colors (e.g. yellow #e5e510) vanish on a light background.
|
|
9
9
|
const LIGHT_ANSI = { black: '#000000', red: '#cd3131', green: '#107c10', yellow: '#949800', blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555', brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14', brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc', brightCyan: '#0598bc', brightWhite: '#a5a5a5' }
|
|
10
|
-
function terminalTheme(colorScheme: 'dark' | 'light') { return colorScheme === 'dark' ? { background: '#191a1b', foreground: '#bfbfbf', cursor: '#bfbfbf', selectionBackground: '#264f78' } : { background: '#ffffff', foreground: '#1d2025', cursor: '#4b515b', selectionBackground: '#add6ff', ...LIGHT_ANSI } }
|
|
10
|
+
function terminalTheme(colorScheme: 'dark' | 'light' | 'white' | 'eyecare') { return colorScheme === 'dark' ? { background: '#191a1b', foreground: '#bfbfbf', cursor: '#bfbfbf', selectionBackground: '#264f78' } : { background: '#ffffff', foreground: '#1d2025', cursor: '#4b515b', selectionBackground: '#add6ff', ...LIGHT_ANSI } }
|
|
11
11
|
|
|
12
|
-
function XtermView({ workspace, id, colorScheme }: { workspace: TerminalWorkspaceController; id: string; colorScheme: 'dark' | 'light' }) {
|
|
12
|
+
function XtermView({ workspace, id, colorScheme }: { workspace: TerminalWorkspaceController; id: string; colorScheme: 'dark' | 'light' | 'white' | 'eyecare' }) {
|
|
13
13
|
const root = useRef<HTMLDivElement>(null)
|
|
14
14
|
const terminalRef = useRef<Terminal>()
|
|
15
15
|
const replaying = useRef(false)
|
|
@@ -59,7 +59,7 @@ export function TerminalPanel({ sessionId, colorScheme }: BottomPanelProps) {
|
|
|
59
59
|
const [sideWidth, setSideWidth] = useState(220)
|
|
60
60
|
useEffect(() => {
|
|
61
61
|
let cancelled = false
|
|
62
|
-
void workspace.refresh().then(
|
|
62
|
+
void workspace.refresh().then(ready => { if (!cancelled && ready && workspace.getSnapshot().terminals.length === 0) void workspace.create().then(value => { if (!cancelled) setActive(value.id) }).catch(() => undefined) })
|
|
63
63
|
return () => { cancelled = true }
|
|
64
64
|
}, [workspace])
|
|
65
65
|
useEffect(() => { if (snapshot.terminals.length > 0 && (!active || !snapshot.terminals.some(value => value.id === active))) setActive(snapshot.terminals[0]?.id) }, [snapshot.terminals, active])
|
package/src/client/Workbench.tsx
CHANGED
|
@@ -8,27 +8,40 @@ import { FileIcon } from './FileIcon.tsx'
|
|
|
8
8
|
import { branchErrorMessage } from './gitError.ts'
|
|
9
9
|
import { MarkdownPreview, markdownHeadings } from './MarkdownPreview.tsx'
|
|
10
10
|
import type { MarkdownHeading } from './MarkdownPreview.tsx'
|
|
11
|
-
import { MonacoDiffEditor, MonacoEditor, loadMonaco, preloadMonaco, retainEditorModels } from './MonacoEditor.tsx'
|
|
11
|
+
import { MonacoDiffEditor, MonacoEditor, isMacPlatform, loadMonaco, preloadMonaco, retainEditorModels } from './MonacoEditor.tsx'
|
|
12
12
|
import { DshConversationPanel, DshConversationTheme } from './DshConversationPanel.tsx'
|
|
13
13
|
import { DshMark } from './DshMark.tsx'
|
|
14
14
|
import { ActivityBar, normalizeActivityOrder, type ActivityView } from './ActivityBar.tsx'
|
|
15
15
|
import { WorkspaceSwitcher } from './WorkspaceSwitcher.tsx'
|
|
16
16
|
import type { DshNavigation, WorkspaceChoice } from './dshNavigation.ts'
|
|
17
|
-
import { LspManager,
|
|
17
|
+
import { LspManager, type LspServerStatus } from './lsp.ts'
|
|
18
18
|
import { TS_INTELLISENSE_KEY, disposeTypeScriptProject, indexTypeScriptProject, refreshIndexedFiles, tsIntellisenseEnabled, workspacePathFromModel } from './typescriptProject.ts'
|
|
19
19
|
import { ADD_TO_DSH_SHORTCUT, dshReference, insertIntoDshComposer, isAddToDshShortcut } from './dshComposer.ts'
|
|
20
20
|
import { DocumentPreview, documentPreviewKind, type DocumentPreviewKind } from './documentPreview.tsx'
|
|
21
21
|
import { toggleTerminal } from './shortcuts.ts'
|
|
22
22
|
import { websocketUrl } from './websocket.ts'
|
|
23
23
|
import { resetWorkspaceDocuments, WorkspaceRequestScope } from './workspaceState.ts'
|
|
24
|
+
import type { CommitDetail, CommitLanguage, CommitMessagePreferences } from '../commit-message.ts'
|
|
24
25
|
|
|
25
26
|
interface Tab extends FileDocument { instance?: symbol; dirty: boolean; draft: string; loaded?: boolean; line?: number; column?: number; highlight?: string; external?: boolean; preview?: boolean; title?: string; diff?: GitDiff; diffMode?: 'inline' | 'split'; diffSourcePath?:string; imageUrl?:string; documentPreview?:DocumentPreviewKind; documentUrl?:string }
|
|
26
|
-
type WorkbenchTheme = 'dsh' | 'dark' | 'light'
|
|
27
|
+
type WorkbenchTheme = 'dsh' | 'dark' | 'light' | 'white' | 'eyecare'
|
|
28
|
+
export type MarkdownDefaultView = 'preview' | 'source'
|
|
29
|
+
export const MARKDOWN_DEFAULT_VIEW_KEY = 'dvw:markdown-default-view'
|
|
27
30
|
interface TreeProps { sessionId: string; path: string; depth: number; expanded: Set<string>; selected?: string; selectedDirectory: string; statuses: Map<string, string>; onToggle(path: string): void; onSelectDirectory(path: string): void; onOpen(path: string): void; onChange(): void; onUpload(directory: string, files: FileList): void; onAddToDsh(path: string, directory: boolean): void }
|
|
28
31
|
interface CreatingEntry { kind: 'file' | 'directory'; directory: string; value: string; error?: string }
|
|
29
32
|
|
|
30
33
|
type ResizeDirection = 1 | -1
|
|
31
34
|
|
|
35
|
+
export const DSH_CHAT_MIN_WIDTH = 440
|
|
36
|
+
export const DSH_CHAT_MAX_WIDTH = 720
|
|
37
|
+
export const DSH_CHAT_DEFAULT_WIDTH = 440
|
|
38
|
+
|
|
39
|
+
/** Keep persisted or dragged widths inside the range the DSH composer can lay out cleanly. */
|
|
40
|
+
export function clampChatWidth(width: number): number {
|
|
41
|
+
if (!Number.isFinite(width)) return DSH_CHAT_DEFAULT_WIDTH
|
|
42
|
+
return Math.max(DSH_CHAT_MIN_WIDTH, Math.min(DSH_CHAT_MAX_WIDTH, Math.round(width)))
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
/** Calculate a panel width from a horizontal pointer movement. */
|
|
33
46
|
export function resizedPanelWidth(startWidth: number, startX: number, nextX: number, direction: ResizeDirection, min: number, max: number): number {
|
|
34
47
|
return Math.max(min, Math.min(max, Math.round(startWidth + (nextX - startX) * direction)))
|
|
@@ -83,7 +96,10 @@ function PanelResizer({ className, label, width, min, max, defaultWidth, directi
|
|
|
83
96
|
}}/>
|
|
84
97
|
}
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
/** Read the saved Markdown default without depending on browser storage during unit tests. */
|
|
100
|
+
export function markdownDefaultView():MarkdownDefaultView{try{return localStorage.getItem(MARKDOWN_DEFAULT_VIEW_KEY)==='source'?'source':'preview'}catch{return 'preview'}}
|
|
101
|
+
|
|
102
|
+
function unloadedTab(path:string):Tab{return {instance:Symbol(path),path,content:'',draft:'',revision:'',size:0,binary:false,tooLarge:false,dirty:false,loaded:false,preview:/\.md$/i.test(path)?markdownDefaultView()==='preview':undefined}}
|
|
87
103
|
|
|
88
104
|
function WorkbenchTitlebar({ name }: { name?: string }) {
|
|
89
105
|
return <div className="dvw-titlebar"><strong>DSH Workbench</strong>{name !== undefined && <span>{name}</span>}</div>
|
|
@@ -377,22 +393,33 @@ function GitGroupHeader({ title, count, expanded, onToggle, children }: { title:
|
|
|
377
393
|
return <div className="dvw-group-head"><button className="dvw-group-toggle" aria-expanded={expanded} onClick={onToggle}><Icon name={expanded?'chevron-down':'chevron-right'}/><span>{title}</span><span className="dvw-count">{count}</span></button>{children}</div>
|
|
378
394
|
}
|
|
379
395
|
|
|
380
|
-
function Git(props: { sessionId: string; onDiff(key:string, diff:GitDiff, title?:string, sourcePath?:string): void }) {
|
|
381
|
-
const [status, setStatus] = useState<GitStatus>(); const [commits,setCommits]=useState<GitCommit[]>([]); const [message, setMessage] = useState(''); const [error, setError] = useState<string>(); const [busy,setBusy]=useState(false); const [expanded,setExpanded]=useState({staged:true,changes:true,graph:true});const [expandedCommit,setExpandedCommit]=useState<string>();const [commitFileMap,setCommitFileMap]=useState<Map<string,GitCommitFile[]>>(new Map());const [branchMenu,setBranchMenu]=useState(false);const [branches,setBranches]=useState<GitBranch[]>([]);const [newBranch,setNewBranch]=useState('');const [branchError,setBranchError]=useState<string>()
|
|
396
|
+
function Git(props: { sessionId: string; preferences: CommitMessagePreferences; onDiff(key:string, diff:GitDiff, title?:string, sourcePath?:string): void }) {
|
|
397
|
+
const [status, setStatus] = useState<GitStatus>(); const [commits,setCommits]=useState<GitCommit[]>([]); const [message, setMessage] = useState(''); const [error, setError] = useState<string>(); const [busy,setBusy]=useState(false); const [expanded,setExpanded]=useState({staged:true,changes:true,graph:true});const [expandedCommit,setExpandedCommit]=useState<string>();const [commitFileMap,setCommitFileMap]=useState<Map<string,GitCommitFile[]>>(new Map());const [branchMenu,setBranchMenu]=useState(false);const [branches,setBranches]=useState<GitBranch[]>([]);const [newBranch,setNewBranch]=useState('');const [branchError,setBranchError]=useState<string>();const [generating,setGenerating]=useState(false);const [commitMenu,setCommitMenu]=useState(false)
|
|
382
398
|
const refresh = useCallback(() => { void Promise.all([api<GitStatus>('git.status',{sessionId:props.sessionId}),api<GitCommit[]>('git.action',{sessionId:props.sessionId,action:'log'})]).then(([nextStatus,nextCommits])=>{setStatus(nextStatus);setCommits(nextCommits);setError(undefined)}).catch(error=>{setStatus(undefined);setCommits([]);setError(error instanceof ApiError&&error.code==='not-repository'?undefined:String(error))}) }, [props.sessionId])
|
|
383
399
|
const refreshDebounced = useDebouncedCallback(refresh, 500)
|
|
384
400
|
useEffect(refresh,[refresh]); useEffect(()=>{const fn=()=>refreshDebounced();window.addEventListener('dvw-files-changed',fn);return()=>window.removeEventListener('dvw-files-changed',fn)},[refreshDebounced])
|
|
385
401
|
const action = async (name: string, path?: string, value?: string) => { setBusy(true);try { const result = await api<GitStatus | GitDiff>('git.action',{sessionId:props.sessionId,action:name,path,value}); if(name==='diff'&&'diff'in result){const key=path??'';props.onDiff(key,result,key.split('/').at(-1)??key,key)} else {setStatus(result as GitStatus);if(name==='commit')refresh()} } catch(error){setError(String(error))}finally{setBusy(false)} }
|
|
386
402
|
useEffect(()=>{if(!branchMenu)return;const close=()=>setBranchMenu(false);const escape=(event:KeyboardEvent)=>{if(event.key==='Escape')close()};window.addEventListener('pointerdown',close);window.addEventListener('keydown',escape);return()=>{window.removeEventListener('pointerdown',close);window.removeEventListener('keydown',escape)}},[branchMenu])
|
|
403
|
+
useEffect(()=>{if(!commitMenu)return;const close=()=>setCommitMenu(false);const escape=(event:KeyboardEvent)=>{if(event.key==='Escape')close()};window.addEventListener('pointerdown',close);window.addEventListener('keydown',escape);return()=>{window.removeEventListener('pointerdown',close);window.removeEventListener('keydown',escape)}},[commitMenu])
|
|
387
404
|
const openBranchMenu=async()=>{if(branchMenu){setBranchMenu(false);return}setBusy(true);try{setBranches(await api<GitBranch[]>('git.action',{sessionId:props.sessionId,action:'branch.list'}));setBranchMenu(true)}catch(error){setError(String(error))}finally{setBusy(false)}}
|
|
388
405
|
const switchBranch=async(name:string)=>{if(name===status?.branch){setBranchMenu(false);return}setBusy(true);try{setStatus(await api<GitStatus>('git.action',{sessionId:props.sessionId,action:'branch.switch',value:name}));setBranchMenu(false);refresh()}catch(error){setError(undefined);setBranchError(branchErrorMessage(error))}finally{setBusy(false)}}
|
|
389
406
|
const createBranch=async()=>{const name=newBranch.trim();if(name==='')return;setBusy(true);try{setStatus(await api<GitStatus>('git.action',{sessionId:props.sessionId,action:'branch.create',value:name}));setNewBranch('');setBranchMenu(false);refresh()}catch(error){setError(undefined);setBranchError(branchErrorMessage(error))}finally{setBusy(false)}}
|
|
390
407
|
const toggleCommit=async(commit:GitCommit)=>{if(expandedCommit===commit.hash){setExpandedCommit(undefined);return}setExpandedCommit(commit.hash);if(commitFileMap.has(commit.hash))return;setBusy(true);try{const files=await api<GitCommitFile[]>('git.action',{sessionId:props.sessionId,action:'commit.files',value:commit.hash});setCommitFileMap(current=>new Map(current).set(commit.hash,files))}catch(error){setError(String(error))}finally{setBusy(false)}}
|
|
391
408
|
const openCommitFile=async(commit:GitCommit,file:GitCommitFile)=>{setBusy(true);try{const result=await api<GitDiff>('git.action',{sessionId:props.sessionId,action:'commit.file.diff',path:file.path,value:commit.hash});props.onDiff(`commit:${commit.hash}:${file.path}`,result,file.path.split('/').at(-1)??file.path,file.path)}catch(error){setError(String(error))}finally{setBusy(false)}}
|
|
409
|
+
const generateMessage=async()=>{if(generating)return;setGenerating(true);setError(undefined);try{const result=await api<{message:string}>('git.commitMessage',{sessionId:props.sessionId,detail:props.preferences.detail,language:props.preferences.language});setMessage(result.message)}catch(error){setError(error instanceof ApiError?error.message:String(error))}finally{setGenerating(false)}}
|
|
392
410
|
const staged=status?.changes.filter(change=>change.index!=='.'&&change.index!=='?')??[]
|
|
393
411
|
const changed=status?.changes.filter(change=>change.worktree!=='.'||change.untracked)??[]
|
|
394
412
|
const renderChange=(change:GitStatus['changes'][number],group:'staged'|'worktree')=><div className="dvw-change" key={`${group}:${change.path}`} onClick={()=>void action('diff',change.path,change.untracked?'untracked':group)}><FileIcon name={change.path.split('/').at(-1) ?? change.path}/><span className="dvw-change-path">{change.path}</span><span className="dvw-badge">{change.untracked?'U':group==='staged'?change.index:change.worktree}</span><button className="dvw-icon" title={group==='staged'?'取消暂存':'暂存更改'} onClick={event=>{event.stopPropagation();void action(group==='staged'?'unstage':'stage',change.path)}}><Icon name={group==='staged'?'remove':'add'}/></button>{group==='worktree'&&<button className="dvw-icon" title="放弃更改" onClick={event=>{event.stopPropagation();if(confirm(`放弃 ${change.path} 的更改?此操作无法撤销。`))void action('discard',change.path,change.untracked?'untracked':undefined)}}><Icon name="discard"/></button>}</div>
|
|
395
|
-
return
|
|
413
|
+
return <>
|
|
414
|
+
<div className="dvw-side-head">源代码管理<div className="dvw-toolbar"><button className="dvw-icon" title="刷新" disabled={busy} onClick={refresh}><Icon name="refresh"/></button></div></div>
|
|
415
|
+
{error&&<div className="dvw-error">{error}</div>}
|
|
416
|
+
{status&&<>
|
|
417
|
+
<div className="dvw-git-head"><button className="dvw-branch" aria-haspopup="menu" aria-expanded={branchMenu} onClick={()=>void openBranchMenu()}><Icon name="git-branch"/> {status.branch} <Icon name="chevron-down" className="dvw-branch-caret"/></button><span>↑{status.ahead} ↓{status.behind}</span>{branchMenu&&<div className="dvw-branch-menu" role="menu" onPointerDown={event=>event.stopPropagation()}><div className="dvw-branch-current">当前分支:{status.branch}</div><div className="dvw-branch-list">{branches.map(branch=><button key={branch.name} role="menuitem" data-active={branch.name===status.branch} disabled={busy||branch.worktree!==undefined} title={branch.worktree!==undefined?`已在 worktree ${branch.worktree} 中检出`:undefined} onClick={()=>void switchBranch(branch.name)}><Icon name={branch.name===status.branch?'check':'git-branch'}/><span>{branch.name}</span>{branch.worktree!==undefined&&<span className="dvw-branch-worktree">worktree</span>}</button>)}</div><div className="dvw-branch-create"><input className="dvw-input" value={newBranch} placeholder="新分支名称" onChange={event=>setNewBranch(event.target.value)} onKeyDown={event=>{if(event.key==='Enter'){event.preventDefault();void createBranch()}}}/><button className="dvw-icon dvw-branch-create-button" title="创建并切换分支" disabled={busy||!newBranch.trim()} onClick={()=>void createBranch()}><Icon name="add"/></button></div></div>}</div>
|
|
418
|
+
<div className="dvw-commit"><div className="dvw-commit-box"><textarea className="dvw-textarea dvw-commit-input" rows={1} value={message} onChange={event=>setMessage(event.target.value)} onKeyDown={event=>{if((event.ctrlKey||event.metaKey)&&event.key==='Enter'&&message.trim())void action('commit',undefined,message).then(()=>setMessage(''))}} placeholder={`消息 (${isMacPlatform()?'⌘':'Ctrl+'}Enter 在“${status.branch}”提交)`}/><button className="dvw-commit-ai" disabled={busy||generating||status.changes.length===0} title={generating?'正在生成提交信息':'根据当前更改生成提交信息'} aria-label="生成提交信息" onClick={()=>void generateMessage()}><Icon name="sparkle"/></button></div><div className="dvw-commit-actions"><button className="dvw-commit-button" disabled={busy||!message.trim()||status.changes.length===0} title="提交暂存的更改" onClick={()=>void action('commit',undefined,message).then(()=>setMessage(''))}><Icon name="check"/> 提交</button><button className="dvw-commit-more" disabled={busy||!message.trim()||status.changes.length===0} aria-haspopup="menu" aria-expanded={commitMenu} title="更多提交操作" aria-label="更多提交操作" onClick={()=>setCommitMenu(value=>!value)}><Icon name="chevron-down"/></button>{commitMenu&&<div className="dvw-commit-menu" role="menu" onPointerDown={event=>event.stopPropagation()}><button role="menuitem" onClick={()=>{setCommitMenu(false);void action('commit',undefined,message).then(()=>setMessage(''))}}>提交</button><button role="menuitem" onClick={()=>{setCommitMenu(false);void action('commit.amend',undefined,message).then(()=>setMessage(''))}}>提交(修改)</button><div className="dvw-commit-menu-sep" role="separator"/><button role="menuitem" onClick={()=>{setCommitMenu(false);void action('commit',undefined,message).then(()=>action('push')).then(()=>setMessage(''))}}>提交和推送</button><button role="menuitem" onClick={()=>{setCommitMenu(false);void action('commit',undefined,message).then(()=>action('sync')).then(()=>setMessage(''))}}>提交和同步</button></div>}</div></div>
|
|
419
|
+
<div className="dvw-git">{staged.length>0&&<section><GitGroupHeader title="暂存的更改" count={staged.length} expanded={expanded.staged} onToggle={()=>setExpanded(value=>({...value,staged:!value.staged}))}><button className="dvw-icon" title="取消暂存所有更改" onClick={()=>void action('unstage.all')}><Icon name="remove"/></button></GitGroupHeader>{expanded.staged&&staged.map(change=>renderChange(change,'staged'))}</section>}<section><GitGroupHeader title="更改" count={changed.length} expanded={expanded.changes} onToggle={()=>setExpanded(value=>({...value,changes:!value.changes}))}><button className="dvw-icon" title="放弃所有更改" onClick={()=>{if(confirm('放弃所有已跟踪和未跟踪的更改?此操作无法撤销。'))void action('discard.all')}}><Icon name="discard"/></button><button className="dvw-icon" title="暂存所有更改" onClick={()=>void action('stage.all')}><Icon name="add"/></button></GitGroupHeader>{expanded.changes&&changed.map(change=>renderChange(change,'worktree'))}</section><section className="dvw-graph"><GitGroupHeader title="图表" count={commits.length} expanded={expanded.graph} onToggle={()=>setExpanded(value=>({...value,graph:!value.graph}))}><button className="dvw-icon" title="刷新提交图表" onClick={refresh}><Icon name="refresh"/></button></GitGroupHeader>{expanded.graph&&<CommitGraph commits={commits} expanded={expandedCommit} files={commitFileMap} onToggle={commit=>void toggleCommit(commit)} onOpenFile={(commit,file)=>void openCommitFile(commit,file)}/>}</section></div>
|
|
420
|
+
{branchError!==undefined&&<div className="dvw-confirm-backdrop" role="presentation" onMouseDown={()=>setBranchError(undefined)}><section className="dvw-confirm" role="alertdialog" aria-modal="true" aria-labelledby="dvw-branch-error-title" onMouseDown={event=>event.stopPropagation()}><h2 id="dvw-branch-error-title">无法切换分支</h2><p className="dvw-branch-error-message">{branchError}</p><footer><button autoFocus onClick={()=>setBranchError(undefined)}>确定</button></footer></section></div>}
|
|
421
|
+
</>}
|
|
422
|
+
</>
|
|
396
423
|
}
|
|
397
424
|
|
|
398
425
|
/** Keep range dragging responsive and commit the editor font size after a short pause. */
|
|
@@ -414,15 +441,23 @@ const GitView = memo(Git)
|
|
|
414
441
|
export function Workbench({ service, sessionId, cwd, navigation }: { service: VscodeWorkbench; sessionId?: string; cwd?: string; navigation?: DshNavigation }) {
|
|
415
442
|
const [snapshot, setSnapshot] = useState(() => service.getSnapshot()); const [panels, setPanels] = useState(() => service.getPanels())
|
|
416
443
|
useEffect(() => service.subscribe(() => { setSnapshot(service.getSnapshot()); setPanels(service.getPanels()) }), [service])
|
|
417
|
-
const [workspace,setWorkspace]=useState<{cwd:string;name:string}>(); const [tabs,setTabs]=useState<Tab[]>([]); const [active,setActive]=useState<string>(); const [error,setError]=useState<string>(); const [panelHeight,setPanelHeight]=useState(260); const [sideWidth,setSideWidth]=useState(210); const [primarySideVisible,setPrimarySideVisible]=useState(()=>localStorage.getItem('dvw:primary-side-visible')!=='closed');const [statusVisible,setStatusVisible]=useState(()=>localStorage.getItem('dvw:status-bar-visible')==='open');const [settingsOpen,setSettingsOpen]=useState(false);const [chatWidth,setChatWidth]=useState(()=>
|
|
444
|
+
const [workspace,setWorkspace]=useState<{cwd:string;name:string}>(); const [tabs,setTabs]=useState<Tab[]>([]); const [active,setActive]=useState<string>(); const [error,setError]=useState<string>(); const [panelHeight,setPanelHeight]=useState(260); const [sideWidth,setSideWidth]=useState(210); const [primarySideVisible,setPrimarySideVisible]=useState(()=>localStorage.getItem('dvw:primary-side-visible')!=='closed');const [statusVisible,setStatusVisible]=useState(()=>localStorage.getItem('dvw:status-bar-visible')==='open');const [settingsOpen,setSettingsOpen]=useState(false);const [chatWidth,setChatWidth]=useState(()=>clampChatWidth(Number(localStorage.getItem('dvw:dsh-chat-width'))||DSH_CHAT_DEFAULT_WIDTH));const [chatVisible,setChatVisible]=useState(()=>localStorage.getItem('dvw:dsh-chat-visible')!=='closed');const [outlineVisible,setOutlineVisible]=useState(()=>localStorage.getItem('dvw:outline')!=='closed');const [tsIntellisense,setTsIntellisense]=useState(tsIntellisenseEnabled);const [tsIndex,setTsIndex]=useState<{state:'idle'|'loading'|'ready'|'error';files?:number;truncated?:boolean;error?:string}>({state:'idle'});const [lspServers,setLspServers]=useState<LspServerStatus[]>([]);const [lspBusy,setLspBusy]=useState<string>();const [lspError,setLspError]=useState('');const lspManager=useRef<LspManager>();const [fontSize,setFontSize]=useState(()=>{const saved=Math.round(Number(localStorage.getItem('dvw:font-size')));return Number.isFinite(saved)&&saved>=11&&saved<=20?saved:14});const [tabMenu,setTabMenu]=useState<{x:number;y:number;path:string}>()
|
|
418
445
|
const tabsRef=useRef(tabs);tabsRef.current=tabs
|
|
446
|
+
const [settingsSection,setSettingsSection]=useState<'appearance'|'layout'|'commit'|'language'>('appearance')
|
|
419
447
|
const tabListRef=useRef<HTMLDivElement>(null)
|
|
420
448
|
const workspaceRequests=useRef(new WorkspaceRequestScope());const loadedWorkspaceCwd=useRef<string>();const loadingTabs=useRef(new Map<string, symbol>())
|
|
421
449
|
const dshDark = useDshDarkTheme()
|
|
422
|
-
const [theme,setTheme]=useState<WorkbenchTheme>(()=>{const saved=localStorage.getItem('dvw:theme');return saved==='dark'||saved==='light'?saved:'dsh'})
|
|
423
|
-
const
|
|
450
|
+
const [theme,setTheme]=useState<WorkbenchTheme>(()=>{const saved=localStorage.getItem('dvw:theme');return saved==='dark'||saved==='light'||saved==='white'||saved==='eyecare'?saved:'dsh'})
|
|
451
|
+
const [markdownView,setMarkdownView]=useState<MarkdownDefaultView>(()=>markdownDefaultView())
|
|
452
|
+
const [commitDetail,setCommitDetail]=useState<CommitDetail>(()=>{const saved=localStorage.getItem('dvw:commit-detail');return saved==='concise'||saved==='detailed'?saved:'standard'})
|
|
453
|
+
const [commitLanguage,setCommitLanguage]=useState<CommitLanguage>(()=>{const saved=localStorage.getItem('dvw:commit-language');return saved==='zh'||saved==='en'?saved:'auto'})
|
|
454
|
+
const colorScheme: 'dark' | 'light' | 'white' | 'eyecare' = theme === 'dsh' ? dshDark ? 'dark' : 'light' : theme
|
|
455
|
+
const commitPreferences=useMemo<CommitMessagePreferences>(()=>({detail:commitDetail,language:commitLanguage}),[commitDetail,commitLanguage])
|
|
424
456
|
useEffect(()=>localStorage.setItem('dvw:theme',theme),[theme])
|
|
425
|
-
useEffect(()=>
|
|
457
|
+
useEffect(()=>localStorage.setItem(MARKDOWN_DEFAULT_VIEW_KEY,markdownView),[markdownView])
|
|
458
|
+
useEffect(()=>localStorage.setItem('dvw:commit-detail',commitDetail),[commitDetail])
|
|
459
|
+
useEffect(()=>localStorage.setItem('dvw:commit-language',commitLanguage),[commitLanguage])
|
|
460
|
+
useEffect(()=>{if(!snapshot.visible||theme==='dsh')return;const body=document.body;const root=document.documentElement;const wasDark=body.hasAttribute('data-ds-dark-theme');const previousColorScheme=root.style.colorScheme;body.toggleAttribute('data-ds-dark-theme',theme==='dark');root.style.colorScheme=theme==='dark'?'dark':'light';return()=>{body.toggleAttribute('data-ds-dark-theme',wasDark);if(previousColorScheme==='')root.style.removeProperty('color-scheme');else root.style.colorScheme=previousColorScheme}},[snapshot.visible,theme])
|
|
426
461
|
useEffect(()=>localStorage.setItem('dvw:outline',outlineVisible?'open':'closed'),[outlineVisible])
|
|
427
462
|
useEffect(()=>localStorage.setItem('dvw:dsh-chat-visible',chatVisible?'open':'closed'),[chatVisible])
|
|
428
463
|
useEffect(()=>localStorage.setItem('dvw:dsh-chat-width',String(Math.round(chatWidth))),[chatWidth])
|
|
@@ -439,7 +474,7 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
|
|
|
439
474
|
// The conversation may still be attaching after the chat column opens, so retry for about a second.
|
|
440
475
|
const addToDsh=useCallback((reference:string)=>{setChatVisible(true);let frames=0;const attempt=()=>{void insertIntoDshComposer(reference).then(done=>{if(done)return;if(++frames<60){requestAnimationFrame(attempt);return}alert('DSH 对话输入框当前不可用,请先选择会话或等待当前回复结束。')})};requestAnimationFrame(attempt)},[])
|
|
441
476
|
const addEntryToDsh=useCallback((path:string,directory:boolean)=>addToDsh(dshReference(path,{directory})),[addToDsh])
|
|
442
|
-
const openForWorkspace=(path:string,line?:number,column?:number,highlight?:string)=>{const existing=tabsRef.current.find(tab=>tab.path===path);if(existing)setTabs(rows=>rows.map(tab=>tab.path===path?{...tab,line,column,highlight,preview:line!==undefined?false:tab.preview}:tab));else setTabs(rows=>[...rows,{...unloadedTab(path),line,column,highlight
|
|
477
|
+
const openForWorkspace=(path:string,line?:number,column?:number,highlight?:string)=>{const existing=tabsRef.current.find(tab=>tab.path===path);if(existing)setTabs(rows=>rows.map(tab=>tab.path===path?{...tab,line,column,highlight,preview:line!==undefined?false:tab.preview}:tab));else setTabs(rows=>[...rows,{...unloadedTab(path),line,column,highlight,...(line!==undefined?{preview:false}:{})}]);setActive(path)}
|
|
443
478
|
useEffect(() => {
|
|
444
479
|
if (sessionId === undefined) return
|
|
445
480
|
// Switching sessions inside one workspace must keep the explorer, tabs and layout as they are.
|
|
@@ -573,24 +608,6 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
|
|
|
573
608
|
// Stable handlers let the memoised side views skip re-rendering while the editor types.
|
|
574
609
|
const openRef=useRef(open);openRef.current=open
|
|
575
610
|
const openPath=useCallback((path:string,line?:number,column?:number,highlight?:string)=>openRef.current(path,line,column,highlight),[])
|
|
576
|
-
// Offer to install the language service that handles the file being opened.
|
|
577
|
-
useEffect(()=>{
|
|
578
|
-
if(active===undefined||active.startsWith('diff:'))return
|
|
579
|
-
const id=languageServerIdForPath(active)
|
|
580
|
-
if(id===undefined||dismissedLsp.current.has(id))return
|
|
581
|
-
if(id==='typescript'){ if(!tsIntellisense)setLspPrompt({id:'typescript',name:'TypeScript / JavaScript',builtin:true,installable:true}); return }
|
|
582
|
-
const server=lspServers.find(value=>value.id===id)
|
|
583
|
-
if(server===undefined||server.installed)return
|
|
584
|
-
setLspPrompt({id:server.id,name:server.name,builtin:false,installable:server.installable,hint:server.hint})
|
|
585
|
-
},[active,lspServers,tsIntellisense])
|
|
586
|
-
const dismissLspPrompt=()=>{ if(lspPrompt!==undefined)dismissedLsp.current.add(lspPrompt.id); setLspPrompt(undefined) }
|
|
587
|
-
const installLspPrompt=async()=>{
|
|
588
|
-
const prompt=lspPrompt; if(prompt===undefined)return
|
|
589
|
-
setLspPrompt(undefined)
|
|
590
|
-
if(prompt.builtin){setTsIntellisense(true);return}
|
|
591
|
-
if(!prompt.installable){setSettingsOpen(true);return}
|
|
592
|
-
await changeLsp(prompt.id,'install')
|
|
593
|
-
}
|
|
594
611
|
const [switcherOpen,setSwitcherOpen]=useState(false);const [switcherChoices,setSwitcherChoices]=useState<WorkspaceChoice[]>([]);const [switcherBusy,setSwitcherBusy]=useState(false);const [switcherError,setSwitcherError]=useState<string>()
|
|
595
612
|
const openSwitcher=useCallback(()=>{setSwitcherError(undefined);setSwitcherChoices(navigation?.choices(sessionId)??[]);setSwitcherOpen(true)},[navigation,sessionId])
|
|
596
613
|
const switchSession=useCallback((id:string)=>{navigation?.openSession(id);setSwitcherOpen(false)},[navigation])
|
|
@@ -623,8 +640,22 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
|
|
|
623
640
|
const refreshLsp=useCallback(async()=>{if(sessionId===undefined)return;try{const list=await api<LspServerStatus[]>('lsp.status',{sessionId});setLspServers(list);await startLsp(list)}catch(error){setLspError(error instanceof Error?error.message:String(error))}},[sessionId,startLsp])
|
|
624
641
|
const changeLsp=useCallback(async(id:string,action:'install'|'uninstall')=>{if(sessionId===undefined)return;setLspBusy(id);setLspError('');try{const list=await api<LspServerStatus[]>(`lsp.${action}`,{sessionId,id});setLspServers(list);await startLsp(list)}catch(error){setLspError(error instanceof Error?error.message:String(error))}finally{setLspBusy(undefined)}},[sessionId,startLsp])
|
|
625
642
|
useEffect(()=>{if(!snapshot.visible||sessionId===undefined||workspace===undefined)return;void refreshLsp()},[snapshot.visible,sessionId,workspace,refreshLsp])
|
|
643
|
+
// Opening the language panel re-probes the machine; there is no manual "re-detect" button to click.
|
|
644
|
+
useEffect(()=>{if(settingsOpen&&settingsSection==='language')void refreshLsp()},[settingsOpen,settingsSection,refreshLsp])
|
|
626
645
|
useEffect(()=>()=>{lspManager.current?.dispose()},[])
|
|
627
646
|
useEffect(()=>{try{localStorage.setItem(TS_INTELLISENSE_KEY,tsIntellisense?'on':'off')}catch{}},[tsIntellisense])
|
|
647
|
+
// Definition and reference jumps land in workbench tabs for every language, including LSP servers
|
|
648
|
+
// that are independent of Monaco's built-in TypeScript index.
|
|
649
|
+
useEffect(()=>{
|
|
650
|
+
if(!snapshot.visible||sessionId===undefined||workspace===undefined)return
|
|
651
|
+
let cancelled=false;let disposable:{dispose():void}|undefined
|
|
652
|
+
void loadMonaco().then(monaco=>{
|
|
653
|
+
if(cancelled)return
|
|
654
|
+
const cwd=workspace.cwd
|
|
655
|
+
disposable=monaco.editor.registerEditorOpener({openCodeEditor:(_source,resource,selection)=>{const path=workspacePathFromModel(cwd,resource.toString());if(path===undefined)return false;const position=selection===undefined?undefined:'startLineNumber' in selection?{line:selection.startLineNumber,column:selection.startColumn}:{line:selection.lineNumber,column:selection.column};openPath(path,position?.line,position?.column);return true}})
|
|
656
|
+
})
|
|
657
|
+
return()=>{cancelled=true;disposable?.dispose()}
|
|
658
|
+
},[snapshot.visible,sessionId,workspace,openPath])
|
|
628
659
|
useEffect(()=>{
|
|
629
660
|
if(!snapshot.visible||sessionId===undefined||workspace===undefined)return
|
|
630
661
|
// Not installed: never load Monaco just for this, only release an index a previous install built.
|
|
@@ -633,13 +664,12 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
|
|
|
633
664
|
void loadMonaco().then(monaco=>{
|
|
634
665
|
if(cancelled)return
|
|
635
666
|
setTsIndex({state:'loading'})
|
|
636
|
-
disposables.push(monaco.editor.registerEditorOpener({openCodeEditor:(_source,resource,selection)=>{const path=workspacePathFromModel(cwd,resource.toString());if(path===undefined)return false;const position=selection===undefined?undefined:'startLineNumber' in selection?{line:selection.startLineNumber,column:selection.startColumn}:{line:selection.lineNumber,column:selection.column};openPath(path,position?.line,position?.column);return true}}))
|
|
637
667
|
const changed=(event:Event)=>{const paths=(event as CustomEvent<string[]|undefined>).detail;if(Array.isArray(paths))void refreshIndexedFiles(monaco,sessionId,cwd,paths)}
|
|
638
668
|
window.addEventListener('dvw-files-changed',changed);disposables.push({dispose:()=>window.removeEventListener('dvw-files-changed',changed)})
|
|
639
669
|
void indexTypeScriptProject(monaco,sessionId,cwd).then(result=>{if(!cancelled)setTsIndex({state:'ready',...result})}).catch(error=>{if(!cancelled)setTsIndex({state:'error',error:error instanceof Error?error.message:String(error)})})
|
|
640
670
|
})
|
|
641
671
|
return()=>{cancelled=true;for(const disposable of disposables)disposable.dispose()}
|
|
642
|
-
},[snapshot.visible,sessionId,workspace,tsIntellisense
|
|
672
|
+
},[snapshot.visible,sessionId,workspace,tsIntellisense])
|
|
643
673
|
const openDiff=useCallback((path:string,diff:GitDiff,title?:string,diffSourcePath?:string)=>{const key=`diff:${path}`;const source=diffSourcePath??path;// Previewable documents open like VS Code's PDF/Office editors; history diffs keep the binary notice.
|
|
644
674
|
if(isPreviewDocumentPath(source)&&!path.startsWith('commit:')){openPath(source);return}const binary=isBinaryDiffPath(source);setTabs(rows=>[...rows.filter(row=>row.path!==key),{path:key,title,content:diff.diff,draft:diff.diff,revision:'',size:diff.diff.length,binary,tooLarge:false,dirty:false,diff:binary?undefined:diff,diffMode:'inline',diffSourcePath:source}]);setActive(key)},[openPath])
|
|
645
675
|
useEffect(()=>{const listener=(event:Event)=>void open((event as CustomEvent<string>).detail);window.addEventListener('dvw-open-workspace-path',listener);return()=>window.removeEventListener('dvw-open-workspace-path',listener)},[sessionId,tabs])
|
|
@@ -676,26 +706,53 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
|
|
|
676
706
|
const selectHeading=(index:number)=>{if(activeTab===undefined)return;setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab));window.setTimeout(()=>window.dispatchEvent(new CustomEvent('dvw-markdown-heading',{detail:index})),50)}
|
|
677
707
|
const suppressAttachmentDrop=(event:DragEvent)=>{if(!event.dataTransfer.types.includes('Files'))return;const target=event.target;const insideTree=target instanceof Element&&target.closest('.dvw-tree')!==null;if(!insideTree){event.preventDefault();event.stopPropagation()}}
|
|
678
708
|
if(!snapshot.visible)return null
|
|
679
|
-
if(sessionId===undefined)return <div className="dvw-overlay" data-dvw-theme={colorScheme}><WorkbenchTitlebar/><div className="dvw-empty" style={{gridColumn:'1/4'}}><div>请先选择或创建一个 DSH 会话。<br/><button onClick={()=>service.hide()}>返回 DSH</button></div></div></div>
|
|
709
|
+
if(sessionId===undefined)return <div className="dvw-overlay" data-dvw-theme={colorScheme} data-dvw-theme-name={theme}><WorkbenchTitlebar/><div className="dvw-empty" style={{gridColumn:'1/4'}}><div>请先选择或创建一个 DSH 会话。<br/><button onClick={()=>service.hide()}>返回 DSH</button></div></div></div>
|
|
680
710
|
const markdown=activeTab!==undefined&&!activeTab.path.startsWith('diff:')&&/\.md$/i.test(activeTab.path)
|
|
681
711
|
const diffMarkdown=activeTab?.diff!==undefined&&/\.md$/i.test(activeTab.diffSourcePath??'')
|
|
682
712
|
const markdownPreview=markdown||(diffMarkdown&&activeTab?.preview===true)
|
|
683
713
|
const headings=markdownPreview&&activeTab?markdownHeadings(activeTab.diff?.modified??activeTab.draft):[]
|
|
684
|
-
return <div className="dvw-overlay" data-dvw-theme={colorScheme} data-dvw-chat={chatVisible||undefined} data-dvw-primary-side={primarySideVisible||undefined} data-dvw-status={statusVisible||undefined} data-dvw-bottom-panel={snapshot.bottomPanel||undefined} style={{'--dvw-side-width':`${sideWidth}px`,'--dvw-chat-width':`${chatWidth}px`} as CSSProperties} role="dialog" aria-label="VS Code 工作台" onDragEnterCapture={suppressAttachmentDrop} onDragOverCapture={suppressAttachmentDrop} onDropCapture={suppressAttachmentDrop}>
|
|
714
|
+
return <div className="dvw-overlay" data-dvw-theme={colorScheme} data-dvw-theme-name={theme} data-dvw-chat={chatVisible||undefined} data-dvw-primary-side={primarySideVisible||undefined} data-dvw-status={statusVisible||undefined} data-dvw-bottom-panel={snapshot.bottomPanel||undefined} style={{'--dvw-side-width':`${sideWidth}px`,'--dvw-chat-width':`${chatWidth}px`} as CSSProperties} role="dialog" aria-label="VS Code 工作台" onDragEnterCapture={suppressAttachmentDrop} onDragOverCapture={suppressAttachmentDrop} onDropCapture={suppressAttachmentDrop}>
|
|
685
715
|
<WorkbenchTitlebar name={workspace?.name}/>
|
|
686
716
|
<DshConversationPanel visible={chatVisible&&!settingsOpen} width={chatWidth} statusHeight={statusVisible?30:6}/>
|
|
687
717
|
<DshConversationTheme visible={chatVisible} colorScheme={colorScheme} followDsh={false}/>
|
|
688
|
-
{chatVisible&&<aside className="dvw-chat"><PanelResizer className="dvw-chat-resizer" label="拖动调整 DSH 对话栏宽度" width={chatWidth} min={
|
|
689
|
-
{
|
|
690
|
-
{
|
|
691
|
-
|
|
718
|
+
{chatVisible&&<aside className="dvw-chat"><PanelResizer className="dvw-chat-resizer" label="拖动调整 DSH 对话栏宽度" width={chatWidth} min={DSH_CHAT_MIN_WIDTH} max={DSH_CHAT_MAX_WIDTH} defaultWidth={DSH_CHAT_DEFAULT_WIDTH} direction={-1} hideBelow={200} onHide={()=>setChatVisible(false)} onWidth={setChatWidth}/><header className="dvw-chat-head"><span><DshMark/> DSH</span><button className="dvw-icon" title="关闭 DSH 对话栏" onClick={()=>setChatVisible(false)}><Icon name="close"/></button></header><div className="dvw-chat-body"/></aside>}
|
|
719
|
+
{switcherOpen&&<WorkspaceSwitcher choices={switcherChoices} activeSessionId={sessionId} unavailable={navigation?.available===false} busy={switcherBusy} error={switcherError} theme={colorScheme} onCreate={()=>void createWorkspace()} onSwitch={switchSession} onOpenWorkspace={enterWorkspace} onClose={()=>setSwitcherOpen(false)}/>}
|
|
720
|
+
{settingsOpen&&<div className="dvw-settings-backdrop" role="presentation" onMouseDown={()=>setSettingsOpen(false)}>
|
|
721
|
+
<section className="dvw-settings" role="dialog" aria-modal="true" aria-label="工作台设置" onMouseDown={event=>event.stopPropagation()}>
|
|
722
|
+
<header><span><Icon name="settings"/> 工作台设置</span><button className="dvw-icon" title="关闭设置" onClick={()=>setSettingsOpen(false)}><Icon name="close"/></button></header>
|
|
723
|
+
<div className="dvw-settings-body" data-section={settingsSection}>
|
|
724
|
+
<nav className="dvw-settings-nav" aria-label="设置分类">{([['appearance','外观','color-mode'],['layout','布局','layout-sidebar-right'],['commit','提交信息','sparkle'],['language','语言智能','code']] as const).map(([value,label,icon])=><button key={value} data-active={settingsSection===value} onClick={()=>setSettingsSection(value)}><Icon name={icon}/><span>{label}</span></button>)}</nav>
|
|
725
|
+
<section><h3>外观</h3><label>颜色主题</label><div className="dvw-settings-themes">{([['dsh','跟随 DSH'],['dark','深色'],['light','浅色'],['white','纯白'],['eyecare','护眼']] as const).map(([value,label])=><button key={value} data-active={theme===value} onClick={()=>setTheme(value)}><span className={`dvw-theme-swatch ${value}`}/><span>{label}</span>{theme===value&&<Icon name="check"/>}</button>)}</div><FontSizeControl value={fontSize} onChange={setFontSize}/></section>
|
|
726
|
+
<section><h3>布局</h3><label className="dvw-setting-check"><input type="checkbox" checked={primarySideVisible} onChange={event=>setPrimarySideVisible(event.target.checked)}/><span>显示左侧视图</span></label><label className="dvw-setting-check"><input type="checkbox" checked={chatVisible} onChange={event=>setChatVisible(event.target.checked)}/><span>显示 DSH 对话栏</span></label><label className="dvw-setting-check"><input type="checkbox" checked={statusVisible} onChange={event=>setStatusVisible(event.target.checked)}/><span>显示底部状态栏</span></label><label className="dvw-setting-range"><span>左侧视图宽度</span><input type="range" min="210" max="640" step="1" value={Math.round(sideWidth)} onChange={event=>setSideWidth(Math.round(Number(event.target.value)))}/><output>{sideWidth}px</output></label><label className="dvw-setting-range"><span>DSH 对话栏宽度</span><input type="range" min={DSH_CHAT_MIN_WIDTH} max={DSH_CHAT_MAX_WIDTH} step="1" value={Math.round(chatWidth)} onChange={event=>setChatWidth(clampChatWidth(Number(event.target.value)))}/><output>{chatWidth}px</output></label><div className="dvw-setting-choice"><span>Markdown 默认视图</span><div className="dvw-setting-segment">{([['preview','预览'],['source','源码']] as const).map(([value,label])=><button key={value} data-active={markdownView===value} onClick={()=>setMarkdownView(value)}>{label}</button>)}</div></div></section>
|
|
727
|
+
<section><h3>提交信息</h3><div className="dvw-setting-choice"><span>详细程度</span><div className="dvw-setting-segment">{([['concise','简洁'],['standard','标准'],['detailed','详细']] as const).map(([value,label])=><button key={value} data-active={commitDetail===value} onClick={()=>setCommitDetail(value)}>{label}</button>)}</div></div><div className="dvw-setting-choice"><span>语言</span><div className="dvw-setting-segment">{([['auto','跟随最近提交'],['zh','中文'],['en','English']] as const).map(([value,label])=><button key={value} data-active={commitLanguage===value} onClick={()=>setCommitLanguage(value)}>{label}</button>)}</div></div><p className="dvw-setting-hint">详细模式会生成标题和 2–4 条说明;简洁模式限制标题长度,标准模式保持单行约定式提交。</p></section>
|
|
728
|
+
<section><h3>语言智能</h3>
|
|
729
|
+
<div className="dvw-setting-extension"><div><strong>TypeScript / JavaScript</strong><span>内置 · {tsIntellisense ? tsIndex.state === 'loading' ? '正在索引工作区…' : tsIndex.state === 'error' ? `索引失败:${tsIndex.error}` : tsIndex.state === 'ready' ? `已索引 ${tsIndex.files} 个文件${tsIndex.truncated ? '(已达上限)' : ''}` : '已安装,转到定义、查找引用、Peek、补全、签名帮助' : '点击安装'}</span></div><button className={tsIntellisense?undefined:'dvw-primary-button'} onClick={()=>setTsIntellisense(value=>!value)}>{tsIntellisense?'卸载':'安装'}</button></div>
|
|
730
|
+
{lspServers.map(server=><div className="dvw-setting-extension" key={server.id}>
|
|
731
|
+
<div>
|
|
732
|
+
<strong>{server.name}</strong>
|
|
733
|
+
<div className="dvw-lsp-status">
|
|
734
|
+
<span className="dvw-lsp-chip" data-installed={server.language===undefined||server.language.installed} data-action={server.language!==undefined&&!server.language.installed}>{server.language===undefined?'语言:无需安装':server.language.installed?'语言:已安装':'安装语言'}</span>
|
|
735
|
+
<span className="dvw-lsp-chip" data-installed={server.installed}>{server.installed?'LSP:已安装':'LSP:未安装'}</span>
|
|
736
|
+
{!server.installed&&server.hint!==undefined&&<span className="dvw-lsp-hint">{server.hint}</span>}
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
{server.language!==undefined&&!server.language.installed
|
|
740
|
+
? <button disabled title="先安装语言工具链,再安装 LSP">等待</button>
|
|
741
|
+
: server.installable&&<button disabled={lspBusy===server.id} className={server.installed?undefined:'dvw-primary-button'} onClick={()=>void changeLsp(server.id,server.installed?'uninstall':'install')}>{lspBusy===server.id?'处理中…':server.installed?'卸载 LSP':'安装 LSP'}</button>}
|
|
742
|
+
</div>)}
|
|
743
|
+
<p className="dvw-setting-hint">每种语言使用该生态里最活跃的语言服务器;能自动安装的会装进本插件的私有目录,需要手动安装的会分别检查语言工具链和服务器本身。</p>
|
|
744
|
+
{lspError&&<p className="dvw-setting-hint">{lspError}</p>}
|
|
745
|
+
</section>
|
|
746
|
+
</div>
|
|
747
|
+
</section>
|
|
748
|
+
</div>}
|
|
692
749
|
<ActivityBar
|
|
693
750
|
views={[
|
|
694
751
|
{id:'explorer',label:'资源管理器',icon:<Icon name="files"/>,active:primarySideVisible&&snapshot.view==='explorer',onActivate:()=>selectView('explorer')},
|
|
695
752
|
{id:'search',label:'搜索',icon:<Icon name="search"/>,active:primarySideVisible&&snapshot.view==='search',onActivate:()=>selectView('search')},
|
|
696
753
|
{id:'git',label:'源代码管理',icon:<Icon name="source-control"/>,active:primarySideVisible&&snapshot.view==='git',onActivate:()=>selectView('git')},
|
|
697
754
|
{id:'terminal',label:terminalAvailable?(snapshot.bottomPanel==='terminal'?'隐藏终端':'显示终端'):'终端适配器未安装',icon:<Icon name="terminal"/>,active:snapshot.bottomPanel==='terminal',disabled:!terminalAvailable,onActivate:()=>toggleTerminal(service)},
|
|
698
|
-
{id:'chat',label:chatVisible?'关闭 DSH 对话栏 (Ctrl+Alt+B)':'显示 DSH 对话栏 (Ctrl+Alt+B)',icon:<DshMark size={
|
|
755
|
+
{id:'chat',label:chatVisible?'关闭 DSH 对话栏 (Ctrl+Alt+B)':'显示 DSH 对话栏 (Ctrl+Alt+B)',icon:<DshMark size={22}/>,active:chatVisible,onActivate:()=>setChatVisible(value=>!value)},
|
|
699
756
|
]}
|
|
700
757
|
fixed={[{id:'workspaces',label:'打开工作区 / 切换会话',icon:<Icon name="folder-opened"/>,active:false,onActivate:openSwitcher}]}
|
|
701
758
|
bottom={[
|
|
@@ -704,7 +761,7 @@ export function Workbench({ service, sessionId, cwd, navigation }: { service: Vs
|
|
|
704
761
|
]}
|
|
705
762
|
order={activityOrder} hidden={activityHidden}
|
|
706
763
|
onOrderChange={setActivityOrder} onHiddenChange={setActivityHidden}/>
|
|
707
|
-
<aside className="dvw-side" data-side-width={sideWidth}>{snapshot.view==='explorer'&&workspace!==undefined&&<ExplorerView sessionId={sessionId} cwd={workspace.cwd} selected={active} onOpen={openPath} onAddToDsh={addEntryToDsh}/>} {snapshot.view==='search'&&<SearchView sessionId={sessionId} onOpen={openPath}/>} {snapshot.view==='git'&&<GitView sessionId={sessionId} onDiff={openDiff}/>}</aside>
|
|
764
|
+
<aside className="dvw-side" data-side-width={sideWidth}>{snapshot.view==='explorer'&&workspace!==undefined&&<ExplorerView sessionId={sessionId} cwd={workspace.cwd} selected={active} onOpen={openPath} onAddToDsh={addEntryToDsh}/>} {snapshot.view==='search'&&<SearchView sessionId={sessionId} onOpen={openPath}/>} {snapshot.view==='git'&&<GitView sessionId={sessionId} preferences={commitPreferences} onDiff={openDiff}/>}</aside>
|
|
708
765
|
<PanelResizer className="dvw-side-resizer" label="拖动调整源码管理侧边栏宽度" width={sideWidth} min={210} max={640} defaultWidth={210} direction={1} hideBelow={105} onHide={()=>setPrimarySideVisible(false)} onWidth={setSideWidth}/>
|
|
709
766
|
<main className="dvw-main" onDragOver={event => { if (draggedEntry?.kind !== 'file') return; event.preventDefault(); event.dataTransfer.dropEffect = 'copy' }} onDrop={event => { const entry = draggedEntry; if (entry?.kind !== 'file') return; event.preventDefault(); endEntryDrag(); openPath(entry.path) }}><div className="dvw-tabs" data-empty={tabs.length===0||undefined}><div className="dvw-tab-list" ref={tabListRef}>{tabs.map(tab=><button className="dvw-tab" data-active={tab.path===active} key={tab.path} title={tab.diffSourcePath??tab.path} onClick={()=>setActive(tab.path)} onAuxClick={event=>{if(event.button===1){event.preventDefault();close(tab)}}} onMouseDown={event=>{if(event.button===1)event.preventDefault()}} onContextMenu={event=>{event.preventDefault();setActive(tab.path);setTabMenu({x:event.clientX,y:event.clientY,path:tab.path})}}><FileIcon name={tabLabel(tab)} className="dvw-tab-icon"/><span className="dvw-tab-name">{tab.dirty?'● ':''}{tabLabel(tab)}</span><span className="dvw-tab-close" onClick={event=>{event.stopPropagation();close(tab)}}><Icon name="close"/></span></button>)}</div></div><div className="dvw-editor" data-outline={markdownPreview&&outlineVisible&&headings.length>0}>{error&&<div className="dvw-error"><button className="dvw-icon" onClick={()=>setError(undefined)}><Icon name="close"/></button>{error}</div>}{markdownPreview&&outlineVisible&&headings.length>0&&<MarkdownOutline headings={headings} onClose={()=>setOutlineVisible(false)} onHeading={selectHeading}/>} {markdown&&activeTab&&<div className="dvw-editor-toolbar"><button className="dvw-icon dvw-outline-toggle" data-active={outlineVisible} title={outlineVisible?'关闭 Markdown 大纲':'显示 Markdown 大纲'} onClick={()=>setOutlineVisible(value=>!value)}><Icon name="list-tree"/></button><button className="dvw-mode-button" data-active={!activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:false}:tab))}><Icon name="code"/> 源码</button><button className="dvw-mode-button" data-active={activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab))}><Icon name="preview"/> 预览</button></div>}{activeTab?.diff&&<div className="dvw-editor-toolbar dvw-diff-toolbar">{diffMarkdown&&<div className="dvw-diff-preview-switch"><button className="dvw-mode-button" data-diff-view="diff" data-active={!activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:false}:tab))}><Icon name="diff"/> 差异</button><button className="dvw-mode-button" data-diff-view="preview" data-active={activeTab.preview} onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,preview:true}:tab))}><Icon name="preview"/> 预览</button></div>}{!activeTab.preview&&<><button className="dvw-mode-button dvw-diff-mode-button" data-mode="inline" data-active={activeTab.diffMode!=='split'} title="内联视图" onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,diffMode:'inline'}:tab))}><Icon name="diff-single"/> 内联</button><button className="dvw-mode-button dvw-diff-mode-button" data-mode="split" data-active={activeTab.diffMode==='split'} title="并排视图" onClick={()=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,diffMode:'split'}:tab))}><Icon name="diff-sidebyside"/> 并排</button></>}</div>}<div className="dvw-editor-body" data-toolbar={markdown||activeTab?.diff!==undefined}>{activeTab===undefined?<div className="dvw-empty"><div><strong>DSH Workbench</strong><br/>从资源管理器或搜索结果打开文件</div></div>:activeTab.diff?diffMarkdown&&activeTab.preview?<MarkdownPreview source={activeTab.diff.modified} sessionId={sessionId} path={activeTab.diffSourcePath??''}/>:<MonacoDiffEditor uri={`${workspace?.cwd??''}/${activeTab.path}`} original={activeTab.diff.original} modified={activeTab.diff.modified} mode={activeTab.diffMode??'inline'} theme={colorScheme} fontSize={fontSize}/>:activeTab.imageUrl?<div className="dvw-image-preview"><img src={activeTab.imageUrl} alt={activeTab.path}/><span>{activeTab.path}</span></div>:activeTab.documentPreview&&activeTab.documentUrl?<DocumentPreview kind={activeTab.documentPreview} src={activeTab.documentUrl} path={activeTab.path}/>:markdown&&activeTab.preview?<MarkdownPreview source={activeTab.draft} sessionId={sessionId} path={activeTab.path}/>:activeTab.binary||activeTab.tooLarge?<div className="dvw-message">{activeTab.binary?'二进制文件无法在文本编辑器或差异视图中打开':`文件超过 5 MiB:${activeTab.path}`}{activeTab.binary&&activeTab.diffSourcePath!==undefined&&!activeTab.path.startsWith('diff:commit:')&&<button onClick={()=>void openPath(activeTab.diffSourcePath!)}>打开文件</button>}</div>:activeTab.loaded===false?<div className="dvw-message">正在加载文件…</div>:<MonacoEditor uri={`${workspace?.cwd??''}/${activeTab.path}`} onAddToDsh={selection=>addToDsh(dshReference(activeTab.path,{selection}))} value={activeTab.draft} theme={colorScheme} fontSize={fontSize} line={activeTab.line} column={activeTab.column} highlight={activeTab.highlight} onChange={draft=>setTabs(rows=>rows.map(tab=>tab.path===activeTab.path?{...tab,draft,dirty:draft!==tab.content}:tab))} onSave={()=>void save(activeTab)}/>}</div></div>{snapshot.bottomPanel===undefined&&<div className="dvw-bottom-reveal" title="向上拖动打开终端" onPointerDown={event=>{if(event.button!==0)return;event.preventDefault();service.toggleBottomPanel('terminal');const statusHeight=statusVisible?24:0;const heightAt=(clientY:number)=>Math.max(120,Math.min(window.innerHeight-160,window.innerHeight-clientY-statusHeight));const move=(e:PointerEvent)=>setPanelHeight(heightAt(e.clientY));const up=()=>{window.removeEventListener('pointermove',move);window.removeEventListener('pointerup',up)};window.addEventListener('pointermove',move);window.addEventListener('pointerup',up)}}/>}</main>
|
|
710
767
|
{tabMenu&&menuTab&&<div className="dvw-context-menu dvw-tab-menu" role="menu" style={{left:Math.min(tabMenu.x,window.innerWidth-240),top:Math.min(tabMenu.y,window.innerHeight-180)}} onPointerDown={event=>event.stopPropagation()}>
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { Icon } from './Icon.tsx'
|
|
3
4
|
import type { WorkspaceChoice } from './dshNavigation.ts'
|
|
4
5
|
|
|
5
6
|
/** Centered switcher: existing workspaces, their sessions, and a new-workspace action. */
|
|
6
|
-
export function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy, error, onCreate, onSwitch, onOpenWorkspace, onClose }: {
|
|
7
|
+
export function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy, error, theme, onCreate, onSwitch, onOpenWorkspace, onClose }: {
|
|
7
8
|
choices: WorkspaceChoice[]
|
|
8
9
|
activeSessionId?: string
|
|
9
10
|
unavailable: boolean
|
|
10
11
|
busy: boolean
|
|
11
12
|
error?: string
|
|
13
|
+
theme?: 'dark' | 'light' | 'white' | 'eyecare'
|
|
12
14
|
onCreate(): void
|
|
13
15
|
onSwitch(sessionId: string): void
|
|
14
16
|
onOpenWorkspace(workspaceId: string): void
|
|
@@ -21,7 +23,8 @@ export function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy,
|
|
|
21
23
|
window.addEventListener('keydown', escape)
|
|
22
24
|
return () => window.removeEventListener('keydown', escape)
|
|
23
25
|
}, [onClose])
|
|
24
|
-
return <div className="dvw-
|
|
26
|
+
return createPortal(<div className="dvw-overlay dvw-workspace-layer" data-dvw-theme={theme}>
|
|
27
|
+
<div className="dvw-workspace-backdrop" role="presentation" onMouseDown={onClose}>
|
|
25
28
|
<section className="dvw-workspace-switcher" role="dialog" aria-modal="true" aria-label="打开工作区" onMouseDown={event => event.stopPropagation()}>
|
|
26
29
|
<header><h2>打开工作区</h2><button className="dvw-icon" title="关闭" onClick={onClose}><Icon name="close"/></button></header>
|
|
27
30
|
<div className="dvw-workspace-list">
|
|
@@ -49,5 +52,6 @@ export function WorkspaceSwitcher({ choices, activeSessionId, unavailable, busy,
|
|
|
49
52
|
{error !== undefined && <p className="dvw-workspace-error">{error}</p>}
|
|
50
53
|
<footer><button className="dvw-primary-button" disabled={busy} onClick={onCreate}>{busy ? '正在打开…' : '新建工作区…'}</button><button onClick={onClose}>取消</button></footer>
|
|
51
54
|
</section>
|
|
52
|
-
|
|
55
|
+
</div>
|
|
56
|
+
</div>, document.body)
|
|
53
57
|
}
|