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,1404 @@
|
|
|
1
|
+
/* dsh-git-changes client half — Monaco AMD loader, inline diff renderer, editor host. */
|
|
2
|
+
|
|
3
|
+
/** Singleton Monaco promise; reset on failure so a later mount can retry. */
|
|
4
|
+
let monacoPromise = null
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Load the vendored Monaco AMD build from the authenticated vendor route.
|
|
8
|
+
* Adapted from the MIT `dsh-vscode-mode` loader (global `module`/`exports`
|
|
9
|
+
* pollution guard included: other plugins may define them, which makes the
|
|
10
|
+
* AMD loader believe it runs under Node).
|
|
11
|
+
* @returns {Promise<any>} the `monaco` namespace
|
|
12
|
+
*/
|
|
13
|
+
function loadMonaco() {
|
|
14
|
+
if (monacoPromise) return monacoPromise
|
|
15
|
+
monacoPromise = new Promise((resolve, reject) => {
|
|
16
|
+
const base = appUrl(VENDOR_BASE)
|
|
17
|
+
const root = typeof globalThis !== 'undefined' ? globalThis : {}
|
|
18
|
+
const hasModule = Object.prototype.hasOwnProperty.call(root, 'module')
|
|
19
|
+
const hasExports = Object.prototype.hasOwnProperty.call(root, 'exports')
|
|
20
|
+
const savedModule = root.module
|
|
21
|
+
const savedExports = root.exports
|
|
22
|
+
const hideNodeGlobals = () => {
|
|
23
|
+
try { delete root.module } catch (error) { /* read-only global */ }
|
|
24
|
+
try { delete root.exports } catch (error) { /* read-only global */ }
|
|
25
|
+
}
|
|
26
|
+
const restoreNodeGlobals = () => {
|
|
27
|
+
if (hasModule) root.module = savedModule
|
|
28
|
+
else { try { delete root.module } catch (error) { /* ignore */ } }
|
|
29
|
+
if (hasExports) root.exports = savedExports
|
|
30
|
+
else { try { delete root.exports } catch (error) { /* ignore */ } }
|
|
31
|
+
}
|
|
32
|
+
const loaderTagSelector = 'script[data-drp-monaco-loader]'
|
|
33
|
+
const removeLoaderTag = () => {
|
|
34
|
+
const existing = document.querySelector(loaderTagSelector)
|
|
35
|
+
if (existing && existing.parentNode) existing.parentNode.removeChild(existing)
|
|
36
|
+
}
|
|
37
|
+
const fail = (error) => {
|
|
38
|
+
monacoPromise = null
|
|
39
|
+
removeLoaderTag()
|
|
40
|
+
restoreNodeGlobals()
|
|
41
|
+
reject(error instanceof Error ? error : new Error(String(error)))
|
|
42
|
+
}
|
|
43
|
+
const boot = () => {
|
|
44
|
+
restoreNodeGlobals()
|
|
45
|
+
try {
|
|
46
|
+
if (typeof window.require !== 'function' || typeof window.require.config !== 'function') {
|
|
47
|
+
fail(new Error('Monaco AMD loader did not expose window.require'))
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
window.require.config({ paths: { vs: base } })
|
|
51
|
+
window.require(['vs/editor/editor.main'], () => {
|
|
52
|
+
const monaco = window.monaco
|
|
53
|
+
if (!monaco || !monaco.editor) {
|
|
54
|
+
fail(new Error('Monaco editor namespace missing'))
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
registerMonacoThemes(monaco)
|
|
58
|
+
resolve(monaco)
|
|
59
|
+
}, (error) => fail(new Error('Monaco module load failed: ' + String(error))))
|
|
60
|
+
} catch (error) {
|
|
61
|
+
fail(error)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const existing = document.querySelector(loaderTagSelector)
|
|
66
|
+
if (existing && typeof window.require === 'function' && typeof window.require.config === 'function') {
|
|
67
|
+
boot()
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (existing) removeLoaderTag()
|
|
71
|
+
const script = document.createElement('script')
|
|
72
|
+
script.src = base + '/loader.js'
|
|
73
|
+
script.dataset.didMonacoLoader = '1'
|
|
74
|
+
script.onload = boot
|
|
75
|
+
script.onerror = () => fail(new Error('Monaco loader script failed to load'))
|
|
76
|
+
hideNodeGlobals()
|
|
77
|
+
document.head.appendChild(script)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
fail(error)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
return monacoPromise
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compute git-changes regions from a comparison against the current file text.
|
|
87
|
+
*
|
|
88
|
+
* A region is one contiguous change group. `start`/`end` are 1-based new-file
|
|
89
|
+
* line numbers (`end` exclusive); `oldStart`/`oldLines` describe the deleted
|
|
90
|
+
* original lines shown as an inline block above `start`.
|
|
91
|
+
*
|
|
92
|
+
* @param {any} comparison WorkspaceFileDiff text comparison
|
|
93
|
+
* @param {string} text current file text
|
|
94
|
+
* @returns {{regions: Array<object>, total: number, added: number, deleted: number}}
|
|
95
|
+
*/
|
|
96
|
+
function buildRegions(comparison, text) {
|
|
97
|
+
const lines = splitLines(text)
|
|
98
|
+
const lineCount = lines.length
|
|
99
|
+
const empty = { regions: [], total: 0, added: 0, deleted: 0 }
|
|
100
|
+
if (!comparison || comparison.kind !== 'text' || !Array.isArray(comparison.hunks)) return empty
|
|
101
|
+
if (comparison.coarse) {
|
|
102
|
+
if (lineCount === 0) return empty
|
|
103
|
+
const region = { start: 1, end: Math.min(lineCount, MAX_RENDER_LINES) + 1, oldStart: 1, oldLines: [], addedLines: Math.min(lineCount, MAX_RENDER_LINES) }
|
|
104
|
+
return { regions: [region], total: 1, added: lineCount, deleted: 0 }
|
|
105
|
+
}
|
|
106
|
+
const regions = []
|
|
107
|
+
let added = 0
|
|
108
|
+
let deleted = 0
|
|
109
|
+
for (const hunk of comparison.hunks) {
|
|
110
|
+
if (!hunk || !Array.isArray(hunk.lines)) continue
|
|
111
|
+
let newLine = Math.max(1, Number(hunk.newStart) || 1)
|
|
112
|
+
let oldLine = Math.max(1, Number(hunk.oldStart) || 1)
|
|
113
|
+
let index = 0
|
|
114
|
+
while (index < hunk.lines.length) {
|
|
115
|
+
const raw = String(hunk.lines[index])
|
|
116
|
+
const prefix = raw.charAt(0)
|
|
117
|
+
if (prefix === ' ') {
|
|
118
|
+
newLine += 1
|
|
119
|
+
oldLine += 1
|
|
120
|
+
index += 1
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
const start = newLine
|
|
124
|
+
const oldStart = oldLine
|
|
125
|
+
const oldLines = []
|
|
126
|
+
let addedCount = 0
|
|
127
|
+
while (index < hunk.lines.length) {
|
|
128
|
+
const line = String(hunk.lines[index])
|
|
129
|
+
const kind = line.charAt(0)
|
|
130
|
+
if (kind === ' ') break
|
|
131
|
+
if (kind === '-') {
|
|
132
|
+
oldLines.push(line.slice(1))
|
|
133
|
+
oldLine += 1
|
|
134
|
+
deleted += 1
|
|
135
|
+
} else if (kind === '+') {
|
|
136
|
+
addedCount += 1
|
|
137
|
+
added += 1
|
|
138
|
+
}
|
|
139
|
+
index += 1
|
|
140
|
+
}
|
|
141
|
+
if (addedCount === 0 && oldLines.length === 0) continue
|
|
142
|
+
const clampedStart = Math.min(start, Math.max(1, lineCount + 1))
|
|
143
|
+
const clampedAdded = Math.min(addedCount, Math.max(0, lineCount - clampedStart + 1))
|
|
144
|
+
regions.push({
|
|
145
|
+
start: clampedStart,
|
|
146
|
+
end: clampedStart + clampedAdded,
|
|
147
|
+
oldStart,
|
|
148
|
+
oldLines,
|
|
149
|
+
addedLines: clampedAdded,
|
|
150
|
+
})
|
|
151
|
+
newLine += addedCount
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
regions.sort((a, b) => a.start - b.start || a.oldStart - b.oldStart)
|
|
155
|
+
return { regions, total: regions.length, added, deleted }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Per-editor inline diff renderer: whole-line decorations for added lines and
|
|
160
|
+
* Monaco view zones for deleted lines (with their original line numbers).
|
|
161
|
+
* Browser-only; owns its decorations/zones and releases them on dispose.
|
|
162
|
+
* @param {any} monaco Monaco namespace
|
|
163
|
+
* @param {any} editor Monaco editor
|
|
164
|
+
* @returns {{render:Function, refresh:Function, reflow:Function, dispose:Function}}
|
|
165
|
+
*/
|
|
166
|
+
function createDiffRenderer(monaco, editor) {
|
|
167
|
+
let zones = []
|
|
168
|
+
let listeners = []
|
|
169
|
+
let lastRegions = null
|
|
170
|
+
let generation = 0
|
|
171
|
+
let disposed = false
|
|
172
|
+
|
|
173
|
+
const colorVars = () => diffCssVarsOf(themeRuntime.scheme || detectScheme())
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Apply the diff color variables to the editor DOM.
|
|
177
|
+
* @param {HTMLElement} node target node
|
|
178
|
+
*/
|
|
179
|
+
const setColors = (node) => {
|
|
180
|
+
if (!node || !node.style) return
|
|
181
|
+
const vars = colorVars()
|
|
182
|
+
node.style.setProperty('--drp-diff-add', vars.add)
|
|
183
|
+
node.style.setProperty('--drp-diff-del', vars.del)
|
|
184
|
+
node.style.setProperty('--drp-diff-fg', vars.fg)
|
|
185
|
+
node.style.setProperty('--drp-diff-number', vars.number)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Remove every decoration and view zone this renderer owns. */
|
|
189
|
+
const clear = () => {
|
|
190
|
+
generation += 1
|
|
191
|
+
lastRegions = null
|
|
192
|
+
try {
|
|
193
|
+
if (decorations.length > 0) decorations = editor.deltaDecorations(decorations, [])
|
|
194
|
+
} catch (error) {
|
|
195
|
+
decorations = []
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
if (zones.length > 0) {
|
|
199
|
+
editor.changeViewZones((accessor) => {
|
|
200
|
+
for (const zone of zones) {
|
|
201
|
+
try { accessor.removeZone(zone.id) } catch (error) { /* already gone */ }
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
} catch (error) { /* editor may be disposed */ }
|
|
206
|
+
zones = []
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Map a pointer position onto a caret inside a deleted-line zone. Coordinates
|
|
211
|
+
* outside the zone are clamped to its first/last row so a drag that starts in
|
|
212
|
+
* the zone can never create a page-wide native selection.
|
|
213
|
+
* @param {object} zone zone record
|
|
214
|
+
* @param {number} x client X
|
|
215
|
+
* @param {number} y client Y
|
|
216
|
+
* @returns {{node:Node, offset:number} | null}
|
|
217
|
+
*/
|
|
218
|
+
const zoneCaretPoint = (zone, x, y) => {
|
|
219
|
+
const rows = zone.domNode.querySelectorAll('.drp-del-row')
|
|
220
|
+
if (!rows.length) return null
|
|
221
|
+
const first = rows[0]
|
|
222
|
+
const last = rows[rows.length - 1]
|
|
223
|
+
const firstRect = first.getBoundingClientRect()
|
|
224
|
+
const lastRect = last.getBoundingClientRect()
|
|
225
|
+
const firstText = zone.rows[0] && zone.rows[0].firstChild
|
|
226
|
+
const lastText = zone.rows[zone.rows.length - 1] && zone.rows[zone.rows.length - 1].firstChild
|
|
227
|
+
if (y <= firstRect.top) return firstText ? { node: firstText, offset: 0 } : null
|
|
228
|
+
if (y >= lastRect.bottom) return lastText ? { node: lastText, offset: lastText.textContent.length } : null
|
|
229
|
+
let caret = null
|
|
230
|
+
try { caret = document.caretRangeFromPoint(x, y) } catch (error) { caret = null }
|
|
231
|
+
if (caret && zone.domNode.contains(caret.startContainer)) {
|
|
232
|
+
return { node: caret.startContainer, offset: caret.startOffset }
|
|
233
|
+
}
|
|
234
|
+
const rowCount = Math.max(1, rows.length)
|
|
235
|
+
const step = rowCount > 1 ? (lastRect.top - firstRect.top) / (rowCount - 1) : 20
|
|
236
|
+
const index = Math.max(0, Math.min(rowCount - 1, Math.floor((y - firstRect.top) / Math.max(1, step))))
|
|
237
|
+
const text = zone.rows[index] && zone.rows[index].firstChild
|
|
238
|
+
if (!text) return null
|
|
239
|
+
const right = rows[index].getBoundingClientRect().right
|
|
240
|
+
return { node: text, offset: x >= right ? text.textContent.length : 0 }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Zone DOM node → zone record (mouse selection needs the text rows). */
|
|
244
|
+
const zoneByNode = new WeakMap()
|
|
245
|
+
let zoneMouseInstalled = false
|
|
246
|
+
|
|
247
|
+
/** Apply one restricted Range selection inside a deleted-line zone. */
|
|
248
|
+
const applyZoneRange = (anchor, focus) => {
|
|
249
|
+
try {
|
|
250
|
+
const anchorFirst = anchor.node === focus.node
|
|
251
|
+
? anchor.offset <= focus.offset
|
|
252
|
+
: Boolean(anchor.node.compareDocumentPosition(focus.node) & 4)
|
|
253
|
+
const start = anchorFirst ? anchor : focus
|
|
254
|
+
const end = anchorFirst ? focus : anchor
|
|
255
|
+
const range = document.createRange()
|
|
256
|
+
range.setStart(start.node, start.offset)
|
|
257
|
+
range.setEnd(end.node, end.offset)
|
|
258
|
+
const selection = window.getSelection()
|
|
259
|
+
if (!selection) return
|
|
260
|
+
selection.removeAllRanges()
|
|
261
|
+
selection.addRange(range)
|
|
262
|
+
} catch (error) { /* ignore */ }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Install one document-level capture handler for deleted-line zones. Capture is
|
|
267
|
+
* required: Monaco's own mousedown handling would otherwise clear the native
|
|
268
|
+
* Range we create for the zone (the editor normally keeps native selection off).
|
|
269
|
+
*/
|
|
270
|
+
const installZoneMouse = () => {
|
|
271
|
+
if (zoneMouseInstalled) return
|
|
272
|
+
zoneMouseInstalled = true
|
|
273
|
+
document.addEventListener('mousedown', (event) => {
|
|
274
|
+
if (event.button !== 0) return
|
|
275
|
+
const target = event.target
|
|
276
|
+
if (!(target instanceof Element) || !target.closest) return
|
|
277
|
+
const zoneEl = target.closest('.drp-del-zone')
|
|
278
|
+
if (!zoneEl) return
|
|
279
|
+
if (target.closest('.drp-del-copy')) return
|
|
280
|
+
const zone = zoneByNode.get(zoneEl)
|
|
281
|
+
if (!zone) return
|
|
282
|
+
const anchor = zoneCaretPoint(zone, event.clientX, event.clientY)
|
|
283
|
+
if (!anchor) return
|
|
284
|
+
try {
|
|
285
|
+
event.preventDefault()
|
|
286
|
+
event.stopPropagation()
|
|
287
|
+
} catch (error) { /* ignore */ }
|
|
288
|
+
applyZoneRange(anchor, anchor)
|
|
289
|
+
const onMove = (moveEvent) => {
|
|
290
|
+
if (moveEvent.buttons === 0) return
|
|
291
|
+
const focus = zoneCaretPoint(zone, moveEvent.clientX, moveEvent.clientY)
|
|
292
|
+
if (focus) applyZoneRange(anchor, focus)
|
|
293
|
+
try { moveEvent.preventDefault(); moveEvent.stopPropagation() } catch (error) { /* ignore */ }
|
|
294
|
+
}
|
|
295
|
+
const onUp = (upEvent) => {
|
|
296
|
+
document.removeEventListener('mousemove', onMove, true)
|
|
297
|
+
document.removeEventListener('mouseup', onUp, true)
|
|
298
|
+
try { upEvent.stopPropagation() } catch (error) { /* ignore */ }
|
|
299
|
+
}
|
|
300
|
+
document.addEventListener('mousemove', onMove, true)
|
|
301
|
+
document.addEventListener('mouseup', onUp, true)
|
|
302
|
+
}, true)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Build one deleted-line view zone (body + gutter twin).
|
|
307
|
+
* @param {object} region change group
|
|
308
|
+
* @returns {object} zone record
|
|
309
|
+
*/
|
|
310
|
+
const makeZone = (region) => {
|
|
311
|
+
const domNode = document.createElement('div')
|
|
312
|
+
domNode.className = 'drp-del-zone'
|
|
313
|
+
const marginDomNode = document.createElement('div')
|
|
314
|
+
marginDomNode.className = 'drp-del-margin'
|
|
315
|
+
const rows = []
|
|
316
|
+
for (let index = 0; index < region.oldLines.length; index += 1) {
|
|
317
|
+
const row = document.createElement('div')
|
|
318
|
+
row.className = 'drp-del-row'
|
|
319
|
+
const code = document.createElement('span')
|
|
320
|
+
code.className = 'drp-del-text'
|
|
321
|
+
code.textContent = region.oldLines[index]
|
|
322
|
+
row.appendChild(code)
|
|
323
|
+
domNode.appendChild(row)
|
|
324
|
+
rows.push(code)
|
|
325
|
+
const marginRow = document.createElement('div')
|
|
326
|
+
marginRow.className = 'drp-del-margin-row'
|
|
327
|
+
const number = document.createElement('span')
|
|
328
|
+
number.className = 'drp-del-number'
|
|
329
|
+
number.textContent = String((region.oldStart || 1) + index)
|
|
330
|
+
const marker = document.createElement('span')
|
|
331
|
+
marker.className = 'drp-del-marker'
|
|
332
|
+
marker.textContent = '−'
|
|
333
|
+
marginRow.appendChild(number)
|
|
334
|
+
marginRow.appendChild(marker)
|
|
335
|
+
marginDomNode.appendChild(marginRow)
|
|
336
|
+
}
|
|
337
|
+
const zone = {
|
|
338
|
+
domNode,
|
|
339
|
+
marginDomNode,
|
|
340
|
+
rows,
|
|
341
|
+
lines: region.oldLines,
|
|
342
|
+
after: Math.max(0, (Number(region.start) || 1) - 1),
|
|
343
|
+
}
|
|
344
|
+
// One-click copy for the whole deleted block, placed in the gutter so it
|
|
345
|
+
// never covers the deleted code text itself.
|
|
346
|
+
const copyBtn = document.createElement('button')
|
|
347
|
+
copyBtn.type = 'button'
|
|
348
|
+
copyBtn.className = 'drp-del-copy'
|
|
349
|
+
copyBtn.title = '复制删除的行'
|
|
350
|
+
copyBtn.textContent = '\u29C9'
|
|
351
|
+
copyBtn.addEventListener('mousedown', (event) => {
|
|
352
|
+
try { event.preventDefault(); event.stopPropagation() } catch (error) { /* ignore */ }
|
|
353
|
+
})
|
|
354
|
+
copyBtn.addEventListener('click', (event) => {
|
|
355
|
+
try { event.preventDefault(); event.stopPropagation() } catch (error) { /* ignore */ }
|
|
356
|
+
const text = zone.lines.join('\n')
|
|
357
|
+
Promise.resolve(typeof copyText === 'function' ? copyText(text) : false)
|
|
358
|
+
.then((ok) => {
|
|
359
|
+
copyBtn.textContent = ok ? '\u2713' : '!'
|
|
360
|
+
setTimeout(() => { copyBtn.textContent = '\u29C9' }, 1300)
|
|
361
|
+
})
|
|
362
|
+
.catch(() => { copyBtn.textContent = '!' })
|
|
363
|
+
})
|
|
364
|
+
marginDomNode.appendChild(copyBtn)
|
|
365
|
+
zoneByNode.set(domNode, zone)
|
|
366
|
+
installZoneMouse()
|
|
367
|
+
return zone
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Align a zone with the editor's font, gutter geometry and theme.
|
|
372
|
+
* @param {object} zone zone record
|
|
373
|
+
* @returns {number} minimum width in px for horizontal scrolling
|
|
374
|
+
*/
|
|
375
|
+
const layoutZone = (zone) => {
|
|
376
|
+
const vars = colorVars()
|
|
377
|
+
try { editor.applyFontInfo?.(zone.domNode) } catch (error) { /* optional */ }
|
|
378
|
+
try { editor.applyFontInfo?.(zone.marginDomNode) } catch (error) { /* optional */ }
|
|
379
|
+
setColors(zone.domNode)
|
|
380
|
+
setColors(zone.marginDomNode)
|
|
381
|
+
let lineHeight = 20
|
|
382
|
+
let layout = { lineNumbersLeft: 0, lineNumbersWidth: 0, decorationsLeft: 0, decorationsWidth: 0 }
|
|
383
|
+
let fontInfo = null
|
|
384
|
+
try {
|
|
385
|
+
lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight) || 20
|
|
386
|
+
layout = editor.getLayoutInfo() || layout
|
|
387
|
+
fontInfo = editor.getOption(monaco.editor.EditorOption.fontInfo)
|
|
388
|
+
} catch (error) { /* defaults */ }
|
|
389
|
+
const tabSize = (zone.domNode && zone.domNode.style && Number(zone.domNode.style.tabSize)) || 2
|
|
390
|
+
for (const node of [zone.domNode, zone.marginDomNode]) {
|
|
391
|
+
node.style.setProperty('--drp-line-height', lineHeight + 'px')
|
|
392
|
+
}
|
|
393
|
+
zone.domNode.style.tabSize = String(tabSize || 2)
|
|
394
|
+
zone.marginDomNode.style.setProperty('--drp-number-left', layout.lineNumbersLeft + 'px')
|
|
395
|
+
zone.marginDomNode.style.setProperty('--drp-number-width', layout.lineNumbersWidth + 'px')
|
|
396
|
+
zone.marginDomNode.style.setProperty('--drp-marker-left', layout.decorationsLeft + 'px')
|
|
397
|
+
zone.marginDomNode.style.setProperty('--drp-marker-width', layout.decorationsWidth + 'px')
|
|
398
|
+
zone.marginDomNode.style.setProperty('--drp-diff-number', vars.number)
|
|
399
|
+
let width = 0
|
|
400
|
+
for (const line of zone.lines) width = Math.max(width, String(line).replace(/\t/g, ' '.repeat(4)).length)
|
|
401
|
+
const charWidth = Math.max(
|
|
402
|
+
(fontInfo && fontInfo.typicalHalfwidthCharacterWidth) || 8,
|
|
403
|
+
(fontInfo && fontInfo.typicalFullwidthCharacterWidth) || 0,
|
|
404
|
+
)
|
|
405
|
+
return width * charWidth + 24
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Asynchronously syntax-color deleted lines with Monaco's own tokenizer.
|
|
410
|
+
* @param {object} zone zone record
|
|
411
|
+
* @param {number} epoch render generation
|
|
412
|
+
*/
|
|
413
|
+
const colorizeZone = async (zone, epoch) => {
|
|
414
|
+
try {
|
|
415
|
+
if (typeof monaco.editor.colorize !== 'function') return
|
|
416
|
+
const model = editor.getModel()
|
|
417
|
+
if (!model) return
|
|
418
|
+
const language = model.getLanguageId()
|
|
419
|
+
const tabSize = (model.getOptions && model.getOptions().tabSize) || 2
|
|
420
|
+
const html = await monaco.editor.colorize(zone.lines.join('\n'), language, { tabSize })
|
|
421
|
+
if (disposed || epoch !== generation || editor.getModel() !== model) return
|
|
422
|
+
const rows = String(html).split(/<br\s*\/?\s*>/i)
|
|
423
|
+
if (rows.length === zone.rows.length + 1 && rows[rows.length - 1] === '') rows.pop()
|
|
424
|
+
if (rows.length !== zone.rows.length) return
|
|
425
|
+
for (let index = 0; index < rows.length; index += 1) zone.rows[index].innerHTML = rows[index]
|
|
426
|
+
} catch (error) {
|
|
427
|
+
/* keep the plain-text fallback visible */
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Paint regions onto the current model.
|
|
433
|
+
* @param {Array<object>} regions change groups (see buildRegions)
|
|
434
|
+
*/
|
|
435
|
+
const paint = (regions) => {
|
|
436
|
+
lastRegions = regions
|
|
437
|
+
clear()
|
|
438
|
+
lastRegions = regions
|
|
439
|
+
if (!editor || disposed) return
|
|
440
|
+
const model = editor.getModel()
|
|
441
|
+
if (!model) return
|
|
442
|
+
setColors(editor.getDomNode())
|
|
443
|
+
const decorationsSpec = []
|
|
444
|
+
const zoneSpecs = []
|
|
445
|
+
for (const region of regions) {
|
|
446
|
+
if (region.addedLines > 0 && region.start >= 1) {
|
|
447
|
+
decorationsSpec.push({
|
|
448
|
+
range: new monaco.Range(region.start, 1, Math.max(region.start, region.end - 1), 1),
|
|
449
|
+
options: {
|
|
450
|
+
isWholeLine: true,
|
|
451
|
+
className: 'drp-add-line',
|
|
452
|
+
marginClassName: 'drp-add-line',
|
|
453
|
+
linesDecorationsClassName: 'drp-add-gutter',
|
|
454
|
+
},
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
if (region.oldLines.length > 0) zoneSpecs.push(makeZone(region))
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
decorations = editor.deltaDecorations([], decorationsSpec)
|
|
461
|
+
} catch (error) {
|
|
462
|
+
decorations = []
|
|
463
|
+
}
|
|
464
|
+
if (zoneSpecs.length > 0) {
|
|
465
|
+
try {
|
|
466
|
+
editor.changeViewZones((accessor) => {
|
|
467
|
+
for (const zone of zoneSpecs) {
|
|
468
|
+
const minWidthInPx = layoutZone(zone)
|
|
469
|
+
zone.id = accessor.addZone({
|
|
470
|
+
afterLineNumber: zone.after,
|
|
471
|
+
heightInLines: Math.max(1, zone.lines.length),
|
|
472
|
+
domNode: zone.domNode,
|
|
473
|
+
marginDomNode: zone.marginDomNode,
|
|
474
|
+
minWidthInPx,
|
|
475
|
+
// Deleted lines are plain DOM (they are not part of the Monaco
|
|
476
|
+
// text model). Suppressing the mouse would make the whole block
|
|
477
|
+
// unselectable and uncopyable, so let the browser handle drag
|
|
478
|
+
// selection inside the zone like normal text.
|
|
479
|
+
suppressMouseDown: false,
|
|
480
|
+
})
|
|
481
|
+
zones.push(zone)
|
|
482
|
+
void colorizeZone(zone, generation)
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
} catch (error) {
|
|
486
|
+
zones = []
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Re-layout zones after a theme or geometry change.
|
|
493
|
+
* @returns {void}
|
|
494
|
+
*/
|
|
495
|
+
const reflow = () => {
|
|
496
|
+
if (disposed) return
|
|
497
|
+
setColors(editor.getDomNode())
|
|
498
|
+
for (const zone of zones) {
|
|
499
|
+
try { layoutZone(zone) } catch (error) { /* ignore */ }
|
|
500
|
+
}
|
|
501
|
+
for (const zone of zones) void colorizeZone(zone, generation)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Layout changes fire on every splitter frame. Only the geometry of the
|
|
505
|
+
// existing zones needs to follow (gutter offsets, line height); re-tokenizing
|
|
506
|
+
// the deleted lines on every frame is what made the DSH divider drag stutter.
|
|
507
|
+
let layoutFrame = 0
|
|
508
|
+
const onLayout = () => {
|
|
509
|
+
if (disposed || layoutFrame) return
|
|
510
|
+
layoutFrame = requestAnimationFrame(() => {
|
|
511
|
+
layoutFrame = 0
|
|
512
|
+
if (disposed) return
|
|
513
|
+
try {
|
|
514
|
+
setColors(editor.getDomNode())
|
|
515
|
+
for (const zone of zones) layoutZone(zone)
|
|
516
|
+
} catch (error) { /* ignore */ }
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
try { listeners.push(editor.onDidLayoutChange(onLayout)) } catch (error) { /* ignore */ }
|
|
520
|
+
try { listeners.push(editor.onDidChangeModel(() => clear())) } catch (error) { /* ignore */ }
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
render(regions) {
|
|
524
|
+
paint(Array.isArray(regions) ? regions : [])
|
|
525
|
+
},
|
|
526
|
+
refresh() {
|
|
527
|
+
paint(lastRegions || [])
|
|
528
|
+
},
|
|
529
|
+
reflow,
|
|
530
|
+
dispose() {
|
|
531
|
+
disposed = true
|
|
532
|
+
if (layoutFrame) {
|
|
533
|
+
try { cancelAnimationFrame(layoutFrame) } catch (error) { /* ignore */ }
|
|
534
|
+
layoutFrame = 0
|
|
535
|
+
}
|
|
536
|
+
clear()
|
|
537
|
+
for (const listener of listeners) {
|
|
538
|
+
try { listener.dispose() } catch (error) { /* ignore */ }
|
|
539
|
+
}
|
|
540
|
+
listeners = []
|
|
541
|
+
},
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Read-only Monaco editor host: one editor instance, one model per reviewed file,
|
|
547
|
+
* per-file view state, inline diff decorations refreshed from `regions`.
|
|
548
|
+
* @param {object} props component props
|
|
549
|
+
* @returns {any} React element
|
|
550
|
+
*/
|
|
551
|
+
/** Minimum editor width that still reads well side by side (VS Code default). */
|
|
552
|
+
const DIFF_SIDE_MIN_WIDTH = 900
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Resolve the effective diff layout for the current viewport.
|
|
556
|
+
* @param {string} preference stored preference ('auto' | 'inline' | 'side')
|
|
557
|
+
* @param {number} width available editor width in CSS pixels
|
|
558
|
+
* @returns {'inline' | 'side'}
|
|
559
|
+
*/
|
|
560
|
+
function resolveDiffLayout(preference, width) {
|
|
561
|
+
if (preference === 'side') return 'side'
|
|
562
|
+
if (preference === 'inline') return 'inline'
|
|
563
|
+
return Number(width) >= DIFF_SIDE_MIN_WIDTH ? 'side' : 'inline'
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Rebuild the "before" side of a diff from the current text and the built regions.
|
|
568
|
+
*
|
|
569
|
+
* `buildRegions` already carries each changed block's old lines, so the original
|
|
570
|
+
* document can be reconstructed without another Host round trip.
|
|
571
|
+
* @param {string} text current ("after") document text
|
|
572
|
+
* @param {Array<object>} regions built diff regions
|
|
573
|
+
* @returns {string} original ("before") document text
|
|
574
|
+
*/
|
|
575
|
+
function originalTextFromRegions(text, regions) {
|
|
576
|
+
const current = text === '' ? [] : String(text || '').split('\n')
|
|
577
|
+
const list = Array.isArray(regions) ? regions.slice() : []
|
|
578
|
+
list.sort((a, b) => (Number(a.start) || 0) - (Number(b.start) || 0) || (Number(a.oldStart) || 0) - (Number(b.oldStart) || 0))
|
|
579
|
+
const out = []
|
|
580
|
+
let cursor = 0
|
|
581
|
+
for (const region of list) {
|
|
582
|
+
const start = Math.max(1, Number(region.start) || 1) - 1
|
|
583
|
+
const added = Math.max(0, Number(region.addedLines) || 0)
|
|
584
|
+
const target = Math.min(current.length, start)
|
|
585
|
+
for (let index = cursor; index < target; index += 1) out.push(current[index])
|
|
586
|
+
for (const line of (Array.isArray(region.oldLines) ? region.oldLines : [])) out.push(String(line))
|
|
587
|
+
cursor = Math.min(current.length, target + added)
|
|
588
|
+
}
|
|
589
|
+
for (let index = cursor; index < current.length; index += 1) out.push(current[index])
|
|
590
|
+
return out.join('\n')
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Read-only side-by-side diff host backed by `monaco.editor.createDiffEditor`.
|
|
595
|
+
*
|
|
596
|
+
* Kept separate from `MonacoHost` on purpose: the inline renderer owns the
|
|
597
|
+
* single-editor path (decorations, LSP announcements, view-state caching), while
|
|
598
|
+
* this host only exists to show Cursor-style two-column diffs. Switching layouts
|
|
599
|
+
* therefore disposes one host and creates the other, which matches the design
|
|
600
|
+
* decision to rebuild the editor instead of mutating it in place.
|
|
601
|
+
* @param {object} props component props
|
|
602
|
+
* @returns {any} React element
|
|
603
|
+
*/
|
|
604
|
+
function MonacoDiffHost(props) {
|
|
605
|
+
const { monaco, file, regions, sessionId, scheme, fontSize, reveal, blame, documentEpoch, onCursor, onReady, onError, onModelReady, t } = props
|
|
606
|
+
const hostRef = React.useRef(null)
|
|
607
|
+
const editorRef = React.useRef(null)
|
|
608
|
+
const modelsRef = React.useRef([])
|
|
609
|
+
const blameDwellRef = React.useRef(null)
|
|
610
|
+
const blameRef = React.useRef(blame)
|
|
611
|
+
blameRef.current = blame
|
|
612
|
+
const fileRef = React.useRef(file)
|
|
613
|
+
fileRef.current = file
|
|
614
|
+
const tRef = React.useRef(t)
|
|
615
|
+
tRef.current = t
|
|
616
|
+
const callbacksRef = React.useRef({ onCursor, onReady, onError, onModelReady })
|
|
617
|
+
callbacksRef.current = { onCursor, onReady, onError, onModelReady }
|
|
618
|
+
// The custom themes carry the git diff colours (inserted/removed line and
|
|
619
|
+
// word backgrounds); the built-in vs/vs-dark themes do not, which made the
|
|
620
|
+
// side-by-side view look like plain code with no highlights.
|
|
621
|
+
const themeOf = () => themeRuntime.scheme === 'light' ? MONACO_THEME.light : MONACO_THEME.dark
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Paint the diff tints through CSS variables on the host node. The custom
|
|
625
|
+
* Monaco theme carries the same colors, but pinning them here guarantees the
|
|
626
|
+
* green/red backgrounds survive even when the theme falls back to a base
|
|
627
|
+
* palette (or an older Monaco build ignores the diff color keys).
|
|
628
|
+
* @param {HTMLElement | null} node host node
|
|
629
|
+
*/
|
|
630
|
+
const setDiffColors = (node) => {
|
|
631
|
+
if (!node || !node.style) return
|
|
632
|
+
const vars = diffCssVarsOf(themeRuntime.scheme === 'light' ? 'light' : 'dark')
|
|
633
|
+
node.style.setProperty('--drp-diff-add', vars.add)
|
|
634
|
+
node.style.setProperty('--drp-diff-del', vars.del)
|
|
635
|
+
node.style.setProperty('--drp-diff-add-word', vars.addWord)
|
|
636
|
+
node.style.setProperty('--drp-diff-del-word', vars.delWord)
|
|
637
|
+
node.style.setProperty('--vscode-diffEditor-insertedLineBackground', vars.add)
|
|
638
|
+
node.style.setProperty('--vscode-diffEditor-removedLineBackground', vars.del)
|
|
639
|
+
node.style.setProperty('--vscode-diffEditor-insertedTextBackground', vars.addWord)
|
|
640
|
+
node.style.setProperty('--vscode-diffEditor-removedTextBackground', vars.delWord)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Create the diff editor once the host node and Monaco both exist. */
|
|
644
|
+
const attach = React.useCallback((node) => {
|
|
645
|
+
hostRef.current = node
|
|
646
|
+
if (!node || editorRef.current || !monaco) return
|
|
647
|
+
try {
|
|
648
|
+
// Register/apply the plugin themes before the diff editor is built so the
|
|
649
|
+
// inserted/removed backgrounds are present from the first paint.
|
|
650
|
+
try { applyMonacoTheme(monaco) } catch (error) { /* keep whatever theme is active */ }
|
|
651
|
+
setDiffColors(node)
|
|
652
|
+
const editor = monaco.editor.createDiffEditor(node, {
|
|
653
|
+
theme: themeOf(),
|
|
654
|
+
readOnly: true,
|
|
655
|
+
originalEditable: false,
|
|
656
|
+
renderSideBySide: true,
|
|
657
|
+
enableSplitViewResizing: true,
|
|
658
|
+
renderIndicators: true,
|
|
659
|
+
ignoreTrimWhitespace: false,
|
|
660
|
+
diffAlgorithm: 'advanced',
|
|
661
|
+
renderOverviewRuler: false,
|
|
662
|
+
automaticLayout: true,
|
|
663
|
+
scrollBeyondLastLine: false,
|
|
664
|
+
minimap: { enabled: false },
|
|
665
|
+
glyphMargin: false,
|
|
666
|
+
lineNumbers: 'on',
|
|
667
|
+
folding: true,
|
|
668
|
+
matchBrackets: 'always',
|
|
669
|
+
renderLineHighlight: 'none',
|
|
670
|
+
smoothScrolling: false,
|
|
671
|
+
mouseWheelZoom: true,
|
|
672
|
+
fixedOverflowWidgets: true,
|
|
673
|
+
fontSize: Math.min(28, Math.max(9, Math.round(Number(fontSize) || 13))),
|
|
674
|
+
lineHeight: 20,
|
|
675
|
+
fontFamily: 'var(--ds-font-family-code, var(--dsh-code-font-family, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace))',
|
|
676
|
+
tabSize: 2,
|
|
677
|
+
padding: { top: 8, bottom: 24 },
|
|
678
|
+
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10, useShadows: false },
|
|
679
|
+
})
|
|
680
|
+
editorRef.current = editor
|
|
681
|
+
const modified = editor.getModifiedEditor()
|
|
682
|
+
try {
|
|
683
|
+
modified.onDidChangeCursorPosition((event) => {
|
|
684
|
+
callbacksRef.current.onCursor?.({ line: event.position.lineNumber, column: event.position.column })
|
|
685
|
+
})
|
|
686
|
+
} catch (error) { /* optional */ }
|
|
687
|
+
const dwells = []
|
|
688
|
+
const blameOptions = () => ({ getBlame: () => blameRef.current, sessionId, getPath: () => (fileRef.current && fileRef.current.path) || '', t: tRef.current })
|
|
689
|
+
try {
|
|
690
|
+
const original = typeof editor.getOriginalEditor === 'function' ? editor.getOriginalEditor() : null
|
|
691
|
+
if (original) dwells.push(attachBlameDwell(original, monaco, blameOptions(), (typeof original.getDomNode === 'function' ? original.getDomNode() : null) || hostRef.current))
|
|
692
|
+
} catch (error) { /* optional */ }
|
|
693
|
+
try { dwells.push(attachBlameDwell(modified, monaco, blameOptions(), hostRef.current)) } catch (error) { /* optional */ }
|
|
694
|
+
blameDwellRef.current = { dispose: () => { for (const dwell of dwells) { try { dwell.dispose?.() } catch (error) { /* ignore */ } } } }
|
|
695
|
+
callbacksRef.current.onReady?.({
|
|
696
|
+
revealLine: (line) => {
|
|
697
|
+
try {
|
|
698
|
+
const target = Math.max(1, Number(line) || 1)
|
|
699
|
+
modified.revealLineInCenter?.(target)
|
|
700
|
+
modified.setPosition({ lineNumber: target, column: 1 })
|
|
701
|
+
} catch (error) { /* ignore */ }
|
|
702
|
+
},
|
|
703
|
+
focus: () => { try { modified.focus() } catch (error) { /* ignore */ } },
|
|
704
|
+
layout: () => { try { editor.layout() } catch (error) { /* ignore */ } },
|
|
705
|
+
getEditor: () => editor,
|
|
706
|
+
setFontSize: (size) => {
|
|
707
|
+
try { editor.updateOptions({ fontSize: Math.min(28, Math.max(9, Math.round(Number(size) || 13))) }) } catch (error) { /* ignore */ }
|
|
708
|
+
},
|
|
709
|
+
})
|
|
710
|
+
} catch (error) {
|
|
711
|
+
log('error', 'monaco diff editor creation failed', error)
|
|
712
|
+
callbacksRef.current.onError?.(error)
|
|
713
|
+
}
|
|
714
|
+
}, [monaco])
|
|
715
|
+
|
|
716
|
+
// Disposal.
|
|
717
|
+
React.useEffect(() => () => {
|
|
718
|
+
try { blameDwellRef.current?.dispose?.() } catch (error) { /* ignore */ }
|
|
719
|
+
blameDwellRef.current = null
|
|
720
|
+
const editor = editorRef.current
|
|
721
|
+
editorRef.current = null
|
|
722
|
+
if (editor) { try { editor.dispose() } catch (error) { /* ignore */ } }
|
|
723
|
+
for (const model of modelsRef.current) { try { model.dispose() } catch (error) { /* ignore */ } }
|
|
724
|
+
modelsRef.current = []
|
|
725
|
+
}, [])
|
|
726
|
+
|
|
727
|
+
// (Re)build both models whenever the file, its text or the regions change.
|
|
728
|
+
React.useEffect(() => {
|
|
729
|
+
const editor = editorRef.current
|
|
730
|
+
if (!editor || !monaco || !file || file.kind !== 'text' || typeof file.text !== 'string') return
|
|
731
|
+
try {
|
|
732
|
+
for (const model of modelsRef.current) { try { model.dispose() } catch (error) { /* ignore */ } }
|
|
733
|
+
modelsRef.current = []
|
|
734
|
+
const path = file.path || ''
|
|
735
|
+
const base = 'did:///' + encodeURIComponent(sessionId || 'workspace') + '/' + path.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/')
|
|
736
|
+
const language = file.language || 'plaintext'
|
|
737
|
+
const original = monaco.editor.createModel(originalTextFromRegions(file.text, regions), language, monaco.Uri.parse(base + '?original'))
|
|
738
|
+
const modified = monaco.editor.createModel(file.text, language, monaco.Uri.parse(base + '?modified'))
|
|
739
|
+
modelsRef.current = [original, modified]
|
|
740
|
+
editor.setModel({ original, modified })
|
|
741
|
+
try { editor.layout() } catch (error) { /* ignore */ }
|
|
742
|
+
callbacksRef.current.onModelReady?.({ path, text: file.text, language, monaco, model: modified })
|
|
743
|
+
} catch (error) {
|
|
744
|
+
log('error', 'monaco diff model switch failed', error)
|
|
745
|
+
}
|
|
746
|
+
}, [monaco, sessionId, file && file.path, file && file.text, file && file.language, file && file.kind, regions])
|
|
747
|
+
|
|
748
|
+
// Theme + font size are prop-driven like the inline host.
|
|
749
|
+
React.useEffect(() => {
|
|
750
|
+
const editor = editorRef.current
|
|
751
|
+
if (!editor || !monaco) return
|
|
752
|
+
try {
|
|
753
|
+
applyMonacoTheme(monaco)
|
|
754
|
+
editor.updateOptions({ theme: themeOf() })
|
|
755
|
+
setDiffColors(hostRef.current)
|
|
756
|
+
} catch (error) { /* ignore */ }
|
|
757
|
+
}, [monaco, scheme])
|
|
758
|
+
|
|
759
|
+
React.useEffect(() => {
|
|
760
|
+
const editor = editorRef.current
|
|
761
|
+
if (!editor) return
|
|
762
|
+
try { editor.updateOptions({ fontSize: Math.min(28, Math.max(9, Math.round(Number(fontSize) || 13))) }) } catch (error) { /* ignore */ }
|
|
763
|
+
}, [fontSize, file && file.path])
|
|
764
|
+
|
|
765
|
+
// Reveal requests: both panes follow the modified line number.
|
|
766
|
+
React.useEffect(() => {
|
|
767
|
+
const editor = editorRef.current
|
|
768
|
+
if (!editor || !reveal || !reveal.line) return
|
|
769
|
+
try {
|
|
770
|
+
const target = Math.max(1, Number(reveal.line) || 1)
|
|
771
|
+
const modified = editor.getModifiedEditor()
|
|
772
|
+
modified.revealLineInCenter(target)
|
|
773
|
+
modified.setPosition({ lineNumber: target, column: 1 })
|
|
774
|
+
modified.focus()
|
|
775
|
+
} catch (error) { /* ignore */ }
|
|
776
|
+
}, [reveal && reveal.line, reveal && reveal.nonce])
|
|
777
|
+
|
|
778
|
+
return h('div', { className: 'drp-monaco-host drp-monaco-diff', ref: attach })
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** How long the cursor must rest on a line before its inline blame appears. */
|
|
782
|
+
const BLAME_DWELL_MS = 900
|
|
783
|
+
/** Grace period before the commit popover opens on the annotation. */
|
|
784
|
+
const BLAME_POPOVER_DELAY_MS = 120
|
|
785
|
+
|
|
786
|
+
/** Human-readable age of a commit (falls back to a plain date). */
|
|
787
|
+
function blameAge(time, t) {
|
|
788
|
+
if (!Number.isFinite(time)) return ''
|
|
789
|
+
const delta = Date.now() - time
|
|
790
|
+
if (delta < 60 * 1000) return t ? t('blame.justNow') : '刚刚'
|
|
791
|
+
if (delta < 60 * 60 * 1000) return t ? t('blame.minutesAgo', { count: String(Math.floor(delta / 60000)) }) : Math.floor(delta / 60000) + ' 分钟前'
|
|
792
|
+
if (delta < 24 * 60 * 60 * 1000) return t ? t('blame.hoursAgo', { count: String(Math.floor(delta / 3600000)) }) : Math.floor(delta / 3600000) + ' 小时前'
|
|
793
|
+
if (delta < 30 * 24 * 60 * 60 * 1000) return t ? t('blame.daysAgo', { count: String(Math.floor(delta / 86400000)) }) : Math.floor(delta / 86400000) + ' 天前'
|
|
794
|
+
try { return new Date(time).toLocaleDateString() } catch (error) { return '' }
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** One-line annotation shown at the end of the dwelled line. */
|
|
798
|
+
function blameAnnotationText(row, t) {
|
|
799
|
+
if (!row) return ''
|
|
800
|
+
if (row.uncommitted) return t ? t('blame.uncommitted') : '未提交'
|
|
801
|
+
const who = row.author || 'unknown'
|
|
802
|
+
const age = blameAge(row.authorTime, t)
|
|
803
|
+
const summary = row.summary || ''
|
|
804
|
+
return who + (age ? ', ' + age : '') + (summary ? ' · ' + summary : '')
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Build the floating commit popover shown when the annotation is hovered.
|
|
809
|
+
* @param {HTMLElement} node editor root node used for positioning
|
|
810
|
+
* @param {object} t translator
|
|
811
|
+
* @returns {object} popover controller
|
|
812
|
+
*/
|
|
813
|
+
function createBlamePopover(node, t) {
|
|
814
|
+
const dom = node.ownerDocument.createElement('div')
|
|
815
|
+
dom.className = 'drp-blame-popover'
|
|
816
|
+
dom.style.display = 'none'
|
|
817
|
+
const head = node.ownerDocument.createElement('div')
|
|
818
|
+
head.className = 'drp-blame-popover-head'
|
|
819
|
+
const author = node.ownerDocument.createElement('span')
|
|
820
|
+
author.className = 'drp-blame-popover-author'
|
|
821
|
+
const when = node.ownerDocument.createElement('span')
|
|
822
|
+
when.className = 'drp-blame-popover-when'
|
|
823
|
+
head.appendChild(author)
|
|
824
|
+
head.appendChild(when)
|
|
825
|
+
const subject = node.ownerDocument.createElement('div')
|
|
826
|
+
subject.className = 'drp-blame-popover-subject'
|
|
827
|
+
const body = node.ownerDocument.createElement('pre')
|
|
828
|
+
body.className = 'drp-blame-popover-body'
|
|
829
|
+
const foot = node.ownerDocument.createElement('div')
|
|
830
|
+
foot.className = 'drp-blame-popover-foot'
|
|
831
|
+
const sha = node.ownerDocument.createElement('code')
|
|
832
|
+
sha.className = 'drp-blame-popover-sha'
|
|
833
|
+
const copy = node.ownerDocument.createElement('button')
|
|
834
|
+
copy.type = 'button'
|
|
835
|
+
copy.className = 'drp-btn drp-blame-popover-copy'
|
|
836
|
+
copy.textContent = t ? t('blame.copySha') : '复制提交号'
|
|
837
|
+
foot.appendChild(sha)
|
|
838
|
+
foot.appendChild(copy)
|
|
839
|
+
dom.appendChild(head)
|
|
840
|
+
dom.appendChild(subject)
|
|
841
|
+
dom.appendChild(body)
|
|
842
|
+
dom.appendChild(foot)
|
|
843
|
+
node.appendChild(dom)
|
|
844
|
+
let current = null
|
|
845
|
+
return {
|
|
846
|
+
dom,
|
|
847
|
+
show(row, rect, commit) {
|
|
848
|
+
current = row
|
|
849
|
+
author.textContent = commit && commit.author ? commit.author : (row.author || 'unknown')
|
|
850
|
+
const age = blameAge(row.authorTime, t)
|
|
851
|
+
const absolute = row.authorTime ? new Date(row.authorTime).toLocaleString() : ''
|
|
852
|
+
when.textContent = age + (absolute ? ' (' + absolute + ')' : '')
|
|
853
|
+
subject.textContent = commit && commit.summary ? commit.summary : (row.summary || '')
|
|
854
|
+
body.textContent = commit && commit.body && commit.body !== subject.textContent ? commit.body : ''
|
|
855
|
+
body.style.display = body.textContent ? '' : 'none'
|
|
856
|
+
sha.textContent = row.short || String(row.sha || '').slice(0, 7)
|
|
857
|
+
dom.style.display = ''
|
|
858
|
+
const nodeRect = node.getBoundingClientRect()
|
|
859
|
+
const width = Math.min(420, Math.max(240, nodeRect.width - 24))
|
|
860
|
+
dom.style.width = width + 'px'
|
|
861
|
+
const left = Math.max(6, Math.min(rect.left - nodeRect.left, nodeRect.width - width - 6))
|
|
862
|
+
dom.style.left = left + 'px'
|
|
863
|
+
const height = dom.offsetHeight || 120
|
|
864
|
+
let top = rect.top - nodeRect.top - height - 6
|
|
865
|
+
if (top < 4) top = rect.bottom - nodeRect.top + 4
|
|
866
|
+
dom.style.top = top + 'px'
|
|
867
|
+
},
|
|
868
|
+
hide() {
|
|
869
|
+
current = null
|
|
870
|
+
dom.style.display = 'none'
|
|
871
|
+
},
|
|
872
|
+
isShowing() {
|
|
873
|
+
return dom.style.display !== 'none'
|
|
874
|
+
},
|
|
875
|
+
getRow() {
|
|
876
|
+
return current
|
|
877
|
+
},
|
|
878
|
+
contains(target) {
|
|
879
|
+
return dom.contains(target)
|
|
880
|
+
},
|
|
881
|
+
dispose() {
|
|
882
|
+
try { dom.remove() } catch (error) { /* optional */ }
|
|
883
|
+
},
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Show the line's blame inline after the cursor rests on that line, and a
|
|
889
|
+
* commit popover when the annotation is hovered.
|
|
890
|
+
* @param {object} editor active Monaco editor
|
|
891
|
+
* @param {object} monaco monaco namespace
|
|
892
|
+
* @param {{ getBlame: () => Array, sessionId: string, t?: Function }} options data access
|
|
893
|
+
*/
|
|
894
|
+
function attachBlameDwell(editor, monaco, options, fallbackNode) {
|
|
895
|
+
const opts = options || {}
|
|
896
|
+
const getBlame = typeof opts.getBlame === 'function' ? opts.getBlame : () => []
|
|
897
|
+
const t = opts.t
|
|
898
|
+
const sessionId = opts.sessionId
|
|
899
|
+
const getPath = typeof opts.getPath === 'function' ? opts.getPath : () => opts.path || ''
|
|
900
|
+
const path = getPath()
|
|
901
|
+
const node = (typeof editor.getContainerDomNode === 'function' && editor.getContainerDomNode())
|
|
902
|
+
|| (typeof editor.getDomNode === 'function' && editor.getDomNode())
|
|
903
|
+
|| fallbackNode
|
|
904
|
+
if (!node || typeof node.addEventListener !== 'function') {
|
|
905
|
+
return { cancel() {}, dispose() {} }
|
|
906
|
+
}
|
|
907
|
+
const popover = createBlamePopover(node, t)
|
|
908
|
+
const annotation = node.ownerDocument.createElement('span')
|
|
909
|
+
annotation.className = 'drp-blame-inline'
|
|
910
|
+
annotation.style.display = 'none'
|
|
911
|
+
node.appendChild(annotation)
|
|
912
|
+
let timer = 0
|
|
913
|
+
let hoverTimer = 0
|
|
914
|
+
let popoverHideTimer = 0
|
|
915
|
+
let decorations = []
|
|
916
|
+
let currentRow = null
|
|
917
|
+
let interacted = false
|
|
918
|
+
let fetchedRows = null
|
|
919
|
+
const infoCache = new Map()
|
|
920
|
+
|
|
921
|
+
const clearInline = () => {
|
|
922
|
+
if (timer) { clearTimeout(timer); timer = 0 }
|
|
923
|
+
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = 0 }
|
|
924
|
+
if (popoverHideTimer) { clearTimeout(popoverHideTimer); popoverHideTimer = 0 }
|
|
925
|
+
popover.hide()
|
|
926
|
+
currentRow = null
|
|
927
|
+
annotation.style.display = 'none'
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const showInline = (row) => {
|
|
931
|
+
if (!row) return
|
|
932
|
+
currentRow = row
|
|
933
|
+
const text = blameAnnotationText(row, t)
|
|
934
|
+
if (!text) return
|
|
935
|
+
annotation.textContent = text
|
|
936
|
+
annotation.title = text
|
|
937
|
+
annotation.style.display = ''
|
|
938
|
+
try {
|
|
939
|
+
const model = typeof editor.getModel === 'function' ? editor.getModel() : null
|
|
940
|
+
const maxColumn = model && typeof model.getLineMaxColumn === 'function' ? model.getLineMaxColumn(row.line) : 1
|
|
941
|
+
const atEnd = typeof editor.getScrolledVisiblePosition === 'function'
|
|
942
|
+
? editor.getScrolledVisiblePosition({ lineNumber: row.line, column: maxColumn })
|
|
943
|
+
: null
|
|
944
|
+
const atStart = typeof editor.getScrolledVisiblePosition === 'function'
|
|
945
|
+
? editor.getScrolledVisiblePosition({ lineNumber: row.line, column: 1 })
|
|
946
|
+
: null
|
|
947
|
+
const nodeWidth = Number(node.clientWidth) || 0
|
|
948
|
+
const width = Number(annotation.offsetWidth) || 0
|
|
949
|
+
const rightLimit = nodeWidth > 0 ? Math.max(12, nodeWidth - width - 10) : 0
|
|
950
|
+
let left = atEnd && Number.isFinite(atEnd.left) ? atEnd.left + 18 : rightLimit
|
|
951
|
+
if (rightLimit > 0) left = Math.min(left, rightLimit)
|
|
952
|
+
annotation.style.left = Math.max(12, left) + 'px'
|
|
953
|
+
annotation.style.top = Math.max(0, atStart && Number.isFinite(atStart.top) ? atStart.top : 0) + 'px'
|
|
954
|
+
annotation.style.height = ((atStart && atStart.height) || 20) + 'px'
|
|
955
|
+
} catch (error) { /* optional */ }
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const rowsOf = () => {
|
|
959
|
+
const rows = getBlame()
|
|
960
|
+
if (Array.isArray(rows) && rows.length) return rows
|
|
961
|
+
return Array.isArray(fetchedRows) ? fetchedRows : []
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const fetchRows = () => {
|
|
965
|
+
const wanted = getPath()
|
|
966
|
+
if (!sessionId || !wanted) return Promise.resolve([])
|
|
967
|
+
if (fetchedRows) return Promise.resolve(fetchedRows)
|
|
968
|
+
return apiGet('blame', { sessionId, path: wanted })
|
|
969
|
+
.then((value) => {
|
|
970
|
+
fetchedRows = value && value.available === true && Array.isArray(value.lines) ? value.lines : []
|
|
971
|
+
return fetchedRows
|
|
972
|
+
})
|
|
973
|
+
.catch((error) => {
|
|
974
|
+
fetchedRows = []
|
|
975
|
+
return []
|
|
976
|
+
})
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const loadInfo = (row) => {
|
|
980
|
+
if (!row || row.uncommitted || !sessionId) return Promise.resolve(null)
|
|
981
|
+
const key = row.sha
|
|
982
|
+
if (infoCache.has(key)) return infoCache.get(key)
|
|
983
|
+
const pending = apiGet('commitInfo', { sessionId, revision: key })
|
|
984
|
+
.then((value) => (value && value.available === true ? value.commit : null))
|
|
985
|
+
.catch(() => null)
|
|
986
|
+
infoCache.set(key, pending)
|
|
987
|
+
return pending
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const openPopover = (row, rect) => {
|
|
991
|
+
if (!row || row.uncommitted) return
|
|
992
|
+
void loadInfo(row).then((commit) => {
|
|
993
|
+
if (!currentRow || currentRow !== row) return
|
|
994
|
+
const span = node.querySelector('.drp-blame-inline')
|
|
995
|
+
popover.show(row, span ? span.getBoundingClientRect() : rect, commit)
|
|
996
|
+
})
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const onPointerOver = (event) => {
|
|
1000
|
+
const target = event.target
|
|
1001
|
+
if (!target || typeof target.closest !== 'function') return
|
|
1002
|
+
if (popover.contains(target)) {
|
|
1003
|
+
if (popoverHideTimer) { clearTimeout(popoverHideTimer); popoverHideTimer = 0 }
|
|
1004
|
+
return
|
|
1005
|
+
}
|
|
1006
|
+
const span = target.closest('.drp-blame-inline')
|
|
1007
|
+
if (!span || !currentRow) {
|
|
1008
|
+
if (!popover.contains(target)) {
|
|
1009
|
+
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = 0 }
|
|
1010
|
+
schedulePopoverHide()
|
|
1011
|
+
}
|
|
1012
|
+
return
|
|
1013
|
+
}
|
|
1014
|
+
if (popoverHideTimer) { clearTimeout(popoverHideTimer); popoverHideTimer = 0 }
|
|
1015
|
+
if (hoverTimer) clearTimeout(hoverTimer)
|
|
1016
|
+
hoverTimer = setTimeout(() => {
|
|
1017
|
+
hoverTimer = 0
|
|
1018
|
+
openPopover(currentRow, span.getBoundingClientRect())
|
|
1019
|
+
}, BLAME_POPOVER_DELAY_MS)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
let selecting = false
|
|
1023
|
+
const schedulePopoverHide = () => {
|
|
1024
|
+
if (selecting) return
|
|
1025
|
+
if (popoverHideTimer) clearTimeout(popoverHideTimer)
|
|
1026
|
+
popoverHideTimer = setTimeout(() => {
|
|
1027
|
+
popoverHideTimer = 0
|
|
1028
|
+
popover.hide()
|
|
1029
|
+
}, 450)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const onPointerLeave = (event) => {
|
|
1033
|
+
const related = event.relatedTarget
|
|
1034
|
+
if (related && (popover.contains(related) || related.closest?.('.drp-blame-inline'))) return
|
|
1035
|
+
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = 0 }
|
|
1036
|
+
schedulePopoverHide()
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const copyButton = popover.dom.querySelector('.drp-blame-popover-copy')
|
|
1040
|
+
const writeClipboard = (text) => {
|
|
1041
|
+
try {
|
|
1042
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
1043
|
+
return Promise.resolve(navigator.clipboard.writeText(text))
|
|
1044
|
+
}
|
|
1045
|
+
} catch (error) { /* fall through */ }
|
|
1046
|
+
return new Promise((resolve, reject) => {
|
|
1047
|
+
try {
|
|
1048
|
+
const area = node.ownerDocument.createElement('textarea')
|
|
1049
|
+
area.value = text
|
|
1050
|
+
area.setAttribute('readonly', '')
|
|
1051
|
+
area.style.position = 'fixed'
|
|
1052
|
+
area.style.top = '-1000px'
|
|
1053
|
+
area.style.opacity = '0'
|
|
1054
|
+
node.ownerDocument.body.appendChild(area)
|
|
1055
|
+
area.select()
|
|
1056
|
+
const ok = typeof node.ownerDocument.execCommand === 'function' && node.ownerDocument.execCommand('copy')
|
|
1057
|
+
area.remove()
|
|
1058
|
+
if (ok) resolve()
|
|
1059
|
+
else reject(new Error('copy-command-failed'))
|
|
1060
|
+
} catch (error) { reject(error) }
|
|
1061
|
+
})
|
|
1062
|
+
}
|
|
1063
|
+
const flashCopy = (text) => {
|
|
1064
|
+
if (!copyButton) return
|
|
1065
|
+
copyButton.textContent = text
|
|
1066
|
+
setTimeout(() => { if (copyButton) copyButton.textContent = t ? t('blame.copySha') : '复制提交号' }, 1200)
|
|
1067
|
+
}
|
|
1068
|
+
const onCopy = () => {
|
|
1069
|
+
const row = popover.getRow()
|
|
1070
|
+
if (!row || !row.sha) return
|
|
1071
|
+
writeClipboard(String(row.sha)).then(
|
|
1072
|
+
() => flashCopy(t ? t('blame.copied') : '已复制'),
|
|
1073
|
+
() => flashCopy(t ? t('blame.copyFailed') : '复制失败'),
|
|
1074
|
+
)
|
|
1075
|
+
}
|
|
1076
|
+
if (copyButton) copyButton.addEventListener('click', onCopy)
|
|
1077
|
+
|
|
1078
|
+
const markInteracted = () => { interacted = true }
|
|
1079
|
+
node.addEventListener('mousedown', markInteracted)
|
|
1080
|
+
node.addEventListener('keydown', markInteracted)
|
|
1081
|
+
|
|
1082
|
+
const positionSubscription = editor.onDidChangeCursorPosition((event) => {
|
|
1083
|
+
clearInline()
|
|
1084
|
+
const focused = typeof editor.hasTextFocus === 'function' ? editor.hasTextFocus() : true
|
|
1085
|
+
if (!interacted && !focused) return
|
|
1086
|
+
const line = event.position.lineNumber
|
|
1087
|
+
timer = setTimeout(() => {
|
|
1088
|
+
timer = 0
|
|
1089
|
+
const rowsNow = rowsOf()
|
|
1090
|
+
let row = rowsNow.find((entry) => entry && entry.line === line)
|
|
1091
|
+
if (row) {
|
|
1092
|
+
showInline(row)
|
|
1093
|
+
return
|
|
1094
|
+
}
|
|
1095
|
+
// The prefetched rows may still be in flight (or failed): fetch once here.
|
|
1096
|
+
void fetchRows().then((rows) => {
|
|
1097
|
+
row = rows.find((entry) => entry && entry.line === line)
|
|
1098
|
+
if (row) showInline(row)
|
|
1099
|
+
})
|
|
1100
|
+
}, BLAME_DWELL_MS)
|
|
1101
|
+
})
|
|
1102
|
+
const scrollSubscription = typeof editor.onDidScrollChange === 'function'
|
|
1103
|
+
? editor.onDidScrollChange(() => clearInline())
|
|
1104
|
+
: null
|
|
1105
|
+
const blurSubscription = null
|
|
1106
|
+
|
|
1107
|
+
const onDocumentMouseDown = (event) => {
|
|
1108
|
+
const target = event.target
|
|
1109
|
+
if (!target || typeof target.closest !== 'function') return
|
|
1110
|
+
if (target.closest('.drp-blame-popover')) { selecting = true; return }
|
|
1111
|
+
if (target.closest('.drp-blame-inline')) return
|
|
1112
|
+
clearInline()
|
|
1113
|
+
}
|
|
1114
|
+
const onDocumentMouseUp = () => { selecting = false }
|
|
1115
|
+
const doc = node.ownerDocument
|
|
1116
|
+
if (doc && typeof doc.addEventListener === 'function') {
|
|
1117
|
+
doc.addEventListener('mousedown', onDocumentMouseDown, true)
|
|
1118
|
+
doc.addEventListener('mouseup', onDocumentMouseUp, true)
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
node.addEventListener('mouseover', onPointerOver)
|
|
1122
|
+
node.addEventListener('mouseout', onPointerLeave)
|
|
1123
|
+
|
|
1124
|
+
return {
|
|
1125
|
+
cancel: clearInline,
|
|
1126
|
+
dispose() {
|
|
1127
|
+
clearInline()
|
|
1128
|
+
try { positionSubscription.dispose() } catch (error) { /* optional */ }
|
|
1129
|
+
try { scrollSubscription?.dispose?.() } catch (error) { /* optional */ }
|
|
1130
|
+
try { blurSubscription?.dispose?.() } catch (error) { /* optional */ }
|
|
1131
|
+
node.removeEventListener('mouseover', onPointerOver)
|
|
1132
|
+
node.removeEventListener('mouseout', onPointerLeave)
|
|
1133
|
+
try {
|
|
1134
|
+
doc?.removeEventListener('mousedown', onDocumentMouseDown, true)
|
|
1135
|
+
doc?.removeEventListener('mouseup', onDocumentMouseUp, true)
|
|
1136
|
+
} catch (error) { /* optional */ }
|
|
1137
|
+
node.removeEventListener('mousedown', markInteracted)
|
|
1138
|
+
node.removeEventListener('keydown', markInteracted)
|
|
1139
|
+
if (copyButton) copyButton.removeEventListener('click', onCopy)
|
|
1140
|
+
try { annotation.remove() } catch (error) { /* optional */ }
|
|
1141
|
+
popover.dispose()
|
|
1142
|
+
},
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function MonacoHost(props) {
|
|
1147
|
+
const { monaco, file, sessionId, regions, blame, wrap, minimap, reveal, scheme, fontSize, documentEpoch, onCursor, onReady, onError, onRequestLine, onRequestOutline, onModelReady, onLsp, t } = props
|
|
1148
|
+
const hostRef = React.useRef(null)
|
|
1149
|
+
const editorRef = React.useRef(null)
|
|
1150
|
+
const rendererRef = React.useRef(null)
|
|
1151
|
+
const modelsRef = React.useRef(new Map())
|
|
1152
|
+
const viewStatesRef = React.useRef(new Map())
|
|
1153
|
+
const fontSizeRef = React.useRef(13)
|
|
1154
|
+
const callbacksRef = React.useRef({ onCursor, onReady, onError, onRequestLine, onRequestOutline, onModelReady, onLsp })
|
|
1155
|
+
const blameDwellRef = React.useRef(null)
|
|
1156
|
+
const blameRef = React.useRef(blame)
|
|
1157
|
+
blameRef.current = blame
|
|
1158
|
+
const fileRef = React.useRef(file)
|
|
1159
|
+
fileRef.current = file
|
|
1160
|
+
const tRef = React.useRef(t)
|
|
1161
|
+
tRef.current = t
|
|
1162
|
+
callbacksRef.current = { onCursor, onReady, onError, onRequestLine, onRequestOutline, onModelReady, onLsp }
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Create the editor once the host node and Monaco both exist.
|
|
1166
|
+
* @param {HTMLDivElement | null} node host node
|
|
1167
|
+
*/
|
|
1168
|
+
const attach = React.useCallback((node) => {
|
|
1169
|
+
hostRef.current = node
|
|
1170
|
+
if (!node || editorRef.current || !monaco) return
|
|
1171
|
+
try {
|
|
1172
|
+
const editor = monaco.editor.create(node, {
|
|
1173
|
+
value: '',
|
|
1174
|
+
language: 'plaintext',
|
|
1175
|
+
theme: themeRuntime.scheme === 'light' ? MONACO_THEME.light : MONACO_THEME.dark,
|
|
1176
|
+
readOnly: true,
|
|
1177
|
+
domReadOnly: true,
|
|
1178
|
+
renderValidationDecorations: 'on',
|
|
1179
|
+
automaticLayout: true,
|
|
1180
|
+
scrollBeyondLastLine: false,
|
|
1181
|
+
wordWrap: 'off',
|
|
1182
|
+
minimap: { enabled: false },
|
|
1183
|
+
glyphMargin: false,
|
|
1184
|
+
lineNumbers: 'on',
|
|
1185
|
+
lineDecorationsWidth: 14,
|
|
1186
|
+
folding: true,
|
|
1187
|
+
bracketPairColorization: { enabled: true },
|
|
1188
|
+
matchBrackets: 'always',
|
|
1189
|
+
renderLineHighlight: 'none',
|
|
1190
|
+
renderWhitespace: 'selection',
|
|
1191
|
+
smoothScrolling: false,
|
|
1192
|
+
cursorBlinking: 'smooth',
|
|
1193
|
+
mouseWheelZoom: true,
|
|
1194
|
+
contextmenu: true,
|
|
1195
|
+
// Let hovers / suggest widgets escape the editor's clipped box: inside the
|
|
1196
|
+
// narrow right sidebar a clipped hover hides its first lines behind the
|
|
1197
|
+
// toolbar (user report). Fixed overflow widgets are positioned in the
|
|
1198
|
+
// viewport, so the tooltip stays readable.
|
|
1199
|
+
fixedOverflowWidgets: true,
|
|
1200
|
+
hover: { above: false, sticky: true, delay: 120 },
|
|
1201
|
+
fontSize: fontSizeRef.current,
|
|
1202
|
+
lineHeight: 20,
|
|
1203
|
+
fontFamily: 'var(--ds-font-family-code, var(--dsh-code-font-family, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace))',
|
|
1204
|
+
tabSize: 2,
|
|
1205
|
+
padding: { top: 8, bottom: 24 },
|
|
1206
|
+
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10, useShadows: false },
|
|
1207
|
+
})
|
|
1208
|
+
rendererRef.current = createDiffRenderer(monaco, editor)
|
|
1209
|
+
editorRef.current = editor
|
|
1210
|
+
try {
|
|
1211
|
+
editor.onDidChangeCursorPosition((event) => {
|
|
1212
|
+
callbacksRef.current.onCursor?.({ line: event.position.lineNumber, column: event.position.column })
|
|
1213
|
+
})
|
|
1214
|
+
} catch (error) { /* optional */ }
|
|
1215
|
+
try { blameDwellRef.current = attachBlameDwell(editor, monaco, { getBlame: () => blameRef.current, sessionId, getPath: () => (fileRef.current && fileRef.current.path) || '', t: tRef.current }, hostRef.current) } catch (error) { /* optional */ }
|
|
1216
|
+
try {
|
|
1217
|
+
const key = monaco.KeyMod
|
|
1218
|
+
const code = monaco.KeyCode
|
|
1219
|
+
const bump = (delta) => {
|
|
1220
|
+
fontSizeRef.current = Math.min(28, Math.max(9, fontSizeRef.current + delta))
|
|
1221
|
+
editor.updateOptions({ fontSize: fontSizeRef.current })
|
|
1222
|
+
rendererRef.current?.reflow()
|
|
1223
|
+
}
|
|
1224
|
+
editor.addCommand(key.CtrlCmd | code.Equal, () => bump(1))
|
|
1225
|
+
editor.addCommand(key.CtrlCmd | code.Minus, () => bump(-1))
|
|
1226
|
+
editor.addCommand(key.CtrlCmd | code.Digit0, () => { fontSizeRef.current = 13; editor.updateOptions({ fontSize: 13 }) })
|
|
1227
|
+
// VS Code go-to-line parity inside the editor (the page-level command
|
|
1228
|
+
// covers the non-editor region with the same overlay).
|
|
1229
|
+
editor.addCommand(key.CtrlCmd | code.KeyG, () => {
|
|
1230
|
+
try {
|
|
1231
|
+
const action = editor.getAction('editor.action.gotoLine')
|
|
1232
|
+
if (action) action.run()
|
|
1233
|
+
else callbacksRef.current.onRequestLine?.()
|
|
1234
|
+
} catch (error) { /* ignore */ }
|
|
1235
|
+
})
|
|
1236
|
+
editor.addCommand(key.CtrlCmd | key.Shift | code.KeyO, () => callbacksRef.current.onRequestOutline?.())
|
|
1237
|
+
// Language-server navigation: F12 definition / Shift+F12 references.
|
|
1238
|
+
editor.addCommand(code.F12, () => {
|
|
1239
|
+
try {
|
|
1240
|
+
const position = lspPosition(editor)
|
|
1241
|
+
if (position) callbacksRef.current.onLsp?.('definition', position.lineNumber, position.column)
|
|
1242
|
+
} catch (error) { /* ignore */ }
|
|
1243
|
+
})
|
|
1244
|
+
editor.addCommand(key.Shift | code.F12, () => {
|
|
1245
|
+
try {
|
|
1246
|
+
const position = lspPosition(editor)
|
|
1247
|
+
if (position) callbacksRef.current.onLsp?.('references', position.lineNumber, position.column)
|
|
1248
|
+
} catch (error) { /* ignore */ }
|
|
1249
|
+
})
|
|
1250
|
+
editor.onMouseUp((event) => {
|
|
1251
|
+
if (!(event.event.ctrlKey || event.event.metaKey) || !event.event.leftButton || !event.target.position) return
|
|
1252
|
+
const position = event.target.position
|
|
1253
|
+
callbacksRef.current.onLsp?.('definition', position.lineNumber, position.column)
|
|
1254
|
+
})
|
|
1255
|
+
} catch (error) { /* keybindings are best-effort */ }
|
|
1256
|
+
callbacksRef.current.onReady?.({
|
|
1257
|
+
revealLine: (line) => {
|
|
1258
|
+
try {
|
|
1259
|
+
const target = Math.max(1, Number(line) || 1)
|
|
1260
|
+
editor.revealLineInCenterIfOutsideViewport?.(target)
|
|
1261
|
+
editor.revealLineInCenter(target)
|
|
1262
|
+
editor.setPosition({ lineNumber: target, column: 1 })
|
|
1263
|
+
} catch (error) { /* ignore */ }
|
|
1264
|
+
},
|
|
1265
|
+
focus: () => { try { editor.focus() } catch (error) { /* ignore */ } },
|
|
1266
|
+
layout: () => { try { editor.layout() } catch (error) { /* ignore */ } },
|
|
1267
|
+
getEditor: () => editor,
|
|
1268
|
+
setFontSize: (size) => { fontSizeRef.current = Math.min(28, Math.max(9, Number(size) || 13)); editor.updateOptions({ fontSize: fontSizeRef.current }) },
|
|
1269
|
+
})
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
log('error', 'monaco editor creation failed', error)
|
|
1272
|
+
callbacksRef.current.onError?.(error)
|
|
1273
|
+
}
|
|
1274
|
+
}, [monaco])
|
|
1275
|
+
|
|
1276
|
+
// Disposal.
|
|
1277
|
+
React.useEffect(() => () => {
|
|
1278
|
+
try { blameDwellRef.current?.dispose?.() } catch (error) { /* ignore */ }
|
|
1279
|
+
blameDwellRef.current = null
|
|
1280
|
+
try { rendererRef.current?.dispose() } catch (error) { /* ignore */ }
|
|
1281
|
+
rendererRef.current = null
|
|
1282
|
+
const editor = editorRef.current
|
|
1283
|
+
lspClearDocument(editor?.getModel?.())
|
|
1284
|
+
editorRef.current = null
|
|
1285
|
+
if (editor) {
|
|
1286
|
+
try { editor.dispose() } catch (error) { /* ignore */ }
|
|
1287
|
+
}
|
|
1288
|
+
for (const model of modelsRef.current.values()) {
|
|
1289
|
+
try { model.dispose() } catch (error) { /* ignore */ }
|
|
1290
|
+
}
|
|
1291
|
+
modelsRef.current.clear()
|
|
1292
|
+
}, [])
|
|
1293
|
+
|
|
1294
|
+
// Model switching with per-file view state. The host stays mounted while a
|
|
1295
|
+
// file loads, so `file` may be null or still loading: the previous model then
|
|
1296
|
+
// stays visible (the page paints a loading veil on top) and the scroll
|
|
1297
|
+
// position of each file survives a switch because the editor is never
|
|
1298
|
+
// destroyed and each model's Monaco view state is restored synchronously.
|
|
1299
|
+
React.useEffect(() => {
|
|
1300
|
+
const editor = editorRef.current
|
|
1301
|
+
if (!editor || !monaco || !file || file.kind !== 'text') return
|
|
1302
|
+
const path = file.path
|
|
1303
|
+
if (!path || typeof file.text !== 'string') return
|
|
1304
|
+
let model = modelsRef.current.get(path)
|
|
1305
|
+
try {
|
|
1306
|
+
if (!model) {
|
|
1307
|
+
const uri = monaco.Uri.parse('did:///' + encodeURIComponent(sessionId || 'workspace') + '/' + path.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/'))
|
|
1308
|
+
model = monaco.editor.getModel(uri) || monaco.editor.createModel(file.text, file.language || 'plaintext', uri)
|
|
1309
|
+
modelsRef.current.set(path, model)
|
|
1310
|
+
} else if (model.getValue() !== file.text) {
|
|
1311
|
+
// Content changed underneath (refresh): keep the user's scroll position.
|
|
1312
|
+
const saved = editor.getModel() === model ? editor.saveViewState() : viewStatesRef.current.get(model.uri.toString())
|
|
1313
|
+
model.setValue(file.text)
|
|
1314
|
+
if (saved) {
|
|
1315
|
+
try { editor.restoreViewState(saved) } catch (error) { /* ignore */ }
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (model.getLanguageId() !== (file.language || 'plaintext')) monaco.editor.setModelLanguage(model, file.language || 'plaintext')
|
|
1319
|
+
if (editor.getModel() !== model) {
|
|
1320
|
+
try {
|
|
1321
|
+
const previous = editor.getModel()
|
|
1322
|
+
if (previous) viewStatesRef.current.set(previous.uri.toString(), editor.saveViewState())
|
|
1323
|
+
} catch (error) { /* ignore */ }
|
|
1324
|
+
editor.setModel(model)
|
|
1325
|
+
try {
|
|
1326
|
+
const saved = viewStatesRef.current.get(model.uri.toString())
|
|
1327
|
+
if (saved) editor.restoreViewState(saved)
|
|
1328
|
+
} catch (error) { /* ignore */ }
|
|
1329
|
+
try { editor.layout() } catch (error) { /* ignore */ }
|
|
1330
|
+
}
|
|
1331
|
+
callbacksRef.current.onModelReady?.({
|
|
1332
|
+
path,
|
|
1333
|
+
text: model.getValue(),
|
|
1334
|
+
language: model.getLanguageId(),
|
|
1335
|
+
monaco,
|
|
1336
|
+
model,
|
|
1337
|
+
})
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
log('error', 'monaco model switch failed', error)
|
|
1340
|
+
}
|
|
1341
|
+
}, [monaco, sessionId, file && file.path, file && file.text, file && file.language, file && file.kind, file && file.missing, documentEpoch])
|
|
1342
|
+
|
|
1343
|
+
// Inline diff decorations.
|
|
1344
|
+
React.useEffect(() => {
|
|
1345
|
+
const renderer = rendererRef.current
|
|
1346
|
+
if (!renderer) return
|
|
1347
|
+
renderer.render(regions || [])
|
|
1348
|
+
}, [regions])
|
|
1349
|
+
|
|
1350
|
+
// Editor options.
|
|
1351
|
+
React.useEffect(() => {
|
|
1352
|
+
const editor = editorRef.current
|
|
1353
|
+
if (!editor) return
|
|
1354
|
+
try {
|
|
1355
|
+
editor.updateOptions({ wordWrap: wrap ? 'on' : 'off', minimap: { enabled: minimap === true } })
|
|
1356
|
+
rendererRef.current?.reflow()
|
|
1357
|
+
} catch (error) { /* ignore */ }
|
|
1358
|
+
}, [wrap, minimap])
|
|
1359
|
+
|
|
1360
|
+
// Font size is prop-driven so menu commands always reach the live editor
|
|
1361
|
+
// (the imperative controls object is only a convenience path).
|
|
1362
|
+
React.useEffect(() => {
|
|
1363
|
+
const editor = editorRef.current
|
|
1364
|
+
if (!editor) return
|
|
1365
|
+
const next = Math.min(28, Math.max(9, Math.round(Number(fontSize) || 13)))
|
|
1366
|
+
fontSizeRef.current = next
|
|
1367
|
+
try {
|
|
1368
|
+
editor.updateOptions({ fontSize: next })
|
|
1369
|
+
rendererRef.current?.reflow()
|
|
1370
|
+
} catch (error) { /* ignore */ }
|
|
1371
|
+
}, [fontSize, file && file.path])
|
|
1372
|
+
|
|
1373
|
+
// Theme switching: re-apply the plugin theme and repaint zone decorations.
|
|
1374
|
+
React.useEffect(() => {
|
|
1375
|
+
const editor = editorRef.current
|
|
1376
|
+
if (!editor || !monaco) return
|
|
1377
|
+
try {
|
|
1378
|
+
applyMonacoTheme(monaco)
|
|
1379
|
+
rendererRef.current?.reflow()
|
|
1380
|
+
} catch (error) { /* ignore */ }
|
|
1381
|
+
}, [monaco, scheme])
|
|
1382
|
+
|
|
1383
|
+
// Reveal requests from hunk navigation / file open.
|
|
1384
|
+
React.useEffect(() => {
|
|
1385
|
+
const editor = editorRef.current
|
|
1386
|
+
if (!editor || !reveal || !reveal.line) return
|
|
1387
|
+
try {
|
|
1388
|
+
const target = Math.max(1, Number(reveal.line) || 1)
|
|
1389
|
+
editor.revealLineInCenter(target)
|
|
1390
|
+
const column = Math.max(1, Number(reveal.column) || 1)
|
|
1391
|
+
if (reveal.endLine && reveal.endColumn) {
|
|
1392
|
+
const range = { startLineNumber: target, startColumn: column, endLineNumber: reveal.endLine, endColumn: reveal.endColumn }
|
|
1393
|
+
editor.setSelection(range)
|
|
1394
|
+
editor.revealRangeInCenter(range)
|
|
1395
|
+
} else {
|
|
1396
|
+
editor.setPosition({ lineNumber: target, column })
|
|
1397
|
+
editor.revealPositionInCenter({ lineNumber: target, column })
|
|
1398
|
+
}
|
|
1399
|
+
editor.focus()
|
|
1400
|
+
} catch (error) { /* ignore */ }
|
|
1401
|
+
}, [reveal && reveal.line, reveal && reveal.nonce])
|
|
1402
|
+
|
|
1403
|
+
return h('div', { className: 'drp-monaco-host', ref: attach })
|
|
1404
|
+
}
|