@young1lin/dsh-ui-gitworkbench 0.1.8 → 0.1.9
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/CHANGELOG.md +6 -0
- package/CHANGELOG_EN.md +6 -0
- package/lib/client.js +388 -357
- package/package.json +1 -1
- package/src/client/GitWorkbenchPanel.module.css +3 -0
- package/src/client/GitWorkbenchPanel.tsx +23 -7
- package/src/client/highlight.ts +58 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@young1lin/dsh-ui-gitworkbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Out-of-tree dsh web UI plugin: a session-header git workbench chip opening a drawer with the file tree, per-file diff, history, compare, staging, commit, and sync (fetch/pull/push).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -1810,6 +1810,9 @@
|
|
|
1810
1810
|
the FILE rather than of whatever is currently rendered. Spans every column,
|
|
1811
1811
|
including the blame gutter's. */
|
|
1812
1812
|
.sideSpacer { grid-column: 1 / -1; }
|
|
1813
|
+
/* The same, for the unified view's flex column. `flex: none` so it is exactly
|
|
1814
|
+
the height it claims rather than one stretched by its siblings. */
|
|
1815
|
+
.diffSpacer { flex: none; }
|
|
1813
1816
|
.sideCodeSame { color: var(--gs-fg-muted); }
|
|
1814
1817
|
.sideCodeAdd { background: var(--gs-add-line); }
|
|
1815
1818
|
.sideCodeDel { background: var(--gs-del-line); }
|
|
@@ -89,7 +89,7 @@ import {
|
|
|
89
89
|
fileCheckState, nextAction, nextBatch, pathsFor, rollUp, settledTicks, withPendingTicks,
|
|
90
90
|
type CheckState, type Tick, type TickAction,
|
|
91
91
|
} from './stage-tree.ts'
|
|
92
|
-
import { grammarLoadCount, highlightFile,
|
|
92
|
+
import { grammarLoadCount, highlightFile, highlightForRowsWindow, highlightWholeFile, highlightWindow, shikiLangOf, shikiThemeOf, subscribeGrammarLoaded, type HighlightRun } from './highlight.ts'
|
|
93
93
|
import { badgeRepeatsBranch, bindingChanged, branchOfWorktree, pathKey, probesClosedBinding, samePath, showsPending, splitPath, turnSettled, viewedPath } from './worktree-view.ts'
|
|
94
94
|
import { BUSY_DELAY_MS, BUSY_HOLD_MS, holdRemaining, quietlyDisabled } from './op-feedback.ts'
|
|
95
95
|
import type { WorkbenchKey } from './locales.ts'
|
|
@@ -5059,14 +5059,25 @@ function DiffView({ segment, path, palette, t }: {
|
|
|
5059
5059
|
const grammarGen = useSyncExternalStore(subscribeGrammarLoaded, grammarLoadCount)
|
|
5060
5060
|
const rowsWithWords = useMemo(() => attachWordRanges(parseRows(segment)), [segment])
|
|
5061
5061
|
const sides = useMemo(() => gutterSides(rowsWithWords), [rowsWithWords])
|
|
5062
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
5063
|
+
// Windowed for the same reason the side-by-side pane is: a unified diff of a
|
|
5064
|
+
// long file put every row in the DOM and re-lexed every one of them, so
|
|
5065
|
+
// opening one froze the pane in exactly the same way.
|
|
5066
|
+
const win = useRowWindow(scrollRef, rowsWithWords.length)
|
|
5062
5067
|
const syntax = useMemo(
|
|
5063
|
-
() =>
|
|
5064
|
-
[rowsWithWords, lang, shikiTheme, grammarGen],
|
|
5068
|
+
() => highlightForRowsWindow(rowsWithWords, lang, shikiTheme, win.start, win.end),
|
|
5069
|
+
[rowsWithWords, lang, shikiTheme, win.start, win.end, grammarGen],
|
|
5065
5070
|
)
|
|
5066
5071
|
const blocks = useMemo(() => unifiedBlocks(rowsWithWords.map(row => row.kind)), [rowsWithWords])
|
|
5067
5072
|
const changes = useMemo(() => countBlocks(blocks), [blocks])
|
|
5068
|
-
|
|
5069
|
-
|
|
5073
|
+
// Derived rather than measured, because a windowed pane has no element for
|
|
5074
|
+
// the block being walked to.
|
|
5075
|
+
const blocksForNav = useRef<readonly number[]>(blocks)
|
|
5076
|
+
blocksForNav.current = blocks
|
|
5077
|
+
const { goToChange } = useChangeNav(
|
|
5078
|
+
scrollRef,
|
|
5079
|
+
useCallback(() => blockTopsFromRows(blocksForNav.current, DIFF_ROW_H, DIFF_GRID_PAD_TOP), []),
|
|
5080
|
+
)
|
|
5070
5081
|
// Read by the key listener below, which is attached once. `goToChange` only
|
|
5071
5082
|
// ever touches refs, but pinning it here says so rather than relying on it.
|
|
5072
5083
|
const walk = useRef(goToChange)
|
|
@@ -5112,7 +5123,10 @@ function DiffView({ segment, path, palette, t }: {
|
|
|
5112
5123
|
) : null}
|
|
5113
5124
|
<div ref={scrollRef} className={css.diffScroll} tabIndex={-1}>
|
|
5114
5125
|
<pre className={css.diffPre}>
|
|
5115
|
-
{
|
|
5126
|
+
{win.padTop > 0 ? <div className={css.diffSpacer} style={{ height: `${win.padTop}px` }} aria-hidden="true" /> : null}
|
|
5127
|
+
{rowsWithWords.slice(win.start, win.end).map((row, k) => {
|
|
5128
|
+
const i = win.start + k
|
|
5129
|
+
return (
|
|
5116
5130
|
<div key={i} className={`${css.line} ${rowClass(row.kind)}`} data-block={blocks[i]! >= 0 ? blocks[i] : undefined}>
|
|
5117
5131
|
{sides.old ? <span className={css.lnOld}>{row.kind === 'add' || row.kind === 'hunk' ? '' : row.oldL}</span> : null}
|
|
5118
5132
|
{sides.new ? <span className={css.lnNew}>{row.kind === 'del' || row.kind === 'hunk' ? '' : row.newL}</span> : null}
|
|
@@ -5121,7 +5135,9 @@ function DiffView({ segment, path, palette, t }: {
|
|
|
5121
5135
|
</span>
|
|
5122
5136
|
<span className={css.code}>{renderCode(row, syntax[i] ?? [])}</span>
|
|
5123
5137
|
</div>
|
|
5124
|
-
|
|
5138
|
+
)
|
|
5139
|
+
})}
|
|
5140
|
+
{win.padBottom > 0 ? <div className={css.diffSpacer} style={{ height: `${win.padBottom}px` }} aria-hidden="true" /> : null}
|
|
5125
5141
|
</pre>
|
|
5126
5142
|
</div>
|
|
5127
5143
|
</div>
|
package/src/client/highlight.ts
CHANGED
|
@@ -317,6 +317,39 @@ export function highlightForRows(
|
|
|
317
317
|
lang: string | undefined,
|
|
318
318
|
theme = 'github-dark-default',
|
|
319
319
|
): HighlightRun[][] {
|
|
320
|
+
// One implementation, asked for the whole range. The panes all window now;
|
|
321
|
+
// this shape is what a caller wants when it really does need every row.
|
|
322
|
+
return highlightForRowsWindow(rows, lang, theme, 0, rows.length)
|
|
323
|
+
.map((runs, i) => runs ?? [{ text: rows[i]!.text, color: undefined }])
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* {@link highlightForRows} for the rows in a window, and nothing outside it.
|
|
328
|
+
*
|
|
329
|
+
* Same reason as {@link highlightWindow}, in the view History and Compare use:
|
|
330
|
+
* a unified diff of a long file re-lexed every one of its rows, so opening one
|
|
331
|
+
* froze the pane exactly as the side-by-side view did.
|
|
332
|
+
*
|
|
333
|
+
* The mapping from rows to the two sides' line arrays is built over ALL rows —
|
|
334
|
+
* it is array bookkeeping with no Shiki in it, and a row's position on its side
|
|
335
|
+
* depends on every row before it. Only the tokenizing is windowed, and because
|
|
336
|
+
* a window of rows is contiguous, so is the span of lines it needs from each
|
|
337
|
+
* side.
|
|
338
|
+
*
|
|
339
|
+
* @param rows - parsed unified-diff rows.
|
|
340
|
+
* @param lang - from {@link shikiLangOf}.
|
|
341
|
+
* @param theme - from {@link shikiThemeOf}.
|
|
342
|
+
* @param from - first row in the window.
|
|
343
|
+
* @param to - one past the last row in the window.
|
|
344
|
+
* @returns an array indexed by ROW, filled only inside the window.
|
|
345
|
+
*/
|
|
346
|
+
export function highlightForRowsWindow(
|
|
347
|
+
rows: readonly Row[],
|
|
348
|
+
lang: string | undefined,
|
|
349
|
+
theme: string,
|
|
350
|
+
from: number,
|
|
351
|
+
to: number,
|
|
352
|
+
): (HighlightRun[] | undefined)[] {
|
|
320
353
|
const oldLines: string[] = []
|
|
321
354
|
const newLines: string[] = []
|
|
322
355
|
const oldAt: number[] = []
|
|
@@ -340,11 +373,29 @@ export function highlightForRows(
|
|
|
340
373
|
newAt.push(-1)
|
|
341
374
|
}
|
|
342
375
|
}
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
376
|
+
const first = Math.max(0, Math.trunc(from))
|
|
377
|
+
const last = Math.min(rows.length, Math.trunc(to))
|
|
378
|
+
const span = (at: readonly number[]): { from: number; to: number } => {
|
|
379
|
+
let lo = -1
|
|
380
|
+
let hi = -1
|
|
381
|
+
for (let i = first; i < last; i += 1) {
|
|
382
|
+
const j = at[i]!
|
|
383
|
+
if (j < 0) continue
|
|
384
|
+
if (lo < 0) lo = j
|
|
385
|
+
hi = j
|
|
386
|
+
}
|
|
387
|
+
return lo < 0 ? { from: 0, to: 0 } : { from: lo, to: hi + 1 }
|
|
388
|
+
}
|
|
389
|
+
const oldSpan = span(oldAt)
|
|
390
|
+
const newSpan = span(newAt)
|
|
391
|
+
const oldTok = highlightWindow(oldLines, lang, theme, oldSpan.from, oldSpan.to)
|
|
392
|
+
const newTok = highlightWindow(newLines, lang, theme, newSpan.from, newSpan.to)
|
|
393
|
+
const out: (HighlightRun[] | undefined)[] = new Array<HighlightRun[] | undefined>(rows.length)
|
|
394
|
+
for (let i = first; i < last; i += 1) {
|
|
395
|
+
const row = rows[i]!
|
|
396
|
+
if (row.kind === 'del') out[i] = oldTok?.[oldAt[i]!] ?? [{ text: row.text, color: undefined }]
|
|
397
|
+
else if (row.kind === 'add' || row.kind === 'context') out[i] = newTok?.[newAt[i]!] ?? [{ text: row.text, color: undefined }]
|
|
398
|
+
else out[i] = [{ text: row.text, color: undefined }]
|
|
399
|
+
}
|
|
400
|
+
return out
|
|
350
401
|
}
|