dsh-git-changes 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.en.md +105 -0
- package/README.md +122 -2
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/assets/vendor/monaco/LICENSE +21 -0
- package/assets/vendor/monaco/ThirdPartyNotices.txt +448 -0
- package/assets/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.js +7 -0
- package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-cn.js +7 -0
- package/assets/vendor/monaco/vs/base/worker/workerMain.js +26 -0
- package/assets/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/assets/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/assets/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/assets/vendor/monaco/vs/editor/editor.main.css +6 -0
- package/assets/vendor/monaco/vs/editor/editor.main.js +744 -0
- package/assets/vendor/monaco/vs/editor/editor.main.nls.js +28 -0
- package/assets/vendor/monaco/vs/editor/editor.main.nls.zh-cn.js +30 -0
- package/assets/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/assets/vendor/monaco/vs/language/css/cssWorker.js +81 -0
- package/assets/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/assets/vendor/monaco/vs/language/html/htmlWorker.js +453 -0
- package/assets/vendor/monaco/vs/language/json/jsonMode.js +15 -0
- package/assets/vendor/monaco/vs/language/json/jsonWorker.js +36 -0
- package/assets/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/assets/vendor/monaco/vs/language/typescript/tsWorker.js +37016 -0
- package/assets/vendor/monaco/vs/loader.js +10 -0
- package/cordis.patch.yml +3 -0
- package/docs/images/git-changes-diff.png +0 -0
- package/icon.svg +10 -0
- package/lib/client.js +9418 -0
- package/lib/host/assets.js +161 -0
- package/lib/host/commit-message.js +158 -0
- package/lib/host/files-index.js +206 -0
- package/lib/host/git-write.js +194 -0
- package/lib/host/git.js +1147 -0
- package/lib/host/lsp.js +842 -0
- package/lib/host/routes.js +976 -0
- package/lib/host/session.js +84 -0
- package/lib/host/util.js +107 -0
- package/lib/index.js +33 -0
- package/licenses/dsh-vscode-mode-LICENSE +21 -0
- package/licenses/vscode-theme-seti-LICENSE +21 -0
- package/licenses/vscode-theme-seti-ThirdPartyNotices.txt +32 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +63 -3
- package/src/client/00-const.js +79 -0
- package/src/client/05-utils.js +277 -0
- package/src/client/10-i18n.js +548 -0
- package/src/client/12-vscode-icons.js +460 -0
- package/src/client/15-theme.js +509 -0
- package/src/client/20-monaco.js +1404 -0
- package/src/client/22-lsp.js +592 -0
- package/src/client/25-tree.js +563 -0
- package/src/client/28-changes.js +1163 -0
- package/src/client/30-page.js +2259 -0
- package/src/client/35-pill.js +185 -0
- package/src/client/40-commands.js +248 -0
- package/src/client/45-css.js +357 -0
- package/src/client/47-pickers.js +457 -0
- package/src/client/50-apply.js +207 -0
- package/src/client/99-footer.js +62 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/* dsh-git-changes client half — lazy workspace file tree styled like the VS Code Explorer. */
|
|
2
|
+
|
|
3
|
+
/** Monaco language id → VS Code/Seti language id (only the ones that differ). */
|
|
4
|
+
const SETI_LANG_ALIAS = {
|
|
5
|
+
shell: 'shellscript',
|
|
6
|
+
bat: 'bat',
|
|
7
|
+
dockerfile: 'dockerfile',
|
|
8
|
+
plaintext: '',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Codicon codepoints used by the page chrome (from the Monaco codicon font). */
|
|
12
|
+
const CODICON_GLYPHS = {
|
|
13
|
+
chevronRight: '\uEAB6',
|
|
14
|
+
chevronDown: '\uEAB4',
|
|
15
|
+
close: '\uEA76',
|
|
16
|
+
ellipsis: '\uEA7C',
|
|
17
|
+
search: '\uEA6D',
|
|
18
|
+
refresh: '\uEB37',
|
|
19
|
+
diff: '\uEAE1',
|
|
20
|
+
files: '\uEAF0',
|
|
21
|
+
filter: '\uEAF1',
|
|
22
|
+
add: '\uEA60',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the Seti file icon for one path (VS Code icon theme precedence:
|
|
27
|
+
* exact file name, longest extension chain, language id, default file icon).
|
|
28
|
+
* @param {string} path workspace-relative or absolute path
|
|
29
|
+
* @param {'light'|'dark'} scheme active color scheme
|
|
30
|
+
* @returns {{char:string,color:string,lightColor:string,lightChar?:string}|null}
|
|
31
|
+
*/
|
|
32
|
+
function vscodeIconFor(path, scheme) {
|
|
33
|
+
const theme = typeof VSCODE_ICON_THEME !== 'undefined' ? VSCODE_ICON_THEME : null
|
|
34
|
+
if (!theme) return null
|
|
35
|
+
let icon = theme.forPath(path)
|
|
36
|
+
if (!icon) {
|
|
37
|
+
const language = (typeof languageOf === 'function' ? languageOf(path) : '') || ''
|
|
38
|
+
const alias = Object.prototype.hasOwnProperty.call(SETI_LANG_ALIAS, language) ? SETI_LANG_ALIAS[language] : language
|
|
39
|
+
icon = alias ? theme.languageIds[alias] : undefined
|
|
40
|
+
}
|
|
41
|
+
if (!icon) icon = theme.file
|
|
42
|
+
return icon || null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One VS Code codicon glyph (chevrons, close, ellipsis…).
|
|
47
|
+
* @param {object} props `char` glyph key, `size` optional font size
|
|
48
|
+
* @returns {any} React element
|
|
49
|
+
*/
|
|
50
|
+
function Codicon(props) {
|
|
51
|
+
const { char, size, className } = props
|
|
52
|
+
return h('span', {
|
|
53
|
+
className: cx('drp-codicon', className),
|
|
54
|
+
style: size ? { fontSize: size } : undefined,
|
|
55
|
+
'aria-hidden': true,
|
|
56
|
+
}, CODICON_GLYPHS[char] || '')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One Seti file icon at VS Code metrics (16 px box, 6 px gap, 150 % glyph).
|
|
61
|
+
* @param {object} props `path` file path, `scheme` active scheme
|
|
62
|
+
* @returns {any} React element
|
|
63
|
+
*/
|
|
64
|
+
function FileIcon(props) {
|
|
65
|
+
const { path, scheme } = props
|
|
66
|
+
const icon = vscodeIconFor(path, scheme)
|
|
67
|
+
if (!icon) return h('span', { className: 'drp-tree-icon' })
|
|
68
|
+
const char = icon.lightChar && scheme === 'light' ? icon.lightChar : icon.char
|
|
69
|
+
const color = scheme === 'light' ? icon.lightColor || icon.color : icon.color
|
|
70
|
+
return h('span', {
|
|
71
|
+
className: 'drp-tree-icon drp-seti-icon',
|
|
72
|
+
style: { color },
|
|
73
|
+
'data-drp-icon': char,
|
|
74
|
+
}, char)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Folder glyph shared by the change tree and the project tree.
|
|
79
|
+
* @returns {any} React element
|
|
80
|
+
*/
|
|
81
|
+
function FolderGlyph() {
|
|
82
|
+
return h(
|
|
83
|
+
'svg',
|
|
84
|
+
{
|
|
85
|
+
width: 12,
|
|
86
|
+
height: 12,
|
|
87
|
+
viewBox: '0 0 16 16',
|
|
88
|
+
'aria-hidden': 'true',
|
|
89
|
+
focusable: 'false',
|
|
90
|
+
fill: 'currentColor',
|
|
91
|
+
fillOpacity: 0.18,
|
|
92
|
+
stroke: 'currentColor',
|
|
93
|
+
strokeWidth: 1.2,
|
|
94
|
+
strokeLinejoin: 'round',
|
|
95
|
+
},
|
|
96
|
+
h('path', { d: 'M1.6 3.4h4.3l1.4 1.7h7.1v7.5H1.6z' }),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build a map of normalized path → change badge data.
|
|
102
|
+
* @param {Array<object> | null} files summary files
|
|
103
|
+
* @param {boolean} enabled whether badges are shown
|
|
104
|
+
* @returns {Map<string, object>}
|
|
105
|
+
*/
|
|
106
|
+
function changedMapOf(files, enabled) {
|
|
107
|
+
const map = new Map()
|
|
108
|
+
if (!enabled || !Array.isArray(files)) return map
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
if (!file || typeof file.path !== 'string') continue
|
|
111
|
+
const key = normalizeRel(file.path)
|
|
112
|
+
if (key !== '') map.set(key, file)
|
|
113
|
+
}
|
|
114
|
+
return map
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One change badge: +added −deleted, or a binary/oversized marker.
|
|
119
|
+
* @param {object} file changed-file record
|
|
120
|
+
* @returns {any} React element
|
|
121
|
+
*/
|
|
122
|
+
function ChangeBadge(file) {
|
|
123
|
+
if (file.binary === true) return h('span', { className: 'drp-badge drp-badge-muted', title: 'binary' }, 'bin')
|
|
124
|
+
if (file.oversized === true) return h('span', { className: 'drp-badge drp-badge-muted', title: 'too large' }, 'big')
|
|
125
|
+
return h(
|
|
126
|
+
'span',
|
|
127
|
+
{ className: 'drp-badge' },
|
|
128
|
+
h('span', { className: 'drp-badge-add' }, '+' + fmtInt(file.added)),
|
|
129
|
+
h('span', { className: 'drp-badge-del' }, '−' + fmtInt(file.deleted)),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build an absolute host path from the session cwd and a workspace-relative path.
|
|
135
|
+
* @param {string | undefined} cwd session working directory
|
|
136
|
+
* @param {string} rel workspace-relative posix path
|
|
137
|
+
* @returns {string} absolute path using the cwd's separators
|
|
138
|
+
*/
|
|
139
|
+
function absolutePathOf(cwd, rel) {
|
|
140
|
+
const root = String(cwd || '').replace(/[\\/]+$/, '')
|
|
141
|
+
const tail = String(rel || '').replace(/^[\\/]+/, '')
|
|
142
|
+
if (!root) return tail
|
|
143
|
+
const separator = root.includes('\\') ? '\\' : '/'
|
|
144
|
+
return root + separator + (separator === '\\' ? tail.replace(/\//g, '\\') : tail)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* One git status letter badge (M/A/D/R/C/U), coloured like VS Code's
|
|
149
|
+
* git decorations. Diff-range statuses are folded onto the four badge colours
|
|
150
|
+
* (`T` type change reads as modified, `U`/`?`/anything else as unmerged).
|
|
151
|
+
* @param {{path:string, code:string}} entry git status record
|
|
152
|
+
* @returns {any} React element
|
|
153
|
+
*/
|
|
154
|
+
function GitBadge(entry) {
|
|
155
|
+
const raw = typeof entry.code === 'string' ? entry.code : ''
|
|
156
|
+
const code = raw === 'T' ? 'M' : /^[MADRC]$/.test(raw) ? raw : 'U'
|
|
157
|
+
const className = code === 'U' ? 'drp-git-other' : 'drp-git-' + code
|
|
158
|
+
return h('span', { className: cx('drp-git-badge', className), title: 'Git: ' + (raw || '?') }, code)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Indentation (px) of a row's left edge inside the tree, matching VS Code:
|
|
163
|
+
* 0 for depth 0, then `8 + (depth-1)*8` (the 4 px row inset comes from CSS).
|
|
164
|
+
* @param {number} depth zero-based row depth
|
|
165
|
+
* @returns {number}
|
|
166
|
+
*/
|
|
167
|
+
function treeIndentOf(depth) {
|
|
168
|
+
const level = Math.max(0, Number(depth) || 0)
|
|
169
|
+
return level === 0 ? 0 : 8 + (level - 1) * 8
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A directory row plus its lazily loaded children. The implicit workspace root
|
|
174
|
+
* is not rendered as a row (VS Code's Explorer lists the top-level entries
|
|
175
|
+
* directly under the section header).
|
|
176
|
+
* @param {object} props component props
|
|
177
|
+
* @returns {any} React element
|
|
178
|
+
*/
|
|
179
|
+
function TreeDir(props) {
|
|
180
|
+
const { relPath, depth, expanded, loadState, onToggle, onOpen, onContextMenu, activePath, changed, badgesEnabled, gitBadges, scheme } = props
|
|
181
|
+
const isExpanded = expanded.has(relPath)
|
|
182
|
+
const state = loadState(relPath)
|
|
183
|
+
const children = state && Array.isArray(state.entries) ? state.entries : []
|
|
184
|
+
const content = []
|
|
185
|
+
if (state && state.status === 'loading' && children.length === 0) {
|
|
186
|
+
content.push(h('div', {
|
|
187
|
+
key: relPath + '::loading',
|
|
188
|
+
className: 'drp-tree-note',
|
|
189
|
+
style: { paddingLeft: (treeIndentOf(depth + 1) + 22) + 'px' },
|
|
190
|
+
}, '…'))
|
|
191
|
+
}
|
|
192
|
+
for (const entry of children) {
|
|
193
|
+
const childRel = joinRel(relPath, entry.name)
|
|
194
|
+
if (entry.type === 'directory') {
|
|
195
|
+
content.push(h(TreeDir, {
|
|
196
|
+
key: childRel,
|
|
197
|
+
relPath: childRel,
|
|
198
|
+
depth: depth + 1,
|
|
199
|
+
expanded,
|
|
200
|
+
loadState,
|
|
201
|
+
onToggle,
|
|
202
|
+
onOpen,
|
|
203
|
+
onContextMenu,
|
|
204
|
+
activePath,
|
|
205
|
+
changed,
|
|
206
|
+
badgesEnabled,
|
|
207
|
+
gitBadges,
|
|
208
|
+
scheme,
|
|
209
|
+
}))
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
const badge = badgesEnabled ? changed.get(childRel) : undefined
|
|
213
|
+
const gitEntry = gitBadges ? gitBadges.get(childRel) : undefined
|
|
214
|
+
const isActive = activePath === childRel
|
|
215
|
+
content.push(h(
|
|
216
|
+
'div',
|
|
217
|
+
{
|
|
218
|
+
key: childRel,
|
|
219
|
+
className: cx('drp-tree-row drp-tree-file', isActive && 'drp-tree-active'),
|
|
220
|
+
style: { paddingLeft: (8 + treeIndentOf(depth + 1)) + 'px' },
|
|
221
|
+
onClick: () => onOpen(childRel),
|
|
222
|
+
onContextMenu: (event) => onContextMenu(event, childRel),
|
|
223
|
+
title: childRel + (gitEntry ? '\nGit: ' + gitEntry.code : ''),
|
|
224
|
+
role: 'treeitem',
|
|
225
|
+
'data-drp-path': childRel,
|
|
226
|
+
},
|
|
227
|
+
h('span', { className: 'drp-tree-twisty' }),
|
|
228
|
+
h(FileIcon, { path: childRel, scheme }),
|
|
229
|
+
h('span', { className: 'drp-tree-name' }, entry.name),
|
|
230
|
+
gitEntry ? GitBadge(gitEntry) : null,
|
|
231
|
+
badge ? h(ChangeBadge, badge) : null,
|
|
232
|
+
))
|
|
233
|
+
}
|
|
234
|
+
if (state && state.truncated) {
|
|
235
|
+
content.push(h('div', {
|
|
236
|
+
key: relPath + '::truncated',
|
|
237
|
+
className: 'drp-tree-note',
|
|
238
|
+
style: { paddingLeft: (treeIndentOf(depth + 1) + 22) + 'px' },
|
|
239
|
+
}, '… truncated'))
|
|
240
|
+
}
|
|
241
|
+
if (relPath === '') {
|
|
242
|
+
// Implicit workspace root: only its children are rendered.
|
|
243
|
+
return h('div', { className: 'drp-tree-branch drp-tree-root' }, content)
|
|
244
|
+
}
|
|
245
|
+
return h(
|
|
246
|
+
'div',
|
|
247
|
+
{ className: 'drp-tree-branch' },
|
|
248
|
+
h(
|
|
249
|
+
'div',
|
|
250
|
+
{
|
|
251
|
+
className: cx('drp-tree-row drp-tree-dir', isExpanded && 'drp-tree-dir-open'),
|
|
252
|
+
style: { paddingLeft: (8 + treeIndentOf(depth)) + 'px' },
|
|
253
|
+
onClick: () => onToggle(relPath),
|
|
254
|
+
role: 'treeitem',
|
|
255
|
+
'aria-expanded': isExpanded,
|
|
256
|
+
'data-drp-dir': relPath,
|
|
257
|
+
title: relPath,
|
|
258
|
+
},
|
|
259
|
+
h('span', { className: cx('drp-tree-twisty', isExpanded && 'drp-tree-twisty-open') },
|
|
260
|
+
h(Codicon, { char: isExpanded ? 'chevronDown' : 'chevronRight' })),
|
|
261
|
+
h('span', { className: 'drp-folder-icon' }, h(FolderGlyph)),
|
|
262
|
+
h('span', { className: 'drp-tree-name' }, basename(relPath)),
|
|
263
|
+
state && state.status === 'loading' ? h('span', { className: 'drp-tree-note' }, '…') : null,
|
|
264
|
+
state && state.status === 'error' ? h('span', { className: 'drp-tree-note drp-tree-error' }, '!') : null,
|
|
265
|
+
),
|
|
266
|
+
isExpanded ? h('div', { className: 'drp-tree-children' }, content) : null,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Workspace tree panel: lazy directories, change badges, "changes only" flat list,
|
|
272
|
+
* reveal-in-tree and manual refresh. Visual metrics follow VS Code's Explorer
|
|
273
|
+
* (22 px rows, 8 px indent, 13 px UI font, Seti icons, 28 px section header).
|
|
274
|
+
* @param {object} props component props
|
|
275
|
+
* @returns {any} React element
|
|
276
|
+
*/
|
|
277
|
+
function FileTree(props) {
|
|
278
|
+
const {
|
|
279
|
+
sessionId, cwd, summaryFiles, gitStatus, activePath, treeMode,
|
|
280
|
+
onOpen, reveal, refreshToken, t, onPreview, onNotify, scheme,
|
|
281
|
+
} = props
|
|
282
|
+
const activeScheme = scheme === 'light' ? 'light' : 'dark'
|
|
283
|
+
const [expanded, setExpanded] = React.useState(() => new Set(['']))
|
|
284
|
+
const [cache, setCache] = React.useState(() => new Map())
|
|
285
|
+
const [error, setError] = React.useState(null)
|
|
286
|
+
const [ctxMenu, setCtxMenu] = React.useState(null)
|
|
287
|
+
const loadingRef = React.useRef(new Map())
|
|
288
|
+
const generationRef = React.useRef(0)
|
|
289
|
+
const rootRef = React.useRef(null)
|
|
290
|
+
const gitBadges = React.useMemo(() => {
|
|
291
|
+
if ((treeMode !== 'git' && treeMode !== 'revision') || !gitStatus || gitStatus.available !== true) return null
|
|
292
|
+
const map = new Map()
|
|
293
|
+
for (const entry of Array.isArray(gitStatus.files) ? gitStatus.files : []) {
|
|
294
|
+
if (!entry || typeof entry.path !== 'string') continue
|
|
295
|
+
const key = normalizeRel(entry.path)
|
|
296
|
+
if (key !== '') map.set(key, entry)
|
|
297
|
+
}
|
|
298
|
+
return map
|
|
299
|
+
}, [treeMode, gitStatus])
|
|
300
|
+
const cacheRef = React.useRef(cache)
|
|
301
|
+
cacheRef.current = cache
|
|
302
|
+
const expandedRef = React.useRef(expanded)
|
|
303
|
+
expandedRef.current = expanded
|
|
304
|
+
const changed = React.useMemo(() => changedMapOf(summaryFiles, treeMode === 'turn'), [summaryFiles, treeMode])
|
|
305
|
+
|
|
306
|
+
/** Open the tree context menu at the pointer for one file row. */
|
|
307
|
+
const showContextMenu = React.useCallback((event, relPath) => {
|
|
308
|
+
try {
|
|
309
|
+
event.preventDefault()
|
|
310
|
+
event.stopPropagation()
|
|
311
|
+
} catch (error) { /* ignore */ }
|
|
312
|
+
const width = 196
|
|
313
|
+
const height = 168
|
|
314
|
+
const x = Math.max(4, Math.min(Number(event.clientX) || 0, (window.innerWidth || 1024) - width - 4))
|
|
315
|
+
const y = Math.max(4, Math.min(Number(event.clientY) || 0, (window.innerHeight || 768) - height - 4))
|
|
316
|
+
setCtxMenu({ x, y, path: normalizeRel(relPath) })
|
|
317
|
+
}, [])
|
|
318
|
+
|
|
319
|
+
// Close the context menu on outside pointer, Escape, scroll or resize.
|
|
320
|
+
React.useEffect(() => {
|
|
321
|
+
if (!ctxMenu) return undefined
|
|
322
|
+
const close = () => setCtxMenu(null)
|
|
323
|
+
const onPointer = (event) => {
|
|
324
|
+
try {
|
|
325
|
+
const target = event.target
|
|
326
|
+
const menu = rootRef.current && rootRef.current.querySelector('.drp-ctx-menu')
|
|
327
|
+
if (menu && target instanceof Node && menu.contains(target)) return
|
|
328
|
+
} catch (error) { /* ignore */ }
|
|
329
|
+
close()
|
|
330
|
+
}
|
|
331
|
+
const onKey = (event) => { if (event.key === 'Escape') close() }
|
|
332
|
+
window.addEventListener('mousedown', onPointer)
|
|
333
|
+
window.addEventListener('keydown', onKey)
|
|
334
|
+
window.addEventListener('resize', close)
|
|
335
|
+
window.addEventListener('scroll', close, true)
|
|
336
|
+
return () => {
|
|
337
|
+
window.removeEventListener('mousedown', onPointer)
|
|
338
|
+
window.removeEventListener('keydown', onKey)
|
|
339
|
+
window.removeEventListener('resize', close)
|
|
340
|
+
window.removeEventListener('scroll', close, true)
|
|
341
|
+
}
|
|
342
|
+
}, [ctxMenu])
|
|
343
|
+
|
|
344
|
+
/** Copy one path from the context menu and notify the page. */
|
|
345
|
+
const copyFromMenu = async (relPath, absolute) => {
|
|
346
|
+
const value = absolute ? absolutePathOf(cwd, relPath) : relPath
|
|
347
|
+
const ok = await copyText(value)
|
|
348
|
+
setCtxMenu(null)
|
|
349
|
+
if (typeof onNotify === 'function') onNotify(ok ? t('common.copied') + ':' + value : t('action.copyPath'), ok ? 'info' : 'warn')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Read one cached directory state.
|
|
354
|
+
* @param {string} relPath directory path
|
|
355
|
+
* @returns {object | undefined}
|
|
356
|
+
*/
|
|
357
|
+
const loadState = (relPath) => cache.get(relPath)
|
|
358
|
+
|
|
359
|
+
/** Retire every request before replacing the cache or workspace. */
|
|
360
|
+
const cancelLoads = React.useCallback(() => {
|
|
361
|
+
generationRef.current += 1
|
|
362
|
+
for (const controller of loadingRef.current.values()) {
|
|
363
|
+
try { controller?.abort() } catch (error) { /* ignore */ }
|
|
364
|
+
}
|
|
365
|
+
loadingRef.current = new Map()
|
|
366
|
+
}, [])
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Load one directory's direct children (idempotent while in flight).
|
|
370
|
+
* @param {string} relPath directory path
|
|
371
|
+
*/
|
|
372
|
+
const loadDir = React.useCallback((relPath) => {
|
|
373
|
+
if (!sessionId || !cwd) return
|
|
374
|
+
if (loadingRef.current.has(relPath)) return
|
|
375
|
+
const existing = cacheRef.current.get(relPath)
|
|
376
|
+
if (existing && (existing.status === 'ready' || existing.status === 'loading')) return
|
|
377
|
+
const generation = generationRef.current
|
|
378
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
|
379
|
+
loadingRef.current.set(relPath, controller)
|
|
380
|
+
cacheRef.current.set(relPath, { status: 'loading', entries: [] })
|
|
381
|
+
setCache(new Map(cacheRef.current))
|
|
382
|
+
apiGet('tree', { sessionId, path: relPath }, controller ? controller.signal : undefined)
|
|
383
|
+
.then((listing) => {
|
|
384
|
+
if (generation !== generationRef.current) return
|
|
385
|
+
const entries = Array.isArray(listing && listing.entries) ? listing.entries.slice() : []
|
|
386
|
+
const sorted = entries.sort((a, b) => {
|
|
387
|
+
const aDir = a.type === 'directory' ? 0 : 1
|
|
388
|
+
const bDir = b.type === 'directory' ? 0 : 1
|
|
389
|
+
if (aDir !== bDir) return aDir - bDir
|
|
390
|
+
return String(a.name).localeCompare(String(b.name))
|
|
391
|
+
})
|
|
392
|
+
cacheRef.current.set(relPath, { status: 'ready', entries: sorted, truncated: listing && listing.truncated === true })
|
|
393
|
+
setError(null)
|
|
394
|
+
})
|
|
395
|
+
.catch((failure) => {
|
|
396
|
+
if (generation !== generationRef.current) return
|
|
397
|
+
cacheRef.current.set(relPath, { status: 'error', entries: [] })
|
|
398
|
+
setError(failure && failure.message ? failure.message : String(failure))
|
|
399
|
+
})
|
|
400
|
+
.finally(() => {
|
|
401
|
+
// An older finally must not clear a newer in-flight request for the
|
|
402
|
+
// same directory, even when the old transport ignored cancellation.
|
|
403
|
+
if (generation !== generationRef.current) return
|
|
404
|
+
loadingRef.current.delete(relPath)
|
|
405
|
+
setCache(new Map(cacheRef.current))
|
|
406
|
+
})
|
|
407
|
+
}, [sessionId, cwd])
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Refresh every visible directory while keeping the expansion state.
|
|
411
|
+
* Dropping the cache alone would leave expanded directories empty (they only
|
|
412
|
+
* load on first expand), so every expanded directory is re-requested here.
|
|
413
|
+
*/
|
|
414
|
+
const refreshTree = React.useCallback(() => {
|
|
415
|
+
cancelLoads()
|
|
416
|
+
cacheRef.current = new Map()
|
|
417
|
+
setCache(new Map())
|
|
418
|
+
setError(null)
|
|
419
|
+
const dirs = new Set(expandedRef.current)
|
|
420
|
+
dirs.add('')
|
|
421
|
+
for (const dir of dirs) loadDir(dir)
|
|
422
|
+
}, [loadDir, cancelLoads])
|
|
423
|
+
|
|
424
|
+
// Page-level refresh (single toolbar ⟳) drives the tree through this token.
|
|
425
|
+
const lastRefreshRef = React.useRef(refreshToken)
|
|
426
|
+
React.useEffect(() => {
|
|
427
|
+
if (refreshToken === undefined) return
|
|
428
|
+
if (lastRefreshRef.current === refreshToken) return
|
|
429
|
+
lastRefreshRef.current = refreshToken
|
|
430
|
+
refreshTree()
|
|
431
|
+
}, [refreshToken, refreshTree])
|
|
432
|
+
|
|
433
|
+
// Reset when the session/workspace changes.
|
|
434
|
+
React.useEffect(() => {
|
|
435
|
+
cancelLoads()
|
|
436
|
+
cacheRef.current = new Map()
|
|
437
|
+
setCache(new Map())
|
|
438
|
+
expandedRef.current = new Set([''])
|
|
439
|
+
setExpanded(expandedRef.current)
|
|
440
|
+
setError(null)
|
|
441
|
+
if (sessionId && cwd) loadDir('')
|
|
442
|
+
return cancelLoads
|
|
443
|
+
}, [sessionId, cwd, loadDir, cancelLoads])
|
|
444
|
+
|
|
445
|
+
// Auto-expand ancestors of the active file.
|
|
446
|
+
React.useEffect(() => {
|
|
447
|
+
if (!activePath) return
|
|
448
|
+
const parts = segmentsOf(activePath)
|
|
449
|
+
parts.pop()
|
|
450
|
+
const next = new Set(expandedRef.current)
|
|
451
|
+
let prefix = ''
|
|
452
|
+
for (const segment of parts) {
|
|
453
|
+
prefix = prefix === '' ? segment : prefix + '/' + segment
|
|
454
|
+
next.add(prefix)
|
|
455
|
+
}
|
|
456
|
+
expandedRef.current = next
|
|
457
|
+
setExpanded(next)
|
|
458
|
+
for (const dir of next) loadDir(dir)
|
|
459
|
+
}, [activePath, loadDir])
|
|
460
|
+
|
|
461
|
+
// Reveal request: expand ancestors, then scroll the row into view.
|
|
462
|
+
const revealNonce = reveal && reveal.nonce
|
|
463
|
+
React.useEffect(() => {
|
|
464
|
+
if (!reveal || !reveal.path) return
|
|
465
|
+
const parts = segmentsOf(reveal.path)
|
|
466
|
+
parts.pop()
|
|
467
|
+
const next = new Set(expandedRef.current)
|
|
468
|
+
let prefix = ''
|
|
469
|
+
for (const segment of parts) {
|
|
470
|
+
prefix = prefix === '' ? segment : prefix + '/' + segment
|
|
471
|
+
next.add(prefix)
|
|
472
|
+
}
|
|
473
|
+
expandedRef.current = next
|
|
474
|
+
setExpanded(next)
|
|
475
|
+
for (const dir of next) loadDir(dir)
|
|
476
|
+
const timer = setTimeout(() => {
|
|
477
|
+
try {
|
|
478
|
+
const node = rootRef.current && rootRef.current.querySelector('[data-drp-path="' + String(reveal.path).replace(/"/g, '\\"') + '"]')
|
|
479
|
+
if (node && node.scrollIntoView) node.scrollIntoView({ block: 'center' })
|
|
480
|
+
} catch (err) { /* ignore */ }
|
|
481
|
+
}, 120)
|
|
482
|
+
return () => clearTimeout(timer)
|
|
483
|
+
}, [revealNonce, reveal && reveal.path, loadDir])
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Toggle one directory open/closed.
|
|
487
|
+
* @param {string} relPath directory path
|
|
488
|
+
*/
|
|
489
|
+
const toggle = (relPath) => {
|
|
490
|
+
const next = new Set(expandedRef.current)
|
|
491
|
+
if (next.has(relPath)) next.delete(relPath)
|
|
492
|
+
else {
|
|
493
|
+
next.add(relPath)
|
|
494
|
+
loadDir(relPath)
|
|
495
|
+
}
|
|
496
|
+
expandedRef.current = next
|
|
497
|
+
setExpanded(next)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Open one file row.
|
|
502
|
+
* @param {string} relPath file path
|
|
503
|
+
*/
|
|
504
|
+
const open = (relPath) => {
|
|
505
|
+
onOpen(relPath)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
const rootName = cwd ? basename(cwd) : t('toolbar.tree.all')
|
|
510
|
+
|
|
511
|
+
return h(
|
|
512
|
+
'div',
|
|
513
|
+
{ className: 'drp-tree', ref: rootRef, 'data-drp-scheme': activeScheme },
|
|
514
|
+
h(
|
|
515
|
+
'div',
|
|
516
|
+
{ className: 'drp-tree-head' },
|
|
517
|
+
h('span', { className: 'drp-tree-head-chevron' }, h(Codicon, { char: 'chevronDown' })),
|
|
518
|
+
h('span', { className: 'drp-tree-title', title: cwd || rootName }, rootName),
|
|
519
|
+
),
|
|
520
|
+
error ? h('div', { className: 'drp-tree-note drp-tree-error', style: { padding: '4px 8px' } }, error) : null,
|
|
521
|
+
h(
|
|
522
|
+
'div',
|
|
523
|
+
{ className: 'drp-tree-body' },
|
|
524
|
+
h(TreeDir, {
|
|
525
|
+
relPath: '',
|
|
526
|
+
depth: 0,
|
|
527
|
+
expanded,
|
|
528
|
+
loadState,
|
|
529
|
+
onToggle: toggle,
|
|
530
|
+
onOpen: open,
|
|
531
|
+
onContextMenu: showContextMenu,
|
|
532
|
+
activePath,
|
|
533
|
+
changed,
|
|
534
|
+
badgesEnabled: treeMode === 'turn',
|
|
535
|
+
gitBadges,
|
|
536
|
+
scheme: activeScheme,
|
|
537
|
+
}),
|
|
538
|
+
),
|
|
539
|
+
ctxMenu
|
|
540
|
+
? h(
|
|
541
|
+
'div',
|
|
542
|
+
{
|
|
543
|
+
className: 'drp-ctx-menu',
|
|
544
|
+
style: { left: ctxMenu.x + 'px', top: ctxMenu.y + 'px' },
|
|
545
|
+
role: 'menu',
|
|
546
|
+
onMouseDown: (event) => { try { event.stopPropagation() } catch (error) { /* ignore */ } },
|
|
547
|
+
},
|
|
548
|
+
h('button', { type: 'button', className: 'drp-menu-item', onClick: () => { void copyFromMenu(ctxMenu.path, false) } }, t('action.copyPath')),
|
|
549
|
+
h('button', { type: 'button', className: 'drp-menu-item', onClick: () => { void copyFromMenu(ctxMenu.path, true) } }, t('action.copyAbsolutePath')),
|
|
550
|
+
h('div', { className: 'drp-menu-sep' }),
|
|
551
|
+
h('button', {
|
|
552
|
+
type: 'button',
|
|
553
|
+
className: 'drp-menu-item',
|
|
554
|
+
onClick: () => {
|
|
555
|
+
const path = ctxMenu.path
|
|
556
|
+
setCtxMenu(null)
|
|
557
|
+
if (typeof onPreview === 'function') onPreview(path)
|
|
558
|
+
},
|
|
559
|
+
}, t('action.openInPreview')),
|
|
560
|
+
)
|
|
561
|
+
: null,
|
|
562
|
+
)
|
|
563
|
+
}
|