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,457 @@
|
|
|
1
|
+
/* dsh-git-changes client half — Quick Open / workspace search / command palette overlay. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fuzzy subsequence score for Quick Open (higher is better; 0 = no match).
|
|
5
|
+
* Consecutive matches and a basename hit score higher; a path hit still counts.
|
|
6
|
+
* @param {string} query typed query
|
|
7
|
+
* @param {string} target candidate path
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
function fuzzyScore(query, target) {
|
|
11
|
+
const needle = String(query || '').toLowerCase()
|
|
12
|
+
const haystack = String(target || '').toLowerCase()
|
|
13
|
+
if (needle === '') return 1
|
|
14
|
+
if (haystack.length === 0) return 0
|
|
15
|
+
const baseStart = haystack.lastIndexOf('/') + 1
|
|
16
|
+
let score = 0
|
|
17
|
+
let index = 0
|
|
18
|
+
let streak = 0
|
|
19
|
+
for (const character of needle) {
|
|
20
|
+
let at = haystack.indexOf(character, index)
|
|
21
|
+
if (at < 0) return 0
|
|
22
|
+
streak = at === index ? streak + 1 : 0
|
|
23
|
+
score += 2 + streak
|
|
24
|
+
if (at >= baseStart) score += 2
|
|
25
|
+
index = at + 1
|
|
26
|
+
}
|
|
27
|
+
if (haystack.endsWith(needle)) score += 6
|
|
28
|
+
return score
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Filter and rank paths for Quick Open.
|
|
33
|
+
* @param {string[]} files candidate paths
|
|
34
|
+
* @param {string} query typed query
|
|
35
|
+
* @param {number} [limit] max results
|
|
36
|
+
* @returns {Array<{path:string, score:number}>}
|
|
37
|
+
*/
|
|
38
|
+
function rankQuickOpen(files, query, limit = 100) {
|
|
39
|
+
const needle = String(query || '').trim()
|
|
40
|
+
if (needle === '') {
|
|
41
|
+
return files.slice(0, limit).map((path) => ({ path, score: 1 }))
|
|
42
|
+
}
|
|
43
|
+
const ranked = []
|
|
44
|
+
for (const path of files) {
|
|
45
|
+
const score = fuzzyScore(needle, path)
|
|
46
|
+
if (score > 0) ranked.push({ path, score })
|
|
47
|
+
}
|
|
48
|
+
ranked.sort((left, right) => right.score - left.score || left.path.length - right.path.length || left.path.localeCompare(right.path))
|
|
49
|
+
return ranked.slice(0, limit)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Short display date for one revision: `MM-DD` for dates in the current calendar
|
|
54
|
+
* year, otherwise the full `YYYY-MM-DD`; unparseable values render as ''.
|
|
55
|
+
* @param {string} iso ISO-8601 date from git
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function formatRevisionDate(iso) {
|
|
59
|
+
// Git dates carry the author's own offset; show that calendar date as-is
|
|
60
|
+
// instead of shifting it into the viewer's time zone (matches `git log`).
|
|
61
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ''))
|
|
62
|
+
if (!match) return ''
|
|
63
|
+
const year = match[1]
|
|
64
|
+
const monthDay = match[2] + '-' + match[3]
|
|
65
|
+
return year === String(new Date().getFullYear()) ? monthDay : year + '-' + monthDay
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether typed picker text should be offered as a raw git revision expression.
|
|
70
|
+
* Shortened to the cases where the query clearly is not a commit subject: a
|
|
71
|
+
* sha-like token, a ref path, a `HEAD~2`-style expression or an `@{...}` ref.
|
|
72
|
+
* A plain word (branch/tag name or subject text) only becomes a raw revision
|
|
73
|
+
* when no loaded commit matches it.
|
|
74
|
+
* @param {string} value typed query
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
function isRevisionLikeQuery(value) {
|
|
78
|
+
const text = String(value || '').trim()
|
|
79
|
+
if (text === '') return false
|
|
80
|
+
if (/^[0-9a-f]{4,64}$/i.test(text)) return true
|
|
81
|
+
if (/^refs\//.test(text) || text.startsWith('@')) return true
|
|
82
|
+
if (/^HEAD([~^]|$)/.test(text) || /^tag[:/]/.test(text) || /^origin\//.test(text)) return true
|
|
83
|
+
return /[~^@]/.test(text) || /\//.test(text) && /^[A-Za-z0-9_.\/-]+$/.test(text)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One picker overlay: Quick Open, workspace search, the command palette, or the
|
|
88
|
+
* git-version picker (mode `revisions`).
|
|
89
|
+
* @param {object} props component props
|
|
90
|
+
* @returns {any} React element
|
|
91
|
+
*/
|
|
92
|
+
function PickerOverlay(props) {
|
|
93
|
+
const {
|
|
94
|
+
mode, t, sessionId, commands, onPickFile, onPickCommand, onClose, initialQuery,
|
|
95
|
+
activePath, lineCount,
|
|
96
|
+
revisionScope, onRevisionScopeChange, onPickRevision,
|
|
97
|
+
} = props
|
|
98
|
+
const inputRef = React.useRef(null)
|
|
99
|
+
const [query, setQuery] = React.useState(initialQuery || '')
|
|
100
|
+
const [files, setFiles] = React.useState(null)
|
|
101
|
+
const [filesTruncated, setFilesTruncated] = React.useState(false)
|
|
102
|
+
const [fileError, setFileError] = React.useState(null)
|
|
103
|
+
const [searchState, setSearchState] = React.useState({ status: 'idle', results: [], scanned: 0, truncated: false })
|
|
104
|
+
const [revisionState, setRevisionState] = React.useState({ status: 'idle', data: null, message: '' })
|
|
105
|
+
const [activeIndex, setActiveIndex] = React.useState(0)
|
|
106
|
+
const debounceRef = React.useRef(null)
|
|
107
|
+
// Optional props: the picker stays usable when the page omits the version props.
|
|
108
|
+
const scope = revisionScope === 'since' ? 'since' : 'commit'
|
|
109
|
+
|
|
110
|
+
// VS Code Quick Open prefixes: `>` runs a command, `:` jumps to a line.
|
|
111
|
+
const trimmed = query.trimStart()
|
|
112
|
+
const effectiveMode = mode === 'quickopen'
|
|
113
|
+
? (trimmed.startsWith('>') ? 'commands' : trimmed.startsWith(':') ? 'line' : 'quickopen')
|
|
114
|
+
: mode
|
|
115
|
+
const prefixQuery = effectiveMode === 'commands' && mode === 'quickopen'
|
|
116
|
+
? trimmed.slice(1).trim()
|
|
117
|
+
: effectiveMode === 'line' && mode === 'quickopen' ? trimmed.slice(1).trim() : query
|
|
118
|
+
|
|
119
|
+
const title = mode === 'revisions'
|
|
120
|
+
? t('picker.revisions.title')
|
|
121
|
+
: mode === 'locations' ? t(props.locationKind === 'definitions' ? 'picker.definitions.title' : 'picker.locations.title')
|
|
122
|
+
: mode === 'outline' ? t('picker.outline.title')
|
|
123
|
+
: mode === 'keys'
|
|
124
|
+
? t('picker.keys.title')
|
|
125
|
+
: mode === 'line'
|
|
126
|
+
? t('picker.line.title')
|
|
127
|
+
: mode === 'quickopen'
|
|
128
|
+
? t('picker.quickOpen.title')
|
|
129
|
+
: mode === 'search' ? t('picker.search.title') : t('picker.commands.title')
|
|
130
|
+
const placeholder = mode === 'revisions'
|
|
131
|
+
? t('picker.revisions.placeholder')
|
|
132
|
+
: mode === 'locations' ? t('picker.locations.placeholder')
|
|
133
|
+
: mode === 'outline' ? t('picker.outline.placeholder')
|
|
134
|
+
: mode === 'keys'
|
|
135
|
+
? t('picker.keys.placeholder')
|
|
136
|
+
: mode === 'line'
|
|
137
|
+
? t('picker.line.placeholder')
|
|
138
|
+
: mode === 'quickopen'
|
|
139
|
+
? t('picker.quickOpen.placeholder')
|
|
140
|
+
: mode === 'search' ? t('picker.search.placeholder') : t('picker.commands.placeholder')
|
|
141
|
+
|
|
142
|
+
// Focus the input as soon as the overlay opens.
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
try { inputRef.current?.focus() } catch (error) { /* ignore */ }
|
|
146
|
+
}, 20)
|
|
147
|
+
return () => clearTimeout(timer)
|
|
148
|
+
}, [])
|
|
149
|
+
|
|
150
|
+
// Quick Open: load the (cached) workspace index once.
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
if (mode !== 'quickopen' || !sessionId) return undefined
|
|
153
|
+
let cancelled = false
|
|
154
|
+
apiGet('files', { sessionId })
|
|
155
|
+
.then((value) => {
|
|
156
|
+
if (cancelled) return
|
|
157
|
+
setFiles(Array.isArray(value?.files) ? value.files : [])
|
|
158
|
+
setFilesTruncated(value?.truncated === true)
|
|
159
|
+
})
|
|
160
|
+
.catch((failure) => {
|
|
161
|
+
if (cancelled) return
|
|
162
|
+
setFiles([])
|
|
163
|
+
setFileError(failure?.message || String(failure))
|
|
164
|
+
})
|
|
165
|
+
return () => { cancelled = true }
|
|
166
|
+
}, [mode, sessionId])
|
|
167
|
+
|
|
168
|
+
// Git versions: load the bounded revision list once per picker open; the token
|
|
169
|
+
// guard drops late responses when the overlay closes or the mode changes.
|
|
170
|
+
React.useEffect(() => {
|
|
171
|
+
if (mode !== 'revisions') return undefined
|
|
172
|
+
let cancelled = false
|
|
173
|
+
setRevisionState({ status: 'loading', data: null, message: '' })
|
|
174
|
+
if (!sessionId) {
|
|
175
|
+
setRevisionState({ status: 'error', data: null, message: t('error.noSession') })
|
|
176
|
+
return () => { cancelled = true }
|
|
177
|
+
}
|
|
178
|
+
apiGet('gitRevisions', { sessionId })
|
|
179
|
+
.then((value) => {
|
|
180
|
+
if (cancelled) return
|
|
181
|
+
setRevisionState({ status: 'ready', data: value || null, message: '' })
|
|
182
|
+
})
|
|
183
|
+
.catch((failure) => {
|
|
184
|
+
if (cancelled) return
|
|
185
|
+
setRevisionState({ status: 'error', data: null, message: failure && failure.message ? failure.message : String(failure) })
|
|
186
|
+
})
|
|
187
|
+
return () => { cancelled = true }
|
|
188
|
+
}, [mode, sessionId])
|
|
189
|
+
|
|
190
|
+
// Workspace search: debounced remote query.
|
|
191
|
+
React.useEffect(() => {
|
|
192
|
+
if (mode !== 'search') return undefined
|
|
193
|
+
const needle = query.trim()
|
|
194
|
+
if (needle.length < 2) {
|
|
195
|
+
setSearchState({ status: 'idle', results: [], scanned: 0, truncated: false })
|
|
196
|
+
return undefined
|
|
197
|
+
}
|
|
198
|
+
setSearchState({ status: 'searching', results: [], scanned: 0, truncated: false })
|
|
199
|
+
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
200
|
+
let cancelled = false
|
|
201
|
+
debounceRef.current = setTimeout(() => {
|
|
202
|
+
apiGet('search', { sessionId, q: needle, limit: 100 })
|
|
203
|
+
.then((value) => {
|
|
204
|
+
if (cancelled) return
|
|
205
|
+
setSearchState({
|
|
206
|
+
status: 'ready',
|
|
207
|
+
results: Array.isArray(value?.results) ? value.results : [],
|
|
208
|
+
scanned: Number(value?.scanned) || 0,
|
|
209
|
+
truncated: value?.truncated === true,
|
|
210
|
+
})
|
|
211
|
+
setActiveIndex(0)
|
|
212
|
+
})
|
|
213
|
+
.catch((failure) => {
|
|
214
|
+
if (cancelled) return
|
|
215
|
+
setSearchState({ status: 'error', results: [], scanned: 0, truncated: false, message: failure?.message || String(failure) })
|
|
216
|
+
})
|
|
217
|
+
}, 250)
|
|
218
|
+
return () => {
|
|
219
|
+
cancelled = true
|
|
220
|
+
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
221
|
+
}
|
|
222
|
+
}, [mode, query, sessionId])
|
|
223
|
+
|
|
224
|
+
const commandItems = React.useMemo(() => {
|
|
225
|
+
if (effectiveMode !== 'commands' && effectiveMode !== 'keys' && mode !== 'locations' && mode !== 'outline') return []
|
|
226
|
+
const needle = prefixQuery.trim().toLowerCase()
|
|
227
|
+
const list = (commands || []).map((command) => ({
|
|
228
|
+
id: command.id,
|
|
229
|
+
label: typeof command.label === 'function' ? command.label() : String(command.label || command.id),
|
|
230
|
+
hint: command.hint || '',
|
|
231
|
+
}))
|
|
232
|
+
if (needle === '') return list
|
|
233
|
+
return list.filter((item) => (item.label + ' ' + item.hint).toLowerCase().includes(needle))
|
|
234
|
+
}, [effectiveMode, commands, prefixQuery])
|
|
235
|
+
|
|
236
|
+
const quickItems = React.useMemo(() => {
|
|
237
|
+
if (effectiveMode !== 'quickopen' || !files) return []
|
|
238
|
+
return rankQuickOpen(files, prefixQuery, 100)
|
|
239
|
+
}, [effectiveMode, files, prefixQuery])
|
|
240
|
+
|
|
241
|
+
// Git versions: loaded payload, typed-query filter, scope toggle and offered rows.
|
|
242
|
+
const revisionData = revisionState.data
|
|
243
|
+
const revisionCommits = revisionData && Array.isArray(revisionData.commits) ? revisionData.commits : []
|
|
244
|
+
const revisionNeedle = query.trim().toLowerCase()
|
|
245
|
+
const revisionRaw = query.trim()
|
|
246
|
+
const revisionMatches = revisionNeedle === ''
|
|
247
|
+
? revisionCommits
|
|
248
|
+
: revisionCommits.filter((commit) => (
|
|
249
|
+
[commit.sha, commit.short, commit.subject, commit.author, Array.isArray(commit.refs) ? commit.refs.join(' ') : '']
|
|
250
|
+
.filter(Boolean).join(' ').toLowerCase().includes(revisionNeedle)
|
|
251
|
+
))
|
|
252
|
+
// Offer the typed expression as a raw revision unless it plainly looks like a
|
|
253
|
+
// commit-subject search that already matched rows.
|
|
254
|
+
const offerRawRevision = revisionRaw !== '' && (revisionMatches.length === 0 || isRevisionLikeQuery(revisionRaw))
|
|
255
|
+
const revisionRows = effectiveMode !== 'revisions' ? [] : [
|
|
256
|
+
...(offerRawRevision
|
|
257
|
+
? [{ key: 'revraw:' + revisionRaw, id: 'revraw:' + revisionRaw, primary: t('picker.revisions.raw', { query: revisionRaw }), raw: revisionRaw }]
|
|
258
|
+
: []),
|
|
259
|
+
...revisionMatches.map((commit) => ({
|
|
260
|
+
key: 'rev:' + commit.sha,
|
|
261
|
+
id: 'rev:' + commit.sha,
|
|
262
|
+
primary: String(commit.short || commit.sha || '') + (commit.subject ? ' ' + commit.subject : ''),
|
|
263
|
+
secondary: [
|
|
264
|
+
Array.isArray(commit.refs) ? commit.refs.join(' ') : '',
|
|
265
|
+
formatRevisionDate(commit.date),
|
|
266
|
+
commit.author,
|
|
267
|
+
].filter(Boolean).join(' · '),
|
|
268
|
+
commit,
|
|
269
|
+
})),
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
/** Status copy for the revisions mode: head/branch/upstream/ahead summary. */
|
|
273
|
+
const revisionStatus = (() => {
|
|
274
|
+
if (revisionState.status === 'error') return revisionState.message || t('picker.empty')
|
|
275
|
+
if (revisionState.status !== 'ready') return t('picker.revisions.loading')
|
|
276
|
+
if (revisionData && revisionData.available === true) {
|
|
277
|
+
const head = revisionData.head
|
|
278
|
+
if (revisionData.unborn === true || !head || revisionCommits.length === 0) return t('picker.revisions.none')
|
|
279
|
+
const parts = [t('picker.revisions.head', { ref: head.short || head.sha || '' })]
|
|
280
|
+
if (head.detached === true) parts.push(t('picker.revisions.detached'))
|
|
281
|
+
else if (head.branch) {
|
|
282
|
+
parts.push(head.upstream
|
|
283
|
+
? t('picker.revisions.branchUpstream', { branch: head.branch, upstream: head.upstream })
|
|
284
|
+
: t('picker.revisions.branch', { branch: head.branch }))
|
|
285
|
+
}
|
|
286
|
+
if (Number(head.ahead) > 0) parts.push(t('picker.revisions.ahead', { count: String(head.ahead) }))
|
|
287
|
+
if (revisionNeedle !== '' && revisionMatches.length === 0) parts.push(t('picker.revisions.empty'))
|
|
288
|
+
return parts.join(' · ')
|
|
289
|
+
}
|
|
290
|
+
return t('mode.revision.unavailable')
|
|
291
|
+
})()
|
|
292
|
+
|
|
293
|
+
/** Empty-list copy for revisions (no commits yet vs no filter match). */
|
|
294
|
+
const revisionEmptyText = revisionState.status === 'ready' && revisionData && revisionData.available === true
|
|
295
|
+
? (revisionCommits.length === 0 ? t('picker.revisions.none') : t('picker.revisions.empty'))
|
|
296
|
+
: revisionStatus
|
|
297
|
+
|
|
298
|
+
const rows = effectiveMode === 'revisions'
|
|
299
|
+
? revisionRows
|
|
300
|
+
: effectiveMode === 'quickopen'
|
|
301
|
+
? quickItems.map((item) => ({ key: 'f:' + item.path, primary: item.path, secondary: '' }))
|
|
302
|
+
: effectiveMode === 'search'
|
|
303
|
+
? searchState.results.map((hit) => ({
|
|
304
|
+
key: 's:' + hit.path + ':' + hit.line + ':' + hit.column,
|
|
305
|
+
primary: hit.path + ':' + hit.line,
|
|
306
|
+
secondary: hit.text || '',
|
|
307
|
+
path: hit.path,
|
|
308
|
+
line: hit.line,
|
|
309
|
+
}))
|
|
310
|
+
: effectiveMode === 'commands' || effectiveMode === 'keys' || mode === 'locations' || mode === 'outline'
|
|
311
|
+
? commandItems.map((item) => ({ key: 'c:' + item.id, primary: item.label, secondary: item.hint, id: item.id }))
|
|
312
|
+
: []
|
|
313
|
+
|
|
314
|
+
// Keep the active row in range while results shrink.
|
|
315
|
+
React.useEffect(() => {
|
|
316
|
+
setActiveIndex((current) => (current < rows.length ? current : Math.max(0, rows.length - 1)))
|
|
317
|
+
}, [rows.length])
|
|
318
|
+
|
|
319
|
+
/** Jump to a typed line number in the active file. */
|
|
320
|
+
const jumpToLine = () => {
|
|
321
|
+
const line = Number(prefixQuery.trim())
|
|
322
|
+
const total = Number(lineCount) || 0
|
|
323
|
+
if (!Number.isFinite(line) || line <= 0 || (total > 0 && line > total)) return false
|
|
324
|
+
onPickFile?.(activePath, line)
|
|
325
|
+
return true
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Activate one result row. */
|
|
329
|
+
const choose = (row) => {
|
|
330
|
+
if (effectiveMode === 'line') { jumpToLine(); return }
|
|
331
|
+
if (!row) return
|
|
332
|
+
if (effectiveMode === 'quickopen') { onPickFile?.(row.primary, undefined); return }
|
|
333
|
+
if (effectiveMode === 'search') { onPickFile?.(row.path, row.line); return }
|
|
334
|
+
if (effectiveMode === 'revisions') {
|
|
335
|
+
if (typeof row.raw === 'string') onPickRevision?.({ raw: row.raw })
|
|
336
|
+
else if (row.commit) onPickRevision?.(row.commit)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
onPickCommand?.(row.id)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Keyboard handling: arrows, Enter, Escape. */
|
|
343
|
+
const onKeyDown = (event) => {
|
|
344
|
+
if (event.key === 'Escape') {
|
|
345
|
+
try { event.stopPropagation() } catch (error) { /* ignore */ }
|
|
346
|
+
onClose?.()
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
if (event.key === 'ArrowDown') {
|
|
350
|
+
try { event.preventDefault() } catch (error) { /* ignore */ }
|
|
351
|
+
setActiveIndex((current) => (rows.length === 0 ? 0 : (current + 1) % rows.length))
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
if (event.key === 'ArrowUp') {
|
|
355
|
+
try { event.preventDefault() } catch (error) { /* ignore */ }
|
|
356
|
+
setActiveIndex((current) => (rows.length === 0 ? 0 : (current - 1 + rows.length) % rows.length))
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
if (event.key === 'Enter') {
|
|
360
|
+
try { event.preventDefault() } catch (error) { /* ignore */ }
|
|
361
|
+
choose(rows[activeIndex])
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const totalLines = Number(lineCount) || 0
|
|
366
|
+
const lineNumber = Number(prefixQuery.trim())
|
|
367
|
+
const lineValid = Number.isFinite(lineNumber) && lineNumber > 0 && (totalLines === 0 || lineNumber <= totalLines)
|
|
368
|
+
|
|
369
|
+
const status = effectiveMode === 'line'
|
|
370
|
+
? (prefixQuery.trim() === ''
|
|
371
|
+
? t('picker.line.hint', { lines: String(totalLines) })
|
|
372
|
+
: lineValid ? t('picker.line.hint', { lines: String(totalLines) }) : t('picker.line.invalid'))
|
|
373
|
+
: effectiveMode === 'search'
|
|
374
|
+
? searchState.status === 'searching'
|
|
375
|
+
? t('picker.search.scanning')
|
|
376
|
+
: searchState.status === 'ready'
|
|
377
|
+
? (searchState.results.length === 0
|
|
378
|
+
? t('picker.search.none')
|
|
379
|
+
: t('picker.search.results', { count: String(searchState.results.length), scanned: String(searchState.scanned) }) + (searchState.truncated ? ' · …' : ''))
|
|
380
|
+
: searchState.status === 'error' ? t('picker.empty') : ''
|
|
381
|
+
: effectiveMode === 'revisions'
|
|
382
|
+
? revisionStatus
|
|
383
|
+
: mode === 'quickopen'
|
|
384
|
+
? (fileError || (filesTruncated ? t('picker.files.truncated') : (files ? '' : t('picker.loading'))))
|
|
385
|
+
: ''
|
|
386
|
+
|
|
387
|
+
return h(
|
|
388
|
+
'div',
|
|
389
|
+
{
|
|
390
|
+
className: 'drp-picker-backdrop',
|
|
391
|
+
onMouseDown: (event) => { try { if (event.target === event.currentTarget) onClose?.() } catch (error) { /* ignore */ } },
|
|
392
|
+
},
|
|
393
|
+
h('div', {
|
|
394
|
+
className: 'drp-picker',
|
|
395
|
+
role: 'dialog',
|
|
396
|
+
'aria-label': title,
|
|
397
|
+
onMouseDown: (event) => { try { event.stopPropagation() } catch (error) { /* ignore */ } },
|
|
398
|
+
},
|
|
399
|
+
h('div', { className: 'drp-picker-head' },
|
|
400
|
+
h('span', { className: 'drp-picker-title' }, title),
|
|
401
|
+
h('button', { type: 'button', className: 'drp-icon-btn', title: t('common.close'), onClick: () => onClose?.() }, '✕')),
|
|
402
|
+
effectiveMode === 'revisions'
|
|
403
|
+
? h('div', { className: 'drp-picker-scope' },
|
|
404
|
+
h('div', { className: 'drp-seg', role: 'group', 'aria-label': t('picker.revisions.title') },
|
|
405
|
+
h('button', {
|
|
406
|
+
type: 'button',
|
|
407
|
+
className: cx('drp-seg-btn', scope === 'commit' && 'drp-seg-btn-on'),
|
|
408
|
+
'aria-pressed': scope === 'commit',
|
|
409
|
+
title: t('picker.revisions.scope.commit.hint'),
|
|
410
|
+
onClick: () => onRevisionScopeChange?.('commit'),
|
|
411
|
+
}, t('picker.revisions.scope.commit')),
|
|
412
|
+
h('button', {
|
|
413
|
+
type: 'button',
|
|
414
|
+
className: cx('drp-seg-btn', scope === 'since' && 'drp-seg-btn-on'),
|
|
415
|
+
'aria-pressed': scope === 'since',
|
|
416
|
+
title: t('picker.revisions.scope.since.hint'),
|
|
417
|
+
onClick: () => onRevisionScopeChange?.('since'),
|
|
418
|
+
}, t('picker.revisions.scope.since'))),
|
|
419
|
+
h('span', { className: 'drp-picker-scope-label' }, t(scope === 'since' ? 'picker.revisions.scope.since.hint' : 'picker.revisions.scope.commit.hint')))
|
|
420
|
+
: null,
|
|
421
|
+
h('input', {
|
|
422
|
+
ref: inputRef,
|
|
423
|
+
className: 'drp-picker-input',
|
|
424
|
+
value: query,
|
|
425
|
+
placeholder,
|
|
426
|
+
spellCheck: false,
|
|
427
|
+
autoComplete: 'off',
|
|
428
|
+
onChange: (event) => { setQuery(event.target.value); setActiveIndex(0) },
|
|
429
|
+
onKeyDown,
|
|
430
|
+
}),
|
|
431
|
+
status ? h('div', { className: 'drp-picker-status' }, status) : null,
|
|
432
|
+
h('div', { className: 'drp-picker-list', role: 'listbox' },
|
|
433
|
+
rows.length === 0
|
|
434
|
+
? h('div', { className: 'drp-picker-empty' },
|
|
435
|
+
effectiveMode === 'line'
|
|
436
|
+
? (lineValid ? '↵ ' + t('picker.line.title') : t('picker.line.placeholder'))
|
|
437
|
+
: effectiveMode === 'revisions'
|
|
438
|
+
? revisionEmptyText
|
|
439
|
+
: mode === 'search' && query.trim().length < 2 ? t('picker.search.placeholder') : t('picker.empty'))
|
|
440
|
+
: rows.slice(0, 200).map((row, index) => h('div', {
|
|
441
|
+
key: row.key,
|
|
442
|
+
className: cx('drp-picker-row', index === activeIndex && 'drp-picker-row-active', row.commit ? 'drp-picker-row-rev' : row.raw !== undefined ? 'drp-picker-row-raw' : ''),
|
|
443
|
+
role: 'option',
|
|
444
|
+
'aria-selected': index === activeIndex,
|
|
445
|
+
title: row.primary,
|
|
446
|
+
onMouseEnter: () => setActiveIndex(index),
|
|
447
|
+
onMouseDown: (event) => { try { event.preventDefault() } catch (error) { /* ignore */ } },
|
|
448
|
+
onClick: () => choose(row),
|
|
449
|
+
},
|
|
450
|
+
h('span', { className: 'drp-picker-primary' }, row.primary),
|
|
451
|
+
row.secondary ? h('span', { className: 'drp-picker-secondary' }, row.secondary) : null,
|
|
452
|
+
))),
|
|
453
|
+
h('div', { className: 'drp-picker-foot' }, t('picker.hint.navigate')),
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/* dsh-git-changes client half — registration entry. */
|
|
2
|
+
|
|
3
|
+
/** Cordis client services required for registration; optional services are probed. */
|
|
4
|
+
const inject = ['slots', 'locale']
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the plugin's client service catalog: styles, dictionaries, the
|
|
8
|
+
* composer pill, official shortcuts, and the right-sidebar tab type with its
|
|
9
|
+
* body (or the degraded central view when the sidebar services are absent).
|
|
10
|
+
* @param {object} ctx client plugin context
|
|
11
|
+
*/
|
|
12
|
+
function apply(ctx) {
|
|
13
|
+
globalCtxRef.current = ctx
|
|
14
|
+
ctx.effect(() => () => lspDispose(), 'dsh-git-changes: client LSP lifecycle')
|
|
15
|
+
|
|
16
|
+
// Optional theme service (snapshot source; DOM markers remain the fallback).
|
|
17
|
+
try {
|
|
18
|
+
themeRuntime.service = safeGet(ctx, 'theme')
|
|
19
|
+
} catch (error) {
|
|
20
|
+
themeRuntime.service = undefined
|
|
21
|
+
}
|
|
22
|
+
themeRuntime.scheme = detectScheme()
|
|
23
|
+
|
|
24
|
+
// Optional conversation service: its per-session binding's `openTurn` source
|
|
25
|
+
// is the precise "turn closed" signal (used by the pill and the page).
|
|
26
|
+
runtimeState.conversation = safeGet(ctx, 'uiConversation')
|
|
27
|
+
|
|
28
|
+
// Plugin-owned stylesheet, cleaned up with the client run.
|
|
29
|
+
ctx.effect(() => {
|
|
30
|
+
const element = document.createElement('style')
|
|
31
|
+
element.setAttribute('data-dsh-git-changes', '1')
|
|
32
|
+
element.textContent = STYLE_TEXT
|
|
33
|
+
document.head.appendChild(element)
|
|
34
|
+
return () => {
|
|
35
|
+
try { element.remove() } catch (error) { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
}, 'dsh-git-changes: styles')
|
|
38
|
+
|
|
39
|
+
// VS Code Seti icon font (embedded woff from the generated icon theme asset).
|
|
40
|
+
ctx.effect(() => {
|
|
41
|
+
const css = typeof VSCODE_ICON_THEME !== 'undefined' && VSCODE_ICON_THEME.fontFaceCss
|
|
42
|
+
if (!css) return undefined
|
|
43
|
+
const element = document.createElement('style')
|
|
44
|
+
element.setAttribute('data-dsh-git-changes-icons', '1')
|
|
45
|
+
element.textContent = css
|
|
46
|
+
document.head.appendChild(element)
|
|
47
|
+
return () => {
|
|
48
|
+
try { element.remove() } catch (error) { /* ignore */ }
|
|
49
|
+
}
|
|
50
|
+
}, 'dsh-git-changes: icon font')
|
|
51
|
+
|
|
52
|
+
// Dictionaries follow the DSH language; there is no plugin-local override.
|
|
53
|
+
ctx.effect(() => ctx.locale.register(NS, { zh: ZH, en: EN }), 'dsh-git-changes: dictionaries')
|
|
54
|
+
let t
|
|
55
|
+
try {
|
|
56
|
+
t = ctx.locale.bind(NS)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
t = (key) => key
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Composer pill (registered independently of the sidebar; it degrades to an
|
|
62
|
+
// inline error if the review tab cannot be opened).
|
|
63
|
+
try {
|
|
64
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
65
|
+
name: 'conversation.input.dock',
|
|
66
|
+
id: 'git-changes-pill',
|
|
67
|
+
order: 30,
|
|
68
|
+
locale: NS,
|
|
69
|
+
}, ChangesPill))
|
|
70
|
+
} catch (error) {
|
|
71
|
+
log('warn', 'composer pill registration failed', error)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Official shortcuts (optional service; bounded probe inside).
|
|
75
|
+
ctx.effect(() => installOfficialShortcuts(ctx, t), 'dsh-git-changes: shortcuts')
|
|
76
|
+
|
|
77
|
+
// Primary container: the official right sidebar's new tab type.
|
|
78
|
+
mountContainer(ctx, t)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Register the resource tab type and its body on the official right sidebar.
|
|
83
|
+
* @param {object} ctx client plugin context
|
|
84
|
+
* @param {Function} t locale-bound translator
|
|
85
|
+
* @param {{ register: Function }} tabs sidebarRightTabs service
|
|
86
|
+
*/
|
|
87
|
+
function registerSidebarTab(ctx, t, tabs) {
|
|
88
|
+
ctx.effect(() => {
|
|
89
|
+
const dispose = tabs.register({
|
|
90
|
+
id: TAB_ID,
|
|
91
|
+
kind: TAB_KIND,
|
|
92
|
+
patterns: ['dsh-resource://git-changes/**'],
|
|
93
|
+
priority: 'extension',
|
|
94
|
+
// Resource opens use our addresses; the sidebar guide opens the page type
|
|
95
|
+
// by kind, whose composed address is `sidebar://<kind>` (format is the
|
|
96
|
+
// sidebar's own; accept any `sidebar://` page address since a named kind
|
|
97
|
+
// is the only way this type is asked to open one).
|
|
98
|
+
canOpen: (address) => typeof address === 'string'
|
|
99
|
+
&& (address.startsWith(ADDRESS_PREFIX) || address.startsWith('sidebar://')),
|
|
100
|
+
title: () => t('tab.title'),
|
|
101
|
+
// Advertise the page in the sidebar's guide ("+") so it is always reachable,
|
|
102
|
+
// even before the first turn with changes exists.
|
|
103
|
+
guide: [{
|
|
104
|
+
id: TAB_ID,
|
|
105
|
+
order: 20,
|
|
106
|
+
kind: TAB_KIND,
|
|
107
|
+
title: () => t('tab.title'),
|
|
108
|
+
description: () => t('tab.description'),
|
|
109
|
+
}],
|
|
110
|
+
})
|
|
111
|
+
return () => {
|
|
112
|
+
try { dispose() } catch (error) { /* ignore */ }
|
|
113
|
+
}
|
|
114
|
+
}, 'dsh-git-changes: tab type')
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
|
|
118
|
+
name: 'sidebar.right.pane.tab',
|
|
119
|
+
key: TAB_ID,
|
|
120
|
+
locale: NS,
|
|
121
|
+
inject: () => ({
|
|
122
|
+
services: { right: runtimeState.right, tabs: runtimeState.tabs },
|
|
123
|
+
}),
|
|
124
|
+
}, ReviewTabBody))
|
|
125
|
+
} catch (error) {
|
|
126
|
+
log('error', 'sidebar tab body registration failed', error)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Degraded container: a central Conversation view tab with the same page.
|
|
132
|
+
* @param {object} ctx client plugin context
|
|
133
|
+
* @param {Function} t locale-bound translator
|
|
134
|
+
* @param {any} right sidebarRight controller (may be undefined)
|
|
135
|
+
*/
|
|
136
|
+
function registerFallbackView(ctx, t, right) {
|
|
137
|
+
runtimeState.right = right
|
|
138
|
+
try {
|
|
139
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
140
|
+
name: 'conversation.view',
|
|
141
|
+
id: 'git-changes-view',
|
|
142
|
+
order: 50,
|
|
143
|
+
label: () => t('tab.title'),
|
|
144
|
+
locale: NS,
|
|
145
|
+
inject: () => ({ services: { right: runtimeState.right } }),
|
|
146
|
+
}, ReviewViewBody))
|
|
147
|
+
log('warn', 'right sidebar services unavailable; registered the fallback conversation view')
|
|
148
|
+
} catch (error) {
|
|
149
|
+
log('error', 'fallback conversation view registration failed', error)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Probe the optional right-sidebar services with a bounded retry, then mount the
|
|
155
|
+
* primary container (falling back to the central view after ~10s).
|
|
156
|
+
* @param {object} ctx client plugin context
|
|
157
|
+
* @param {Function} t locale-bound translator
|
|
158
|
+
*/
|
|
159
|
+
function mountContainer(ctx, t) {
|
|
160
|
+
let stopped = false
|
|
161
|
+
let timer = null
|
|
162
|
+
let attempts = 0
|
|
163
|
+
ctx.effect(() => () => {
|
|
164
|
+
stopped = true
|
|
165
|
+
if (timer) {
|
|
166
|
+
clearTimeout(timer)
|
|
167
|
+
timer = null
|
|
168
|
+
}
|
|
169
|
+
}, 'dsh-git-changes: container probe')
|
|
170
|
+
|
|
171
|
+
const tick = () => {
|
|
172
|
+
if (stopped) return
|
|
173
|
+
const tabs = safeGet(ctx, 'sidebarRightTabs')
|
|
174
|
+
const right = safeGet(ctx, 'sidebarRight')
|
|
175
|
+
const ready = tabs && typeof tabs.register === 'function' && right && typeof right.openResource === 'function'
|
|
176
|
+
if (ready) {
|
|
177
|
+
runtimeState.tabs = tabs
|
|
178
|
+
runtimeState.right = right
|
|
179
|
+
try {
|
|
180
|
+
registerSidebarTab(ctx, t, tabs)
|
|
181
|
+
log('log', 'review tab registered on the official right sidebar')
|
|
182
|
+
} catch (error) {
|
|
183
|
+
log('error', 'right sidebar tab registration failed', error)
|
|
184
|
+
registerFallbackView(ctx, t, right)
|
|
185
|
+
}
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (attempts >= 20) {
|
|
189
|
+
registerFallbackView(ctx, t, right)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
attempts += 1
|
|
193
|
+
timer = setTimeout(tick, 500)
|
|
194
|
+
}
|
|
195
|
+
tick()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Open the review tab for a session (used by the composer pill and commands).
|
|
200
|
+
* @param {string} sessionId session identity
|
|
201
|
+
* @returns {boolean} whether the resource open was issued
|
|
202
|
+
*/
|
|
203
|
+
function openReviewPage(sessionId) {
|
|
204
|
+
if (!sessionId) return false
|
|
205
|
+
runtimeState.sessionId = sessionId
|
|
206
|
+
return openResourceSafe(runtimeState.right, ADDRESS_PREFIX + encodeURIComponent(sessionId))
|
|
207
|
+
}
|