dsh-vscode-mode 0.14.5 → 0.14.7
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.md +6 -4
- package/lib/client.js +969 -166
- package/lib/client.js.map +1 -1
- package/lib/index.js +980 -19
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/capture.ts +4 -1
- package/src/client/events.ts +11 -5
- package/src/client/rpc.ts +10 -0
- package/src/client/sidebar/menuItems.ts +7 -6
- package/src/client/sidebar/panels/FileExplorer.ts +89 -72
- package/src/client/sidebar/panels/explorerView.ts +165 -0
- package/src/client/sidebar/panels/useExplorerSvn.ts +141 -0
- package/src/client/state/editorFindState.ts +102 -0
- package/src/client/state/editorViewState.ts +56 -0
- package/src/client/styles/editor.css +12 -0
- package/src/client/svnNotice.ts +24 -0
- package/src/client/ui/EditorView.ts +27 -50
- package/src/client/ui/SvnOverlay.ts +24 -0
- package/src/dap/manager.ts +65 -17
- package/src/model.ts +112 -1
- package/src/rpc.ts +2 -0
- package/src/shared/rpc.ts +6 -1
- package/src/shared/svn.ts +55 -0
- package/src/shared/svnExplorer.ts +97 -0
- package/src/store.ts +36 -1
- package/src/svn.ts +108 -6
- package/src/svnExplorer.ts +56 -0
- package/src/svnExplorerCache.ts +304 -0
- package/src/svnStatusStream.ts +135 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** Explorer lifecycle: local refresh is offline, remote checks are explicit. @author ddj 2026年10月09号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { bindSvnScope } from '../../svnNotice.js'
|
|
4
|
+
import { rpc } from '../../rpc.js'
|
|
5
|
+
import { explorerPathOf, remoteIndexOf } from '../../../shared/svnExplorer.js'
|
|
6
|
+
import type { SvnViewRequest, SvnViewRow } from '../../../shared/svnExplorer.js'
|
|
7
|
+
import { ExplorerViewClient } from './explorerView.js'
|
|
8
|
+
import type { ExplorerNotice, ExplorerView } from './explorerView.js'
|
|
9
|
+
import type { SvnRemoteOutdatedEntry } from '../../../shared/svn.js'
|
|
10
|
+
import type { SidebarCtx } from '../types.js'
|
|
11
|
+
|
|
12
|
+
interface LocalSnapshot { view: ExplorerView; key: string }
|
|
13
|
+
|
|
14
|
+
// --region Explicit repository check
|
|
15
|
+
/** @private @author ddj 2026年10月09号 @param ctx Sidebar context. @param local Confirmed snapshot. @returns Expiring read-only remote evidence and check action. */
|
|
16
|
+
function useRemoteCheck(ctx: SidebarCtx | undefined, local: LocalSnapshot | null, resetToken: number, scope: string) {
|
|
17
|
+
const [remote, setRemote] = React.useState<Map<string, number> | null>(null)
|
|
18
|
+
const [message, setMessage] = React.useState('')
|
|
19
|
+
const [busy, setBusy] = React.useState(false)
|
|
20
|
+
const control = React.useRef<AbortController | null>(null)
|
|
21
|
+
const epoch = React.useRef(0)
|
|
22
|
+
const expiry = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
23
|
+
|
|
24
|
+
/** @private @author ddj 2026年10月09号 Cancel stale request/timer without updating an unmounted component. */
|
|
25
|
+
function cancel() {
|
|
26
|
+
epoch.current++
|
|
27
|
+
control.current?.abort()
|
|
28
|
+
control.current = null
|
|
29
|
+
if (expiry.current) clearTimeout(expiry.current)
|
|
30
|
+
expiry.current = null
|
|
31
|
+
}
|
|
32
|
+
/** @private @author ddj 2026年10月09号 @returns Cleanup; scope/local changes invalidate repository evidence. */
|
|
33
|
+
function reset() { cancel(); setRemote(null); setMessage(''); setBusy(false); return cancel }
|
|
34
|
+
React.useEffect(reset, [scope, resetToken])
|
|
35
|
+
|
|
36
|
+
/** @public @author ddj 2026年10月09号 Contact repository once; failures/expiry never imply up-to-date. */
|
|
37
|
+
async function checkRemote() {
|
|
38
|
+
if (!local || control.current) return
|
|
39
|
+
cancel()
|
|
40
|
+
setRemote(null)
|
|
41
|
+
const controller = new AbortController()
|
|
42
|
+
control.current = controller
|
|
43
|
+
const seq = epoch.current
|
|
44
|
+
setBusy(true)
|
|
45
|
+
setMessage('正在检查仓库更新(只读)…')
|
|
46
|
+
try {
|
|
47
|
+
const out = await rpc('svn.remoteStatus', { sessionId: ctx?.sessionId, path: '' }, controller.signal)
|
|
48
|
+
if (controller.signal.aborted || seq !== epoch.current) return
|
|
49
|
+
if (!out.ok) { setMessage(out.error); return }
|
|
50
|
+
/** @private @author ddj 2026年10月09号 @param entry WC-relative change. @returns Session subset. */
|
|
51
|
+
function scopeEntry(entry: SvnRemoteOutdatedEntry): SvnRemoteOutdatedEntry[] {
|
|
52
|
+
const path = explorerPathOf(entry.path, local!.view.prefix)
|
|
53
|
+
return path === null ? [] : [{ ...entry, path }]
|
|
54
|
+
}
|
|
55
|
+
const scoped = out.outdated.flatMap(scopeEntry)
|
|
56
|
+
setRemote(remoteIndexOf(scoped))
|
|
57
|
+
setMessage('检查于 ' + new Date().toLocaleTimeString() + ':' + scoped.length + ' 项可更新'
|
|
58
|
+
+ (out.truncated ? '(结果不完整)' : '') + (out.againstRev === null ? '' : ',仓库 r' + out.againstRev))
|
|
59
|
+
/** @private @author ddj 2026年10月09号 Expire evidence; no automatic repository recheck. */
|
|
60
|
+
function expire() { setRemote(null); setMessage('远端检查已过期,请重新检查仓库更新') }
|
|
61
|
+
expiry.current = setTimeout(expire, 60_000)
|
|
62
|
+
} catch (error) { if (!controller.signal.aborted && seq === epoch.current) setMessage(String(error)) }
|
|
63
|
+
finally { if (seq === epoch.current) { control.current = null; setBusy(false) } }
|
|
64
|
+
}
|
|
65
|
+
return { remote, message, busy, checkRemote }
|
|
66
|
+
}
|
|
67
|
+
// --endregion
|
|
68
|
+
|
|
69
|
+
/** @private @author ddj 2026年10月10号 @param view Compact metadata. @returns Honest refresh/failure/last-confirmed status. */
|
|
70
|
+
function localMessage(view?: ExplorerView): string {
|
|
71
|
+
if (!view) return '正在读取本地 SVN 状态…'
|
|
72
|
+
const time = view.confirmedAt ? ';最后确认 ' + new Date(view.confirmedAt).toLocaleTimeString() : ';尚未确认'
|
|
73
|
+
if (view.error) return 'SVN 状态陈旧:' + view.error + time
|
|
74
|
+
return (view.refreshing ? 'SVN 状态刷新中' : view.complete ? 'SVN 本地状态已确认' : 'SVN 状态未完整确认') + time
|
|
75
|
+
}
|
|
76
|
+
// --region Offline local status
|
|
77
|
+
/** @public @author ddj 2026年10月09号 @param ctx Sidebar context. @returns Scope-bound local/remote icons; unknown entries never show green. */
|
|
78
|
+
export function useExplorerSvn(ctx: SidebarCtx | undefined, rows: readonly SvnViewRow[] = []) {
|
|
79
|
+
const [local, setLocal] = React.useState<LocalSnapshot | null>(null)
|
|
80
|
+
const [remoteToken, setRemoteToken] = React.useState(0)
|
|
81
|
+
const owner = React.useRef<ExplorerViewClient | null>(null)
|
|
82
|
+
const ready = ctx?.svn?.managed === true && ctx?.svn?.svnCli === true
|
|
83
|
+
const scope = ctx?.scope ?? ctx?.sessionId ?? ''
|
|
84
|
+
const key = JSON.stringify([scope, ready])
|
|
85
|
+
const snapshot = local?.key === key ? local : null
|
|
86
|
+
const remote = useRemoteCheck(ctx, snapshot, remoteToken, key)
|
|
87
|
+
|
|
88
|
+
/** @private @author ddj 2026年10月10号 @returns Scope owner cleanup; late results cannot populate a new workspace. */
|
|
89
|
+
function mount() {
|
|
90
|
+
setLocal(null)
|
|
91
|
+
bindSvnScope(ctx?.sessionId, scope)
|
|
92
|
+
if (!ready) return
|
|
93
|
+
/** @private @author ddj 2026年10月10号 @param view Compact state. Publish only this mounted scope. */
|
|
94
|
+
function publish(view: ExplorerView) { setLocal({ view, key }) }
|
|
95
|
+
/** @private @author ddj 2026年10月10号 Invalidate explicit remote evidence after non-text changes. */
|
|
96
|
+
function dropRemote() { setRemoteToken((value) => value + 1) }
|
|
97
|
+
/** @private @author ddj 2026年10月10号 @param args Compact projection. @param signal UI fetch lifetime. @returns Authenticated response. */
|
|
98
|
+
function fetch(args: SvnViewRequest, signal: AbortSignal) { return rpc('svn.explorerView', args, signal) }
|
|
99
|
+
const client = new ExplorerViewClient(scope, fetch, publish, dropRemote)
|
|
100
|
+
client.sessionId = ctx?.sessionId
|
|
101
|
+
owner.current = client
|
|
102
|
+
client.setRows(rows)
|
|
103
|
+
client.visibility(document.visibilityState !== 'hidden')
|
|
104
|
+
return () => { client.dispose(); if (owner.current === client) owner.current = null }
|
|
105
|
+
}
|
|
106
|
+
React.useEffect(mount, [key])
|
|
107
|
+
/** @private @author ddj 2026年10月10号 Row expansion/session switch only reprojects; Host owns shared workspace cache. */
|
|
108
|
+
function project() {
|
|
109
|
+
bindSvnScope(ctx?.sessionId, scope)
|
|
110
|
+
if (owner.current) { owner.current.sessionId = ctx?.sessionId; owner.current.setRows(rows) }
|
|
111
|
+
}
|
|
112
|
+
React.useEffect(project, [rows, ctx?.sessionId])
|
|
113
|
+
/** @private @author ddj 2026年10月10号 Unknown changes-list origins calibrate once; known event bursts are already scheduled. */
|
|
114
|
+
function changes() { owner.current?.changes() }
|
|
115
|
+
React.useEffect(changes, [ctx?.svnChanges])
|
|
116
|
+
|
|
117
|
+
/** @private @author ddj 2026年10月10号 @returns Scoped notices/focus/visibility listener cleanup. */
|
|
118
|
+
function listen() {
|
|
119
|
+
/** @private @author ddj 2026年10月10号 @param event Pathful or legacy notification. Legacy unknown scope safely falls back to full. */
|
|
120
|
+
function notice(event: Event) {
|
|
121
|
+
const detail = (event as CustomEvent<ExplorerNotice & { path?: string }>).detail ?? {}
|
|
122
|
+
owner.current?.notify({ ...detail, paths: detail.paths ?? (detail.path ? [detail.path] : undefined) })
|
|
123
|
+
}
|
|
124
|
+
/** @private @author ddj 2026年10月10号 Trigger only offline calibration; focus and visibility coalesce. */
|
|
125
|
+
function visibility() { owner.current?.visibility(document.visibilityState !== 'hidden') }
|
|
126
|
+
window.addEventListener('edrv:refresh', notice)
|
|
127
|
+
window.addEventListener('edrv:file-changed', notice)
|
|
128
|
+
window.addEventListener('focus', visibility)
|
|
129
|
+
document.addEventListener('visibilitychange', visibility)
|
|
130
|
+
return () => {
|
|
131
|
+
window.removeEventListener('edrv:refresh', notice); window.removeEventListener('edrv:file-changed', notice)
|
|
132
|
+
window.removeEventListener('focus', visibility); document.removeEventListener('visibilitychange', visibility)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
React.useEffect(listen, [])
|
|
136
|
+
/** @public @author ddj 2026年10月10号 Request an explicit offline full scan. */
|
|
137
|
+
function refreshLocal() { owner.current?.refresh() }
|
|
138
|
+
return { ...remote, local: snapshot?.view.marks ?? null, remote: snapshot ? remote.remote : null,
|
|
139
|
+
message: localMessage(snapshot?.view) + (remote.message ? ';' + remote.message : ''), canCheck: ready && snapshot !== null, refreshLocal }
|
|
140
|
+
}
|
|
141
|
+
// --endregion
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Workspace-local Ctrl+F state bridge; ddj 2026年10月09号. */
|
|
2
|
+
export interface FindSnapshot {
|
|
3
|
+
searchString: string
|
|
4
|
+
isRevealed: boolean
|
|
5
|
+
matchCase: boolean
|
|
6
|
+
wholeWord: boolean
|
|
7
|
+
isRegex: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FindState extends FindSnapshot {
|
|
11
|
+
change(value: Partial<FindSnapshot> & { searchScope?: null }, moveCursor: boolean): void
|
|
12
|
+
onFindReplaceStateChange(listener: () => void): { dispose(): void }
|
|
13
|
+
}
|
|
14
|
+
interface FindController {
|
|
15
|
+
getState(): FindState
|
|
16
|
+
start(options: Record<string, unknown>, state: FindSnapshot & { searchScope: null }): Promise<unknown>
|
|
17
|
+
}
|
|
18
|
+
export interface FindEditor {
|
|
19
|
+
getContribution(id: string): unknown
|
|
20
|
+
}
|
|
21
|
+
const snapshots = new Map<string, FindSnapshot>()
|
|
22
|
+
|
|
23
|
+
/** @private @author ddj 2026年10月09号 @param state Native find state. @returns Only fields safe to share across files. */
|
|
24
|
+
function snapshotOf(state: FindState): FindSnapshot {
|
|
25
|
+
return {
|
|
26
|
+
searchString: state.searchString, isRevealed: state.isRevealed,
|
|
27
|
+
matchCase: state.matchCase, wholeWord: state.wholeWord, isRegex: state.isRegex,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @private @author ddj 2026年10月09号 @param editor Monaco editor. @returns Supported controller, or null for native-only fallback. */
|
|
32
|
+
function controllerOf(editor: FindEditor): FindController | null {
|
|
33
|
+
try {
|
|
34
|
+
const controller = editor.getContribution('editor.contrib.findController') as FindController | null
|
|
35
|
+
const state = controller?.getState?.()
|
|
36
|
+
return state && typeof state.change === 'function'
|
|
37
|
+
&& typeof state.onFindReplaceStateChange === 'function'
|
|
38
|
+
&& typeof controller?.start === 'function' ? controller : null
|
|
39
|
+
} catch { return null }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @private @author ddj 2026年10月09号 @param controller Optional native controller. @param listener State observer. @returns Disposable subscription, or null when the internal API is incompatible. */
|
|
43
|
+
function listenFind(controller: FindController | null, listener: () => void): { dispose(): void } | null {
|
|
44
|
+
try { return controller?.getState().onFindReplaceStateChange(listener) ?? null } catch { return null }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @private @author ddj 2026年10月09号 @param controller Native controller. @param saved Workspace snapshot. @returns Completion when revealing; never focuses or seeds from selection. */
|
|
48
|
+
function applyFind(controller: FindController, saved: FindSnapshot): Promise<unknown> | void {
|
|
49
|
+
const value = { ...saved, searchScope: null }
|
|
50
|
+
if (!saved.isRevealed) { controller.getState().change(value, false); return }
|
|
51
|
+
return controller.start({
|
|
52
|
+
forceRevealReplace: false, seedSearchStringFromSelection: 'none',
|
|
53
|
+
seedSearchStringFromNonEmptySelection: false, seedSearchStringFromGlobalClipboard: false,
|
|
54
|
+
shouldFocus: 0, shouldAnimate: false, updateSearchScope: false, loop: true,
|
|
55
|
+
}, value)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @public @author ddj 2026年10月09号 @param editor Monaco editor. @returns Model-switch bridge; dispose releases the native listener. */
|
|
59
|
+
export function createFindBridge(editor: FindEditor) {
|
|
60
|
+
const controller = controllerOf(editor)
|
|
61
|
+
let scope: string | null = null
|
|
62
|
+
let paused = true
|
|
63
|
+
let disposed = false
|
|
64
|
+
|
|
65
|
+
/** @private @author ddj 2026年10月09号 Capture only user changes, not model-reset or restoration events. */
|
|
66
|
+
function capture(): void {
|
|
67
|
+
if (disposed || paused || !scope || !controller) return
|
|
68
|
+
try {
|
|
69
|
+
const saved = snapshotOf(controller.getState())
|
|
70
|
+
snapshots.delete(scope)
|
|
71
|
+
snapshots.set(scope, saved)
|
|
72
|
+
if (snapshots.size > 20) snapshots.delete(snapshots.keys().next().value!)
|
|
73
|
+
} catch { /* A disposed contribution must not interrupt view capture. */ }
|
|
74
|
+
}
|
|
75
|
+
const listener = listenFind(controller, capture)
|
|
76
|
+
|
|
77
|
+
/** @public @author ddj 2026年10月09号 Save before setModel; suppress Monaco's per-model option resets. */
|
|
78
|
+
function pause(): void { capture(); paused = true }
|
|
79
|
+
|
|
80
|
+
/** @public @author ddj 2026年10月09号 @param nextScope Workspace owning the bound model. Restore with moveCursor=false; no per-file range or match index. */
|
|
81
|
+
function resume(nextScope: string): void {
|
|
82
|
+
if (disposed || !controller) return
|
|
83
|
+
scope = nextScope
|
|
84
|
+
const saved = snapshots.get(scope) ?? {
|
|
85
|
+
searchString: '', isRevealed: false, matchCase: false, wholeWord: false, isRegex: false,
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const result = applyFind(controller, saved)
|
|
89
|
+
result?.catch(() => {}) // Unsupported internal API leaves native Ctrl+F usable.
|
|
90
|
+
} catch { /* Internal contribution compatibility must not break the editor. */ }
|
|
91
|
+
paused = false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @public @author ddj 2026年10月09号 Save final query and remove listener before editor destruction. */
|
|
95
|
+
function dispose(): void {
|
|
96
|
+
if (disposed) return
|
|
97
|
+
capture()
|
|
98
|
+
disposed = true
|
|
99
|
+
try { listener?.dispose() } catch { /* Already disposed by native editor cleanup. */ }
|
|
100
|
+
}
|
|
101
|
+
return { pause, resume, dispose }
|
|
102
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Model-owned view/find lifecycle; ddj 2026年10月09号. */
|
|
2
|
+
import { createFindBridge, type FindEditor } from './editorFindState.js'
|
|
3
|
+
import { upsertViewState, viewStatesLoad, viewStatesSave } from './viewStateCache.js'
|
|
4
|
+
|
|
5
|
+
export interface ViewEditor extends FindEditor {
|
|
6
|
+
getModel(): unknown
|
|
7
|
+
setModel(model: unknown): void
|
|
8
|
+
saveViewState(): unknown
|
|
9
|
+
restoreViewState(state: unknown): void
|
|
10
|
+
dispose(): void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @public @author ddj 2026年10月09号 @param editor Monaco instance. @returns Keeper owning the model's scope/path, independent of React's active closure. */
|
|
14
|
+
export function createViewKeeper(editor: ViewEditor) {
|
|
15
|
+
const find = createFindBridge(editor)
|
|
16
|
+
let bound: { scope: string; path: string; model: unknown } | null = null
|
|
17
|
+
let disposed = false
|
|
18
|
+
|
|
19
|
+
/** @public @author ddj 2026年10月09号 @param path Optional expected path; mismatches never write a snapshot under the wrong file. Persist viewport even if cursor never moved. */
|
|
20
|
+
function save(path?: string): void {
|
|
21
|
+
if (disposed || !bound || (path && path !== bound.path)) return
|
|
22
|
+
try {
|
|
23
|
+
if (editor.getModel() !== bound.model) return
|
|
24
|
+
const states = viewStatesLoad(bound.scope)
|
|
25
|
+
const next = upsertViewState(states, bound.path, editor.saveViewState())
|
|
26
|
+
if (next !== states) viewStatesSave(bound.scope, next)
|
|
27
|
+
} catch { /* A missing/disposed model must not overwrite a valid snapshot. */ }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @public @author ddj 2026年10月09号 @param scope Model workspace. @param path Cache path. @param model Ready text model. Restore only on identity change, before explicit navigation effects. */
|
|
31
|
+
function bind(scope: string, path: string, model: unknown): void {
|
|
32
|
+
if (disposed || !path || !model) return
|
|
33
|
+
if (bound?.scope === scope && bound.path === path && bound.model === model
|
|
34
|
+
&& editor.getModel() === model) return
|
|
35
|
+
save()
|
|
36
|
+
find.pause()
|
|
37
|
+
if (editor.getModel() !== model) editor.setModel(model)
|
|
38
|
+
bound = { scope, path, model }
|
|
39
|
+
const state = viewStatesLoad(scope)[path]
|
|
40
|
+
if (state) {
|
|
41
|
+
try { editor.restoreViewState(state) } catch { /* Stale fold ranges may be invalid. */ }
|
|
42
|
+
}
|
|
43
|
+
find.resume(scope)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @public @author ddj 2026年10月09号 Save before dispose/ref clearing; repeated cleanup is a no-op. */
|
|
47
|
+
function dispose(): void {
|
|
48
|
+
if (disposed) return
|
|
49
|
+
save()
|
|
50
|
+
find.dispose()
|
|
51
|
+
disposed = true
|
|
52
|
+
bound = null
|
|
53
|
+
editor.dispose()
|
|
54
|
+
}
|
|
55
|
+
return { save, bind, dispose }
|
|
56
|
+
}
|
|
@@ -277,6 +277,16 @@
|
|
|
277
277
|
/* 树行图标:官方原语(文件夹/文件类型),尺寸与行高对齐 @author ddj 2026-09-10 */
|
|
278
278
|
[data-edrv-view] .edrv-tree-icon { flex-shrink: 0; display: inline-flex; align-items: center; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
279
279
|
[data-edrv-view] .edrv-tree-row.edrv-tree-active .edrv-tree-icon { color: var(--dsw-alias-label-secondary, #52606d); }
|
|
280
|
+
/* SVN overlays: official file icon remains visible, independent local/remote signs. ddj 2026-10-09 */
|
|
281
|
+
[data-edrv-view] .edrv-tree-iconbox { position: relative; display: inline-flex; width: 20px; height: 20px; align-items: center; flex-shrink: 0; }
|
|
282
|
+
[data-edrv-view] .edrv-svn-overlay { position: absolute; inset: 0; }
|
|
283
|
+
[data-edrv-view] .edrv-svn-local { position: absolute; right: -1px; bottom: -2px; display: flex; border-radius: 50%; box-shadow: 0 0 0 1px var(--dsw-alias-bg-layer-1, #fff); }
|
|
284
|
+
[data-edrv-view] .edrv-svn-icon-clean { color: #16853b; }
|
|
285
|
+
[data-edrv-view] .edrv-svn-icon-modified, [data-edrv-view] .edrv-svn-icon-deleted { color: #d12e35; }
|
|
286
|
+
[data-edrv-view] .edrv-svn-icon-conflict { color: #c27300; }
|
|
287
|
+
[data-edrv-view] .edrv-svn-icon-added { color: #2465c9; }
|
|
288
|
+
[data-edrv-view] .edrv-svn-icon-unversioned, [data-edrv-view] .edrv-svn-icon-ignored, [data-edrv-view] .edrv-svn-icon-external { color: #75808c; }
|
|
289
|
+
[data-edrv-view] .edrv-svn-remote { position: absolute; right: -3px; top: -4px; color: #2465c9; background: var(--dsw-alias-bg-layer-1, #fff); border-radius: 3px; font-size: 11px; font-weight: 900; line-height: 12px; }
|
|
280
290
|
[data-edrv-view] .edrv-rail-badge { position: absolute; top: 2px; right: 2px; min-width: 14px; height: 14px; padding: 0 3px; border-radius: 999px; background: var(--dsw-alias-brand-primary, #0f9d58); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; }
|
|
281
291
|
[data-edrv-view] .edrv-side-body { flex: 1; min-width: 0; min-height: 0; display: flex; overflow: hidden; }
|
|
282
292
|
[data-edrv-view] .edrv-side-panel { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; position: relative; }
|
|
@@ -298,6 +308,8 @@
|
|
|
298
308
|
[data-edrv-view] .edrv-tree-name.edrv-tree-dim { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
299
309
|
[data-edrv-view] .edrv-tree-badge { min-width: 14px; height: 14px; padding: 0 4px; border-radius: 999px; background: var(--dsw-alias-state-warn-primary, #d97706); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; flex-shrink: 0; }
|
|
300
310
|
[data-edrv-view] .edrv-tree-loading { height: 22px; line-height: 22px; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; }
|
|
311
|
+
/* SVN 状态独立占位:换行不覆盖文件树,长错误在栏内滚动。@author ddj 2026-10-12 */
|
|
312
|
+
[data-edrv-view] .edrv-tree-status { flex: 0 0 auto; min-width: 0; max-height: 96px; overflow: auto; padding: 4px 8px; box-sizing: border-box; line-height: 18px; white-space: normal; overflow-wrap: anywhere; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; }
|
|
301
313
|
[data-edrv-view] .edrv-tree-error { display: flex; align-items: center; gap: 8px; margin: 4px 2px 6px; padding: 6px 8px; border: 1px solid var(--dsw-alias-state-error-primary, rgba(217,83,79,.4)); border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217,83,79,.12)); color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
|
|
302
314
|
/* 大纲面板:kind 字形 / detail / 行号 */
|
|
303
315
|
[data-edrv-view] .edrv-outline-kind { display: inline-flex; align-items: center; justify-content: center; width: 14px; flex-shrink: 0; font-size: 12px; font-weight: 600; text-align: center; }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** SVN mutation notifications, independent of UI stores. @author ddj 2026年10月10号 */
|
|
2
|
+
import type { RpcMethod, RpcRequestMap } from '../shared/rpc.js'
|
|
3
|
+
let sequence = 0
|
|
4
|
+
const scopes = new Map<string, string>()
|
|
5
|
+
/** @public @author ddj 2026年10月10号 @param sessionId Active session. @param scope Canonical client workspace. Record bounded routing hints; unknown sessions invalidate globally, never misapply relative paths. */
|
|
6
|
+
export function bindSvnScope(sessionId: string | undefined, scope: string): void {
|
|
7
|
+
if (!sessionId) return
|
|
8
|
+
scopes.delete(sessionId); scopes.set(sessionId, scope)
|
|
9
|
+
if (scopes.size > 512) scopes.delete(scopes.keys().next().value!)
|
|
10
|
+
}
|
|
11
|
+
const WRITES = new Set<RpcMethod>(['svn.add', 'svn.revert', 'svn.ignore', 'svn.cleanup', 'svn.update', 'svn.tortoise'])
|
|
12
|
+
/** @public @author ddj 2026年10月10号 @param method RPC operation. @returns Whether local evidence may change (launching a writable native dialog counts, not its completion). */
|
|
13
|
+
export function isSvnWrite(method: RpcMethod): boolean { return WRITES.has(method) }
|
|
14
|
+
/** @public @author ddj 2026年10月10号 @param method Operation. @param args Typed request. @param ok Completed successfully. Notify even failed/partially applied operations; no repository access. */
|
|
15
|
+
export function svnNotice<M extends RpcMethod>(method: M, args: RpcRequestMap[M], ok: boolean): void {
|
|
16
|
+
if (!isSvnWrite(method) || typeof window === 'undefined') return
|
|
17
|
+
const request = args as { sessionId?: string; path?: string; paths?: string[]; action?: string; items?: { dir: string }[] }
|
|
18
|
+
if (method === 'svn.tortoise' && !['commit', 'revert'].includes(request.action ?? '')) return
|
|
19
|
+
const scope = request.sessionId ? scopes.get(request.sessionId) : undefined
|
|
20
|
+
let paths: string[] | undefined
|
|
21
|
+
if (scope && ok && method !== 'svn.cleanup') paths = request.paths ?? request.items?.map((item) => item.dir) ?? (request.path ? [request.path] : undefined)
|
|
22
|
+
window.dispatchEvent(new CustomEvent('edrv:refresh', { detail: { scope, paths,
|
|
23
|
+
kind: 'svn', eventId: 'svn-write-' + Date.now() + '-' + (++sequence) } }))
|
|
24
|
+
}
|
|
@@ -53,7 +53,7 @@ import { displayDiffTotal, editorDockMode } from '../diffDock.js'
|
|
|
53
53
|
import { editorHeight } from '../editorLayout.js'
|
|
54
54
|
import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
|
|
55
55
|
import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
|
|
56
|
-
import {
|
|
56
|
+
import { createViewKeeper } from '../state/editorViewState.js'
|
|
57
57
|
import { mdPreviewLoad, mdPreviewSave } from '../state/mdPreviewCache.js'
|
|
58
58
|
import { onUiStateReady, reportUiStateEvent, uiStateSettled } from '../state/uiStatePersist.js'
|
|
59
59
|
import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
|
|
@@ -333,8 +333,8 @@ export function EditorView(props) {
|
|
|
333
333
|
const modelsRef = React.useRef(null)
|
|
334
334
|
// model 跨挂载缓存(按工作区作用域):重挂载秒显内容,消除切换对话闪烁
|
|
335
335
|
if (!modelsRef.current) modelsRef.current = modelsForScope(scope)
|
|
336
|
-
//
|
|
337
|
-
const
|
|
336
|
+
// 视图/查找生命周期跟随实际绑定的 model,不能从新一帧 active/scope 猜旧模型归属。
|
|
337
|
+
const viewKeeperRef = React.useRef(null)
|
|
338
338
|
// 防抖保存槽(arm/flush/cancel;见 saveDebounce.ts 的缺陷说明,勿退回单一取消句柄)
|
|
339
339
|
const saveTimerRef = React.useRef(createSaveTimer())
|
|
340
340
|
const savingRef = React.useRef(new Set()) // 在途保存的路径(关闭前落盘去重,防同内容双发)
|
|
@@ -561,39 +561,16 @@ export function EditorView(props) {
|
|
|
561
561
|
return normalized
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
-
/**
|
|
565
|
-
|
|
566
|
-
* @author ddj 2026年08月28号
|
|
567
|
-
* @param path 要保存的文件路径(缺省 = 当前 active)
|
|
568
|
-
*/
|
|
569
|
-
const saveViewState = (path) => {
|
|
570
|
-
const ed = editorRef.current
|
|
571
|
-
const target = path ?? active
|
|
572
|
-
if (!ed || !target) return
|
|
573
|
-
try {
|
|
574
|
-
const state = ed.saveViewState()
|
|
575
|
-
if (!state) return
|
|
576
|
-
viewStatesRef.current = upsertViewState(viewStatesRef.current, target, state)
|
|
577
|
-
viewStatesSave(scope, viewStatesRef.current)
|
|
578
|
-
} catch (e) { /* 视图状态保存失败忽略 */ }
|
|
579
|
-
}
|
|
564
|
+
/** @private @author ddj 2026年10月09号 @param path 可选预期文件路径。按已绑定模型归属保存,避免新 active 覆盖旧文件状态。 */
|
|
565
|
+
const saveViewState = (path) => viewKeeperRef.current?.save(path)
|
|
580
566
|
saveViewStateRef.current = saveViewState
|
|
581
567
|
|
|
582
|
-
/**
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const ed = editorRef.current
|
|
589
|
-
if (!ed || !path) return
|
|
590
|
-
const saved = viewStatesRef.current[path]
|
|
591
|
-
if (!saved) return
|
|
592
|
-
const next = Object.assign({}, viewStatesRef.current)
|
|
593
|
-
delete next[path]
|
|
594
|
-
viewStatesRef.current = next
|
|
595
|
-
try { ed.restoreViewState(saved) } catch (e) { /* 恢复失败忽略(行号越界自动兜底) */ }
|
|
596
|
-
}
|
|
568
|
+
/** @private @author ddj 2026年10月09号 ref 脱离与 effect 卸载共用:保存视图/查找后销毁,重复调用无副作用。 */
|
|
569
|
+
const disposeEditor = React.useCallback(() => {
|
|
570
|
+
viewKeeperRef.current?.dispose()
|
|
571
|
+
viewKeeperRef.current = null
|
|
572
|
+
editorRef.current = null
|
|
573
|
+
}, [])
|
|
597
574
|
|
|
598
575
|
/**
|
|
599
576
|
* 采集当前焦点条目:路径从编辑器 model 实时读取(避免空依赖闭包读到过期 active),
|
|
@@ -1166,14 +1143,14 @@ export function EditorView(props) {
|
|
|
1166
1143
|
const onDiskChange = (change) => {
|
|
1167
1144
|
if (!change || !change.path) return
|
|
1168
1145
|
// 文件树同步:目录缓存按该路径失效并强制重列(外部新增/删除/改名都能看到)
|
|
1169
|
-
emitFileChanged(change.path)
|
|
1146
|
+
const eventId = emitFileChanged(change.path, { scope, kind: change.kind === 'deleted' ? 'structure' : 'text' })
|
|
1170
1147
|
if (change.kind === 'modified') {
|
|
1171
1148
|
// 干净缓冲:强制从磁盘重读(跳过 model 复用),差异标记随之重算
|
|
1172
1149
|
loadContent(change.path, sessionId, true)
|
|
1173
1150
|
clearSync(scope, change.path)
|
|
1174
1151
|
setDiskFlag((prev) => (prev && prev.path === change.path ? null : prev))
|
|
1175
1152
|
setStatus('已同步外部修改 ' + new Date().toTimeString().slice(0, 8))
|
|
1176
|
-
emitRefresh()
|
|
1153
|
+
emitRefresh({ scope, paths: [change.path], kind: 'text', eventId })
|
|
1177
1154
|
return
|
|
1178
1155
|
}
|
|
1179
1156
|
const kind = change.kind === 'deleted' ? 'deleted' : 'conflict'
|
|
@@ -1366,7 +1343,6 @@ export function EditorView(props) {
|
|
|
1366
1343
|
restoredScopeRef.current = scope
|
|
1367
1344
|
// model 缓存跟随作用域(切工作区释放旧作用域模型;同工作区跨挂载复用)
|
|
1368
1345
|
modelsRef.current = modelsForScope(scope)
|
|
1369
|
-
viewStatesRef.current = viewStatesLoad(scope)
|
|
1370
1346
|
// Markdown 预览态同样按作用域恢复(重启/换端口后由界面状态镜像回填,见 lateHydrateRef 分支)
|
|
1371
1347
|
mdPreviewRef.current = mdPreviewLoad(String(scope))
|
|
1372
1348
|
try {
|
|
@@ -1992,7 +1968,7 @@ export function EditorView(props) {
|
|
|
1992
1968
|
clearSync(scope, path)
|
|
1993
1969
|
acceptSaved(path, text, savedModel)
|
|
1994
1970
|
refreshRecords()
|
|
1995
|
-
emitRefresh()
|
|
1971
|
+
emitRefresh({ scope, paths: [path], kind: 'text' })
|
|
1996
1972
|
// 片段配置文件保存后失效补全缓存(下次补全即读到新片段)
|
|
1997
1973
|
if (/\.code-snippets$/i.test(path)) window.dispatchEvent(new CustomEvent('edrv:snippets-changed'))
|
|
1998
1974
|
return
|
|
@@ -2077,11 +2053,10 @@ export function EditorView(props) {
|
|
|
2077
2053
|
if (!monaco || !editorRef.current || !active || !contentReady) return
|
|
2078
2054
|
const ed = editorRef.current
|
|
2079
2055
|
const model = getModel(active, content)
|
|
2080
|
-
|
|
2056
|
+
viewKeeperRef.current?.bind(String(scope), active, model)
|
|
2081
2057
|
syncTypeSuggest(ed, monaco)
|
|
2082
|
-
restoreViewState(active)
|
|
2083
2058
|
setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
|
|
2084
|
-
}, [monaco, active, content, contentReady, mdPreviewing, svnDiff])
|
|
2059
|
+
}, [monaco, scope, active, content, contentReady, mdPreviewing, svnDiff])
|
|
2085
2060
|
|
|
2086
2061
|
// PDF 面板外壳 ref 回调(div 仅在 PDF 分支渲染,refs 先于 effect 就绪)
|
|
2087
2062
|
const ensurePdfHost = (node) => { pdfHostRef.current = node }
|
|
@@ -2140,7 +2115,7 @@ export function EditorView(props) {
|
|
|
2140
2115
|
// 跳转目标高亮的计时器随卸载清理(装饰随编辑器 dispose 一并消失)
|
|
2141
2116
|
if (navFlashTimerRef.current) { clearTimeout(navFlashTimerRef.current); navFlashTimerRef.current = null }
|
|
2142
2117
|
navFlashRef.current = []
|
|
2143
|
-
|
|
2118
|
+
disposeEditor()
|
|
2144
2119
|
// model 不在此销毁:跨挂载缓存按作用域存活(modelCache 切作用域时统一释放)
|
|
2145
2120
|
for (const ctl of pdfCtlRef.current.values()) ctl.destroy()
|
|
2146
2121
|
pdfCtlRef.current.clear()
|
|
@@ -2155,7 +2130,7 @@ export function EditorView(props) {
|
|
|
2155
2130
|
// 普通 Monaco 编辑器(可编辑、单列行号)+ 自绘行内差异(decoration 绿底+ / view zone 删除块)
|
|
2156
2131
|
const ensureEditor = React.useCallback((node) => {
|
|
2157
2132
|
if (!node) {
|
|
2158
|
-
|
|
2133
|
+
disposeEditor()
|
|
2159
2134
|
return
|
|
2160
2135
|
}
|
|
2161
2136
|
if (editorRef.current || !monacoRef.current) return
|
|
@@ -2367,6 +2342,7 @@ export function EditorView(props) {
|
|
|
2367
2342
|
}
|
|
2368
2343
|
node.addEventListener('contextmenu', onBpCtxCapture, true)
|
|
2369
2344
|
editorRef.current = ed
|
|
2345
|
+
viewKeeperRef.current = createViewKeeper(ed)
|
|
2370
2346
|
}, [])
|
|
2371
2347
|
|
|
2372
2348
|
// 调试断点装饰:断点表/活动文件变化时全量替换行首圆点(独立 collection,与差异/下划线互不干扰);
|
|
@@ -2580,7 +2556,7 @@ export function EditorView(props) {
|
|
|
2580
2556
|
setDirtyMap((d) => Object.assign({}, d, { [path]: false }))
|
|
2581
2557
|
setDiskFlag((prev) => (prev && prev.path === path ? null : prev))
|
|
2582
2558
|
setStatus('已覆盖磁盘')
|
|
2583
|
-
emitRefresh()
|
|
2559
|
+
emitRefresh({ scope, paths: [path], kind: 'text' })
|
|
2584
2560
|
return
|
|
2585
2561
|
}
|
|
2586
2562
|
setStatus('覆盖失败')
|
|
@@ -2793,7 +2769,7 @@ export function EditorView(props) {
|
|
|
2793
2769
|
// #endregion
|
|
2794
2770
|
if (res && res.ok) {
|
|
2795
2771
|
setRecords((prev) => Object.assign({}, prev, { [region.callId]: res.record }))
|
|
2796
|
-
if (!silent) { reloadFile(); emitRefresh() }
|
|
2772
|
+
if (!silent) { reloadFile(); emitRefresh({ scope, paths: [res.record?.path || active], kind: 'structure' }) }
|
|
2797
2773
|
return true
|
|
2798
2774
|
}
|
|
2799
2775
|
if (!silent) setError(res?.error ? String(res.error) : '操作失败')
|
|
@@ -2882,7 +2858,7 @@ export function EditorView(props) {
|
|
|
2882
2858
|
actMany(decideRegions.map((r) => itemOf(r, false))).then((result) => {
|
|
2883
2859
|
batchBusyRef.current = false
|
|
2884
2860
|
reloadFile(true)
|
|
2885
|
-
emitRefresh()
|
|
2861
|
+
emitRefresh({ scope, paths: active ? [active] : undefined, kind: 'structure' })
|
|
2886
2862
|
setStatus(decideStatus('已采纳 ', result))
|
|
2887
2863
|
if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
2888
2864
|
}).catch(() => { batchBusyRef.current = false })
|
|
@@ -2893,7 +2869,7 @@ export function EditorView(props) {
|
|
|
2893
2869
|
actMany([...decideRegions].reverse().map((r) => itemOf(r, true))).then((result) => {
|
|
2894
2870
|
batchBusyRef.current = false
|
|
2895
2871
|
reloadFile(true)
|
|
2896
|
-
emitRefresh()
|
|
2872
|
+
emitRefresh({ scope, paths: active ? [active] : undefined, kind: 'structure' })
|
|
2897
2873
|
setStatus(decideStatus('已不采纳 ', result))
|
|
2898
2874
|
if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
2899
2875
|
}).catch(() => { batchBusyRef.current = false })
|
|
@@ -2923,10 +2899,11 @@ export function EditorView(props) {
|
|
|
2923
2899
|
if (batchBusyRef.current || !allPending.length) return
|
|
2924
2900
|
batchBusyRef.current = true
|
|
2925
2901
|
const list = reject ? [...allPending].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : b.idx - a.idx)) : allPending
|
|
2902
|
+
const paths = [...new Set(list.map((r) => records[r.callId]?.path).filter(Boolean))]
|
|
2926
2903
|
actMany(list.map((r) => itemOf(r, reject))).then((result) => {
|
|
2927
2904
|
batchBusyRef.current = false
|
|
2928
2905
|
reloadFile(true)
|
|
2929
|
-
emitRefresh()
|
|
2906
|
+
emitRefresh({ scope, paths: paths.length ? paths : undefined, kind: 'structure' })
|
|
2930
2907
|
setStatus(decideStatus(reject ? '已不采纳 ' : '已采纳 ', result))
|
|
2931
2908
|
if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
2932
2909
|
}).catch(() => { batchBusyRef.current = false })
|
|
@@ -2939,7 +2916,7 @@ export function EditorView(props) {
|
|
|
2939
2916
|
rpc('edrv.rollback', { sessionId, path: active }).then((res) => {
|
|
2940
2917
|
if (res && res.ok) {
|
|
2941
2918
|
setStatus('已回滚'); reloadFile()
|
|
2942
|
-
emitRefresh()
|
|
2919
|
+
emitRefresh({ scope, paths: [active], kind: 'structure' })
|
|
2943
2920
|
} else setError(res?.error ? String(res.error) : '回滚失败')
|
|
2944
2921
|
}).catch((e) => setError('回滚异常:' + String(e)))
|
|
2945
2922
|
}
|
|
@@ -4221,7 +4198,7 @@ export function EditorView(props) {
|
|
|
4221
4198
|
setStatus(message)
|
|
4222
4199
|
setRecentUpdate({ path: result.target, sessionId, scope, resumeJobId: result.jobId })
|
|
4223
4200
|
refreshSvnChanges(sessionId, scope)
|
|
4224
|
-
emitRefresh()
|
|
4201
|
+
emitRefresh({ scope, kind: 'svn', eventId: 'svn-update-' + result.jobId })
|
|
4225
4202
|
}
|
|
4226
4203
|
const svnUpdateEl = sameUpdateOwner(svnUpdateDialog, sessionId, scope)
|
|
4227
4204
|
? React.createElement(SvnUpdateDialog, {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Small vector overlays over official file-type icons. @author ddj 2026年10月09号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import type { SvnExplorerMark, SvnViewMark } from '../../shared/svnExplorer.js'
|
|
4
|
+
/** @public @author ddj 2026年10月10号 @param row Compact mark. @returns Overlay description; stale evidence never renders a green check. */
|
|
5
|
+
export function viewOverlayOf(row?: SvnViewMark): SvnExplorerMark | null {
|
|
6
|
+
if (!row?.kind || (row.stale && row.kind === 'clean')) return null
|
|
7
|
+
const names = { clean: '本地正常(不表示仓库无更新)', modified: '本地修改', conflict: '冲突或阻塞',
|
|
8
|
+
added: '新增', deleted: '删除或缺失', unversioned: '未受控', ignored: '忽略', external: '外部引用' }
|
|
9
|
+
return { kind: row.kind, title: names[row.kind] + (row.count ? ';子项 ' + row.count + ' 处变更' : '')
|
|
10
|
+
+ (row.stale ? ';状态陈旧,等待确认' : '') }
|
|
11
|
+
}
|
|
12
|
+
const SIGNS = { clean: '✓', modified: '!', conflict: '×', added: '+', deleted: '−', unversioned: '?', ignored: '·', external: '↗' }
|
|
13
|
+
|
|
14
|
+
/** @public @author ddj 2026年10月09号 @param mark Confirmed local mark. @param remote Remote changed-item count. @returns Accessible vector overlay; remote arrow coexists with local conflict. */
|
|
15
|
+
export function svnOverlayEl(mark: SvnExplorerMark | null, remote: number): React.ReactElement | null {
|
|
16
|
+
if (!mark && !remote) return null
|
|
17
|
+
const title = 'SVN ' + (mark?.title ?? '本地状态未确认') + (remote ? ';仓库有可更新内容(' + remote + ' 项)' : '')
|
|
18
|
+
const svg = mark ? React.createElement('svg', { viewBox: '0 0 12 12', width: 12, height: 12, 'aria-hidden': true },
|
|
19
|
+
React.createElement('circle', { cx: 6, cy: 6, r: 5.5, fill: 'currentColor' }),
|
|
20
|
+
React.createElement('text', { x: 6, y: 9, textAnchor: 'middle', fill: '#fff', fontSize: 9, fontFamily: 'Arial, sans-serif', fontWeight: 700 }, SIGNS[mark.kind])) : null
|
|
21
|
+
return React.createElement('span', { className: 'edrv-svn-overlay', title, 'aria-label': title, role: 'img' },
|
|
22
|
+
mark ? React.createElement('span', { className: 'edrv-svn-local edrv-svn-icon-' + mark.kind }, svg) : null,
|
|
23
|
+
remote ? React.createElement('span', { className: 'edrv-svn-remote', 'aria-hidden': true }, '↓') : null)
|
|
24
|
+
}
|