dsh-git-changes 0.0.0-stage → 0.1.1
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 +119 -0
- package/README.md +137 -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/docs/regression-checklist.md +47 -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 +65 -4
- 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,185 @@
|
|
|
1
|
+
/* dsh-git-changes client half — composer pill ("本轮修改"). */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Composer-dock entry: shows the latest turn's changed-file count and opens the
|
|
5
|
+
* review tab. Refreshes when the running turn settles, when the conversation
|
|
6
|
+
* binding closes its open turn, and when the window regains focus (no polling).
|
|
7
|
+
* @param {object} props slot props (`sessionId`, owner props `session`)
|
|
8
|
+
* @returns {any} React element or null when there is nothing to show
|
|
9
|
+
*/
|
|
10
|
+
function ChangesPill(props) {
|
|
11
|
+
const { sessionId, session, t } = props
|
|
12
|
+
const running = session && typeof session.running === 'boolean' ? session.running : undefined
|
|
13
|
+
const [latest, setLatest] = React.useState(null)
|
|
14
|
+
const [error, setError] = React.useState(null)
|
|
15
|
+
const tokenRef = React.useRef(0)
|
|
16
|
+
const lastLoadRef = React.useRef(0)
|
|
17
|
+
const retryTimerRef = React.useRef(null)
|
|
18
|
+
|
|
19
|
+
const load = React.useCallback(async (options) => {
|
|
20
|
+
if (!sessionId) return
|
|
21
|
+
const token = ++tokenRef.current
|
|
22
|
+
lastLoadRef.current = Date.now()
|
|
23
|
+
try {
|
|
24
|
+
const value = await apiGet('latest', { sessionId })
|
|
25
|
+
if (token !== tokenRef.current) return
|
|
26
|
+
setLatest(value || { available: false })
|
|
27
|
+
// A turn end and the workspace/changes append can race by a moment: retry once.
|
|
28
|
+
if (options && options.retry === true && value && value.available === false && value.reason === 'no-changes') {
|
|
29
|
+
if (retryTimerRef.current) clearTimeout(retryTimerRef.current)
|
|
30
|
+
retryTimerRef.current = setTimeout(() => {
|
|
31
|
+
retryTimerRef.current = null
|
|
32
|
+
void load()
|
|
33
|
+
}, 900)
|
|
34
|
+
}
|
|
35
|
+
} catch (failure) {
|
|
36
|
+
if (token !== tokenRef.current) return
|
|
37
|
+
setLatest({ available: false })
|
|
38
|
+
}
|
|
39
|
+
}, [sessionId])
|
|
40
|
+
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
void load()
|
|
43
|
+
}, [load])
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (running === false) void load({ retry: true })
|
|
47
|
+
}, [running, load])
|
|
48
|
+
|
|
49
|
+
// Session snapshot signature changes at turn boundaries; throttle to at most
|
|
50
|
+
// one refresh per 1.2s so stream-driven re-renders cannot spam the route.
|
|
51
|
+
const sessionSignature = session
|
|
52
|
+
? [running, session.turn, session.seq, session.pendingInteraction === undefined ? '' : 'interactive'].join('|')
|
|
53
|
+
: ''
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (Date.now() - lastLoadRef.current < 1200) return
|
|
56
|
+
void load({ retry: true })
|
|
57
|
+
}, [sessionSignature, load])
|
|
58
|
+
|
|
59
|
+
// Precise turn-close signal: the conversation binding's `openTurn` becomes
|
|
60
|
+
// undefined when the turn closes (after workspace/changes was appended).
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
if (!sessionId) return undefined
|
|
63
|
+
let dispose
|
|
64
|
+
try {
|
|
65
|
+
const conversation = runtimeState.conversation
|
|
66
|
+
const binding = conversation && typeof conversation.binding === 'function' ? conversation.binding(sessionId) : undefined
|
|
67
|
+
const source = binding && binding.openTurn
|
|
68
|
+
if (source && typeof source.subscribe === 'function' && typeof source.getSnapshot === 'function') {
|
|
69
|
+
let previous = source.getSnapshot()
|
|
70
|
+
const listener = () => {
|
|
71
|
+
const next = source.getSnapshot()
|
|
72
|
+
if (previous !== undefined && (next === undefined || next !== previous)) void load({ retry: true })
|
|
73
|
+
else if (next !== previous) void load()
|
|
74
|
+
previous = next
|
|
75
|
+
}
|
|
76
|
+
dispose = source.subscribe(listener)
|
|
77
|
+
}
|
|
78
|
+
} catch (failure) {
|
|
79
|
+
/* session binding not ready: the running/signature fallbacks still refresh */
|
|
80
|
+
}
|
|
81
|
+
return () => {
|
|
82
|
+
try {
|
|
83
|
+
if (typeof dispose === 'function') dispose()
|
|
84
|
+
} catch (failure) { /* ignore */ }
|
|
85
|
+
if (retryTimerRef.current) {
|
|
86
|
+
clearTimeout(retryTimerRef.current)
|
|
87
|
+
retryTimerRef.current = null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, [sessionId, load])
|
|
91
|
+
|
|
92
|
+
// Returning focus/visibility to the window also refreshes (covers a reload or
|
|
93
|
+
// a missed event while the page was hidden).
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
const refresh = () => { void load() }
|
|
96
|
+
const onVisibility = () => { if (!document.hidden) refresh() }
|
|
97
|
+
window.addEventListener('focus', refresh)
|
|
98
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
99
|
+
return () => {
|
|
100
|
+
window.removeEventListener('focus', refresh)
|
|
101
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
102
|
+
}
|
|
103
|
+
}, [load])
|
|
104
|
+
|
|
105
|
+
// Remember the session for the global "open review" command.
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
if (sessionId) runtimeState.sessionId = sessionId
|
|
108
|
+
}, [sessionId])
|
|
109
|
+
|
|
110
|
+
if (error) {
|
|
111
|
+
return h('div', { className: 'drp-pill-row' }, h('div', { className: 'drp-pill-error' }, error))
|
|
112
|
+
}
|
|
113
|
+
// While the first read is in flight, render nothing (avoids a flicker).
|
|
114
|
+
if (!latest) return null
|
|
115
|
+
|
|
116
|
+
/** Open (or focus) the review tab for this session. */
|
|
117
|
+
const onOpen = () => {
|
|
118
|
+
runtimeState.sessionId = sessionId
|
|
119
|
+
const address = ADDRESS_PREFIX + encodeURIComponent(sessionId)
|
|
120
|
+
const ok = openResourceSafe(runtimeState.right, address)
|
|
121
|
+
if (!ok) {
|
|
122
|
+
setError(t('error.sidebarUnavailable'))
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
setError(null)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (latest.available !== true) {
|
|
129
|
+
// No TURN data (no changes, or the Host restarted): keep a compact, clickable
|
|
130
|
+
// entry so the review page (and its diagnostics) stays reachable.
|
|
131
|
+
const text = latest.reason === 'no-changes' || latest.reason === 'summary-unavailable'
|
|
132
|
+
? t('pill.none')
|
|
133
|
+
: t('pill.unavailable')
|
|
134
|
+
return h(
|
|
135
|
+
'div',
|
|
136
|
+
{ className: 'drp-pill-row' },
|
|
137
|
+
h(
|
|
138
|
+
'button',
|
|
139
|
+
{
|
|
140
|
+
type: 'button',
|
|
141
|
+
className: 'drp-pill',
|
|
142
|
+
style: { opacity: 0.82 },
|
|
143
|
+
title: t('pill.tooltip') + (latest.reason ? '\n' + String(latest.reason) : ''),
|
|
144
|
+
onClick: onOpen,
|
|
145
|
+
},
|
|
146
|
+
h('span', { className: 'drp-pill-icon', 'aria-hidden': true }, '↯'),
|
|
147
|
+
h('span', { className: 'drp-pill-text' }, h('span', { className: 'drp-pill-count' }, text)),
|
|
148
|
+
h('span', { className: 'drp-pill-open' }, t('tab.title'), ' ›'),
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const summary = latest.summary
|
|
154
|
+
const files = summary && Array.isArray(summary.files) ? summary.files : []
|
|
155
|
+
if (files.length === 0) return null
|
|
156
|
+
|
|
157
|
+
const tooltip = files.slice(0, 10).map((file) => file.display || file.path).join('\n')
|
|
158
|
+
|
|
159
|
+
return h(
|
|
160
|
+
'div',
|
|
161
|
+
{ className: 'drp-pill-row' },
|
|
162
|
+
h(
|
|
163
|
+
'button',
|
|
164
|
+
{
|
|
165
|
+
type: 'button',
|
|
166
|
+
className: 'drp-pill',
|
|
167
|
+
title: (tooltip ? tooltip + '\n' : '') + t('pill.tooltip'),
|
|
168
|
+
onClick: onOpen,
|
|
169
|
+
},
|
|
170
|
+
h('span', { className: 'drp-pill-icon', 'aria-hidden': true }, '↯'),
|
|
171
|
+
h(
|
|
172
|
+
'span',
|
|
173
|
+
{ className: 'drp-pill-text' },
|
|
174
|
+
h('span', { className: 'drp-pill-count' }, t('pill.changes', { count: String(files.length) })),
|
|
175
|
+
h(
|
|
176
|
+
'span',
|
|
177
|
+
{ className: 'drp-pill-stats' },
|
|
178
|
+
h('span', { className: 'drp-badge-add' }, '+' + fmtInt(summary.added)),
|
|
179
|
+
h('span', { className: 'drp-badge-del' }, '−' + fmtInt(summary.deleted)),
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
h('span', { className: 'drp-pill-open' }, t('pill.open'), ' ›'),
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/* dsh-git-changes client half — official shortcuts registration and command dispatch. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build one official shortcut binding.
|
|
5
|
+
* @param {string} code physical key code
|
|
6
|
+
* @param {string[]} [modifiers] logical modifiers
|
|
7
|
+
* @returns {{code:string, modifiers:string[]}}
|
|
8
|
+
*/
|
|
9
|
+
function bindingOf(code, modifiers) {
|
|
10
|
+
return { code, modifiers: modifiers || [] }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Human-readable label for one shortcut binding in the active client profile.
|
|
15
|
+
* @param {{code:string, modifiers:string[]}|undefined} binding binding
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function formatBinding(binding, environment = shortcutEnvironment()) {
|
|
19
|
+
if (!binding || !binding.code) return ''
|
|
20
|
+
const parts = (binding.modifiers || []).map((modifier) => shortcutModifierLabel(modifier, environment))
|
|
21
|
+
const keyOf = (code) => String(code)
|
|
22
|
+
.replace(/^Key/, '')
|
|
23
|
+
.replace(/^Digit/, '')
|
|
24
|
+
.replace(/^Arrow/, '')
|
|
25
|
+
const key = keyOf(binding.code) + (binding.secondCode ? '+' + keyOf(binding.secondCode) : '')
|
|
26
|
+
return parts.concat(key).join('+')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Effective registration outcome per plugin command id: `default` when the
|
|
31
|
+
* declared key combination was accepted, `unbound` when the product rejected
|
|
32
|
+
* it (the command is still registered but without a key), `failed` when even
|
|
33
|
+
* the unbound registration failed. The in-page shortcut panel reports this so
|
|
34
|
+
* a rejected default is visible instead of silently doing nothing.
|
|
35
|
+
*/
|
|
36
|
+
const SHORTCUT_STATUS = new Map()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Shortcut rows for the in-page "快捷键" panel: the plugin command defaults
|
|
40
|
+
* plus the editor-native keys this read-only view supports.
|
|
41
|
+
* @param {(key:string) => string} t translator
|
|
42
|
+
* @returns {Array<{id:string, label:() => string, hint:string}>}
|
|
43
|
+
*/
|
|
44
|
+
function buildKeyHelp(t) {
|
|
45
|
+
const rows = []
|
|
46
|
+
const environment = shortcutEnvironment()
|
|
47
|
+
const profile = environment.runtime + ':' + environment.platform
|
|
48
|
+
const primary = shortcutModifierLabel('primary', environment)
|
|
49
|
+
const foldModifier = environment.platform === 'macos' ? shortcutModifierLabel('alt', environment) : 'Shift'
|
|
50
|
+
const quickOpenBinding = COMMAND_SPECS.find((spec) => spec.id === 'quickOpen')?.defaults?.[profile]
|
|
51
|
+
|| COMMAND_SPECS.find((spec) => spec.id === 'quickOpenAlt')?.defaults?.[profile]
|
|
52
|
+
const quickOpenLabel = formatBinding(quickOpenBinding, environment) || primary + '+P'
|
|
53
|
+
for (const spec of COMMAND_SPECS) {
|
|
54
|
+
const label = formatBinding(spec.defaults && spec.defaults[profile], environment)
|
|
55
|
+
if (!label) continue
|
|
56
|
+
const status = SHORTCUT_STATUS.get(spec.id)
|
|
57
|
+
const suffix = status === 'unbound' ? t('keys.unbound') : status === 'failed' ? t('keys.failed') : ''
|
|
58
|
+
rows.push({ id: 'k:' + spec.id, label: () => t(spec.message), hint: label + suffix })
|
|
59
|
+
}
|
|
60
|
+
rows.push(
|
|
61
|
+
{ id: 'k:find', label: () => '编辑器内查找', hint: primary + '+F' },
|
|
62
|
+
{ id: 'k:replace', label: () => '查找下一个', hint: primary + '+F → Enter' },
|
|
63
|
+
{ id: 'k:fold', label: () => '折叠 / 展开代码块', hint: primary + '+' + foldModifier + '+[ / ]' },
|
|
64
|
+
{ id: 'k:foldAll', label: () => '全部折叠 / 展开', hint: primary + '+K ' + primary + '+0 / ' + primary + '+K ' + primary + '+J' },
|
|
65
|
+
{ id: 'k:fontZoom', label: () => '字号增大 / 减小 / 重置', hint: 'Ctrl+滚轮 · ' + primary + '+= / - / 0' },
|
|
66
|
+
{ id: 'k:lspDefinition', label: () => '跳转到定义', hint: 'F12' },
|
|
67
|
+
{ id: 'k:lspReferences', label: () => '查找引用', hint: 'Shift+F12' },
|
|
68
|
+
{ id: 'k:lspHover', label: () => '悬停提示(类型/文档)', hint: '鼠标悬停' },
|
|
69
|
+
{ id: 'k:quickOpenCommands', label: () => '快速打开:输入 > 运行命令', hint: quickOpenLabel + ' 后输入 >' },
|
|
70
|
+
{ id: 'k:quickOpenLine', label: () => '快速打开:输入 : 跳转行', hint: quickOpenLabel + ' 后输入 :' },
|
|
71
|
+
)
|
|
72
|
+
return rows
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Command catalogue: single data source for official shortcuts (and later the
|
|
77
|
+
* command palette). Linux declares allowed letter combinations; its reserved
|
|
78
|
+
* function/arrow/tab keys and Web profiles stay available through buttons,
|
|
79
|
+
* in-page fallback handlers, or explicit bindings in the official settings.
|
|
80
|
+
*/
|
|
81
|
+
const COMMAND_SPECS = [
|
|
82
|
+
{ id: 'openReview', message: 'cmd.openReview', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyD', ['primary', 'alt']), 'desktop:macos': bindingOf('KeyD', ['primary', 'alt']), 'desktop:linux': bindingOf('KeyD', ['primary', 'alt']) } },
|
|
83
|
+
{ id: 'nextChange', message: 'cmd.nextChange', regions: ['page', 'editable', 'terminal'], defaults: { 'desktop:windows': bindingOf('F7'), 'desktop:macos': bindingOf('F7') } },
|
|
84
|
+
{ id: 'prevChange', message: 'cmd.prevChange', regions: ['page', 'editable', 'terminal'], defaults: { 'desktop:windows': bindingOf('F7', ['shift']), 'desktop:macos': bindingOf('F7', ['shift']) } },
|
|
85
|
+
{ id: 'nextTab', message: 'cmd.nextTab', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('Tab', ['primary']), 'desktop:macos': bindingOf('Tab', ['primary']) } },
|
|
86
|
+
{ id: 'prevTab', message: 'cmd.prevTab', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('Tab', ['primary', 'shift']), 'desktop:macos': bindingOf('Tab', ['primary', 'shift']) } },
|
|
87
|
+
{ id: 'closeTab', message: 'cmd.closeTab', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('F4', ['primary']), 'desktop:macos': bindingOf('F4', ['primary']) } },
|
|
88
|
+
{ id: 'navBack', message: 'cmd.navBack', regions: ['page'], defaults: {} },
|
|
89
|
+
{ id: 'navForward', message: 'cmd.navForward', regions: ['page'], defaults: {} },
|
|
90
|
+
{ id: 'toggleTree', message: 'cmd.toggleTree', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyE', ['primary', 'shift']), 'desktop:macos': bindingOf('KeyE', ['primary', 'shift']), 'desktop:linux': bindingOf('KeyE', ['primary', 'shift']) } },
|
|
91
|
+
{ id: 'refresh', message: 'cmd.refresh', regions: ['page', 'editable'], defaults: {} },
|
|
92
|
+
{ id: 'copyPath', message: 'cmd.copyPath', regions: ['page', 'editable'], defaults: {} },
|
|
93
|
+
{ id: 'openInPreview', message: 'cmd.openInPreview', regions: ['page', 'editable'], defaults: {} },
|
|
94
|
+
// VS Code–style pickers. Primary keys follow VS Code (Mod+P quick open,
|
|
95
|
+
// Mod+Shift+P command palette, Mod+Shift+F search, Mod+G goto line); the
|
|
96
|
+
// Mod+Alt+P variant stays as a fallback when the product owns Mod+P.
|
|
97
|
+
// `editable` is included so the keys also work while the Monaco editor has
|
|
98
|
+
// focus (none of these chords is a Monaco-native binding).
|
|
99
|
+
{ id: 'outline', message: 'cmd.outline', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyO', ['primary', 'shift']), 'desktop:macos': bindingOf('KeyO', ['primary', 'shift']), 'desktop:linux': bindingOf('KeyO', ['primary', 'shift']) } },
|
|
100
|
+
{ id: 'lspRecheck', message: 'lsp.recheck', regions: ['page', 'editable'], defaults: {} },
|
|
101
|
+
{ id: 'quickOpen', message: 'cmd.quickOpen', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyP', ['primary']), 'desktop:macos': bindingOf('KeyP', ['primary']) } },
|
|
102
|
+
{ id: 'quickOpenAlt', message: 'cmd.quickOpen', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyP', ['primary', 'alt']), 'desktop:macos': bindingOf('KeyP', ['primary', 'alt']), 'desktop:linux': bindingOf('KeyP', ['primary', 'alt']) } },
|
|
103
|
+
{ id: 'gotoLine', message: 'cmd.gotoLine', regions: ['page'], defaults: { 'desktop:windows': bindingOf('KeyG', ['primary']), 'desktop:macos': bindingOf('KeyG', ['primary']), 'desktop:linux': bindingOf('KeyG', ['primary']) } },
|
|
104
|
+
{ id: 'searchWorkspace', message: 'cmd.search', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyF', ['primary', 'shift']), 'desktop:macos': bindingOf('KeyF', ['primary', 'shift']), 'desktop:linux': bindingOf('KeyF', ['primary', 'shift']) } },
|
|
105
|
+
{ id: 'commandPalette', message: 'cmd.commands', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyP', ['primary', 'shift']), 'desktop:macos': bindingOf('KeyP', ['primary', 'shift']), 'desktop:linux': bindingOf('KeyP', ['primary', 'shift']) } },
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Run one plugin command against the mounted page, or the global open action.
|
|
110
|
+
* @param {string} id command id (without the plugin prefix)
|
|
111
|
+
* @returns {boolean} whether something ran
|
|
112
|
+
*/
|
|
113
|
+
function runCommand(id) {
|
|
114
|
+
if (id === 'openReview') return openReviewFromChat()
|
|
115
|
+
const page = runtimeState.page
|
|
116
|
+
if (!page) return false
|
|
117
|
+
return page.command(id)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Open the review tab for the last known session (global command).
|
|
122
|
+
* @returns {boolean} whether the open call was issued
|
|
123
|
+
*/
|
|
124
|
+
function openReviewFromChat() {
|
|
125
|
+
const sessionId = runtimeState.sessionId
|
|
126
|
+
if (!sessionId) return false
|
|
127
|
+
const ok = openResourceSafe(runtimeState.right, ADDRESS_PREFIX + encodeURIComponent(sessionId))
|
|
128
|
+
if (!ok) log('warn', 'openReview: sidebarRight unavailable')
|
|
129
|
+
return ok
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Command-specific visibility rules: only claim a key when this plugin's page
|
|
134
|
+
* (or the global open action) is the right target.
|
|
135
|
+
* @param {string} id command id
|
|
136
|
+
* @param {any} context official shortcut invoke context
|
|
137
|
+
* @returns {{status:'handled',run:Function}|{status:'pass'}}
|
|
138
|
+
*/
|
|
139
|
+
function resolveCommand(id, context) {
|
|
140
|
+
if (context && context.modal !== null && context.modal !== undefined) return { status: 'pass' }
|
|
141
|
+
if (id === 'openReview') {
|
|
142
|
+
if (!runtimeState.sessionId) return { status: 'pass' }
|
|
143
|
+
return { status: 'handled', run: () => { runCommand('openReview') } }
|
|
144
|
+
}
|
|
145
|
+
const page = runtimeState.page
|
|
146
|
+
if (!page) return { status: 'pass' }
|
|
147
|
+
const target = context && context.target
|
|
148
|
+
if (target && !page.contains(target)) return { status: 'pass' }
|
|
149
|
+
return { status: 'handled', run: () => { runCommand(id) } }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Register one command, downgrading to an unbound command when its default
|
|
154
|
+
* combination is rejected (duplicate id / reserved / conflicting defaults).
|
|
155
|
+
* @param {any} service official shortcuts service
|
|
156
|
+
* @param {object} spec command spec
|
|
157
|
+
* @param {Function} t locale-bound translator
|
|
158
|
+
* @returns {() => void} disposer
|
|
159
|
+
*/
|
|
160
|
+
function safeRegisterCommand(service, spec, t) {
|
|
161
|
+
const command = {
|
|
162
|
+
id: 'dsh-git-changes.' + spec.id,
|
|
163
|
+
label: () => t(spec.message),
|
|
164
|
+
aliases: [],
|
|
165
|
+
defaults: spec.defaults,
|
|
166
|
+
regions: spec.regions,
|
|
167
|
+
modals: [],
|
|
168
|
+
resolve: (context) => resolveCommand(spec.id, context),
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const dispose = service.register(command)
|
|
172
|
+
SHORTCUT_STATUS.set(spec.id, 'default')
|
|
173
|
+
return dispose
|
|
174
|
+
} catch (error) {
|
|
175
|
+
log('warn', 'shortcut default rejected, registering unbound:', command.id, String(error))
|
|
176
|
+
try {
|
|
177
|
+
const dispose = service.register(Object.assign({}, command, { defaults: {} }))
|
|
178
|
+
SHORTCUT_STATUS.set(spec.id, 'unbound')
|
|
179
|
+
return dispose
|
|
180
|
+
} catch (retryError) {
|
|
181
|
+
log('warn', 'shortcut registration failed:', command.id, String(retryError))
|
|
182
|
+
SHORTCUT_STATUS.set(spec.id, 'failed')
|
|
183
|
+
return () => {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Detect the optional official shortcuts service with a bounded retry, then
|
|
190
|
+
* register the whole catalog exactly once.
|
|
191
|
+
* @param {object} ctx client plugin context
|
|
192
|
+
* @param {Function} t locale-bound translator
|
|
193
|
+
* @returns {() => void} cancel/dispose
|
|
194
|
+
*/
|
|
195
|
+
function installOfficialShortcuts(ctx, t) {
|
|
196
|
+
let stopped = false
|
|
197
|
+
let timer = null
|
|
198
|
+
let attempts = 0
|
|
199
|
+
let registered = []
|
|
200
|
+
|
|
201
|
+
const register = (service) => {
|
|
202
|
+
if (stopped) return
|
|
203
|
+
for (const spec of COMMAND_SPECS) {
|
|
204
|
+
try {
|
|
205
|
+
const dispose = safeRegisterCommand(service, spec, t)
|
|
206
|
+
registered.push(dispose)
|
|
207
|
+
} catch (error) {
|
|
208
|
+
log('warn', 'shortcut setup failed', spec.id, String(error))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
log('log', 'official shortcuts registered:', COMMAND_SPECS.length)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const timerTick = () => {
|
|
215
|
+
if (stopped) return
|
|
216
|
+
let service
|
|
217
|
+
try {
|
|
218
|
+
service = safeGet(ctx, 'shortcuts')
|
|
219
|
+
} catch (error) {
|
|
220
|
+
service = undefined
|
|
221
|
+
}
|
|
222
|
+
if (service && typeof service.register === 'function' && service.catalog && service.config) {
|
|
223
|
+
register(service)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (attempts >= 15) {
|
|
227
|
+
log('warn', 'official shortcuts service unavailable; plugin commands stay unbound')
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
attempts += 1
|
|
231
|
+
timer = setTimeout(timerTick, 2000)
|
|
232
|
+
}
|
|
233
|
+
timerTick()
|
|
234
|
+
|
|
235
|
+
return () => {
|
|
236
|
+
stopped = true
|
|
237
|
+
if (timer) {
|
|
238
|
+
clearTimeout(timer)
|
|
239
|
+
timer = null
|
|
240
|
+
}
|
|
241
|
+
for (const dispose of registered) {
|
|
242
|
+
try {
|
|
243
|
+
dispose()
|
|
244
|
+
} catch (error) { /* ignore */ }
|
|
245
|
+
}
|
|
246
|
+
registered = []
|
|
247
|
+
}
|
|
248
|
+
}
|