@young1lin/dsh-ui-gitworkbench 0.1.11 → 0.1.13
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/AGENTS.md +1 -1
- package/CHANGELOG.md +28 -0
- package/CHANGELOG_EN.md +28 -0
- package/README.md +100 -50
- package/README_EN.md +2 -1
- package/lib/client.js +6902 -6256
- package/package.json +1 -1
- package/src/client/ChangesFileTree.tsx +550 -0
- package/src/client/ChromeGlyph.tsx +27 -0
- package/src/client/CodeEditor.tsx +129 -3
- package/src/client/CommitHistory.tsx +1001 -0
- package/src/client/DiffViews.tsx +1070 -0
- package/src/client/GitWorkbenchPanel.module.css +15 -2513
- package/src/client/GitWorkbenchPanel.tsx +37 -3959
- package/src/client/PaneDivider.tsx +74 -0
- package/src/client/WorkbenchControls.tsx +1047 -0
- package/src/client/WorktreeGlyph.tsx +24 -0
- package/src/client/cm-search-theme.ts +250 -0
- package/src/client/diff-nav.ts +59 -0
- package/src/client/git-workbench-types.ts +252 -0
- package/src/client/locales.ts +14 -8
- package/src/client/row-window.ts +23 -0
- package/src/client/search-count.ts +125 -0
- package/src/client/side-rows.ts +66 -0
- package/src/client/styles/changes.css +505 -0
- package/src/client/styles/controls.css +236 -0
- package/src/client/styles/environment.css +89 -0
- package/src/client/styles/files.css +113 -0
- package/src/client/styles/history-filters.css +400 -0
- package/src/client/styles/history.css +276 -0
- package/src/client/styles/image.css +67 -0
- package/src/client/styles/operations.css +179 -0
- package/src/client/styles/shell.css +431 -0
- package/src/client/styles/themes.css +224 -0
- package/src/client/use-change-nav.ts +6 -5
- package/src/client/use-row-window.ts +55 -0
package/src/client/row-window.ts
CHANGED
|
@@ -72,6 +72,29 @@ export interface RowWindow {
|
|
|
72
72
|
readonly padBottom: number
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** Whether two windows paint the same rows AND reserve the same total height.
|
|
76
|
+
* Comparing only start/end leaves a previous file's bottom spacer behind when
|
|
77
|
+
* two long files happen to expose the same viewport-sized row range. */
|
|
78
|
+
export function sameRowWindow(a: RowWindow, b: RowWindow): boolean {
|
|
79
|
+
return a.start === b.start && a.end === b.end
|
|
80
|
+
&& a.padTop === b.padTop && a.padBottom === b.padBottom
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A rendered window is reusable only for the exact mounted diff. Equal-length
|
|
84
|
+
* files can be at different scroll positions, so row count alone is not an identity. */
|
|
85
|
+
export interface HeldRowWindow {
|
|
86
|
+
readonly mountKey: string
|
|
87
|
+
readonly rowCount: number
|
|
88
|
+
readonly win: RowWindow
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Return the held window when it belongs to this diff, otherwise a fresh top window. */
|
|
92
|
+
export function rowWindowForMount(held: HeldRowWindow, rowCount: number, mountKey: string): RowWindow {
|
|
93
|
+
return held.rowCount === rowCount && held.mountKey === mountKey
|
|
94
|
+
? held.win
|
|
95
|
+
: rowWindow(0, 0, rowCount)
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
/**
|
|
76
99
|
* @param value - a number from the DOM, which can be NaN or negative.
|
|
77
100
|
* @param fallback - used when it is neither finite nor usable.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many matches the find panel found, and which one you are on.
|
|
3
|
+
*
|
|
4
|
+
* The panel ships without a count, which leaves the one question a reader
|
|
5
|
+
* actually has unanswered: a query that highlights nothing on screen might
|
|
6
|
+
* have no matches at all, or two hundred of them below the fold, and the panel
|
|
7
|
+
* looks identical either way.
|
|
8
|
+
*
|
|
9
|
+
* Counting is proportional to the DOCUMENT, and this drawer does not put work
|
|
10
|
+
* proportional to the document on the keystroke path. Two things keep that
|
|
11
|
+
* true, and both live here rather than in the wiring:
|
|
12
|
+
*
|
|
13
|
+
* - the walk stops at {@link MATCH_CAP}. A cap that turns a feature off would
|
|
14
|
+
* be no fix, so this one bounds the WORK and keeps the feature: past the cap
|
|
15
|
+
* the total reads `5000+`, which answers "is my query too broad" as well as
|
|
16
|
+
* an exact number would. It bounds memory the same way — a single-letter
|
|
17
|
+
* search over 20,000 lines of real TypeScript finds 71,515 matches, and an
|
|
18
|
+
* array of those is half a megabyte kept alive for a number nobody reads.
|
|
19
|
+
* - the offsets are KEPT, so moving between matches is a binary search rather
|
|
20
|
+
* than a second walk. Measured on this repo's own client sources: a full
|
|
21
|
+
* count costs 3-5ms at 300 lines, 9-11ms at 2,000, 12ms at 4,000, and 60ms
|
|
22
|
+
* at 20,000 (`SIDE_LINE_CAP`, the ceiling the pane will load). Recounting on
|
|
23
|
+
* every Enter would put that 60ms on the navigation path; recounting when
|
|
24
|
+
* the typing stops puts it nowhere the reader can feel it.
|
|
25
|
+
*
|
|
26
|
+
* The index belongs to one query over one document, and the caller throws it
|
|
27
|
+
* away when either changes — see `SearchCount` in `CodeEditor.tsx`.
|
|
28
|
+
*
|
|
29
|
+
* @module @young1lin/dsh-ui-gitworkbench/client/search-count
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How many match positions are kept.
|
|
34
|
+
*
|
|
35
|
+
* 5,000 is past any count a reader distinguishes from "lots" and well inside
|
|
36
|
+
* what the pane can hold: at 8 bytes an offset it is 40KB, against the half a
|
|
37
|
+
* megabyte an uncapped single-letter search over the ceiling would take.
|
|
38
|
+
*/
|
|
39
|
+
export const MATCH_CAP = 5000
|
|
40
|
+
|
|
41
|
+
/** Where every match starts, and whether the walk stopped early. */
|
|
42
|
+
export interface MatchIndex {
|
|
43
|
+
/** Ascending start offsets, at most {@link MATCH_CAP} of them. */
|
|
44
|
+
readonly offsets: readonly number[]
|
|
45
|
+
/** There were more matches than were kept; the total reads `N+`. */
|
|
46
|
+
readonly capped: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** No query, or a query with nothing to find. */
|
|
50
|
+
export const EMPTY_INDEX: MatchIndex = { offsets: [], capped: false }
|
|
51
|
+
|
|
52
|
+
/** One match start, however it is handed over. */
|
|
53
|
+
interface Match { readonly from: number }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Anything that yields matches in order.
|
|
57
|
+
*
|
|
58
|
+
* Both halves of the union are here for a reason: `SearchQuery.getCursor` is
|
|
59
|
+
* DECLARED as an iterator and is also iterable at runtime, while a test wants
|
|
60
|
+
* to hand over a plain array. Accepting either keeps the rule readable without
|
|
61
|
+
* a document, a view or a DOM behind it.
|
|
62
|
+
*/
|
|
63
|
+
export type MatchWalk = Iterable<Match> | Iterator<Match>
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Walk matches into an index, stopping at `cap`.
|
|
67
|
+
*
|
|
68
|
+
* The walk is driven by hand rather than with `for…of`, because the union
|
|
69
|
+
* above may arrive already unwrapped. A cursor reuses ONE object across
|
|
70
|
+
* iterations, which is safe here only because nothing but the number is kept.
|
|
71
|
+
*/
|
|
72
|
+
export function indexMatches(matches: MatchWalk, cap: number = MATCH_CAP): MatchIndex {
|
|
73
|
+
const step: Iterator<Match> = Symbol.iterator in matches
|
|
74
|
+
? matches[Symbol.iterator]()
|
|
75
|
+
: matches
|
|
76
|
+
const offsets: number[] = []
|
|
77
|
+
for (;;) {
|
|
78
|
+
if (offsets.length >= cap) {
|
|
79
|
+
// Ask once more: `capped` has to mean "there are more", not "there might
|
|
80
|
+
// have been", or a total that lands exactly on the cap reads as `5000+`.
|
|
81
|
+
return { offsets, capped: step.next().done !== true }
|
|
82
|
+
}
|
|
83
|
+
const next = step.next()
|
|
84
|
+
if (next.done === true) return { offsets, capped: false }
|
|
85
|
+
offsets.push(next.value.from)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Which match the position `from` is on or before, 1-based; 0 when there are
|
|
91
|
+
* none.
|
|
92
|
+
*
|
|
93
|
+
* The first match at or after the caret, because that is the one the panel
|
|
94
|
+
* will land on — which makes the answer right in both of the states the reader
|
|
95
|
+
* sees it in. Straight after typing the caret has not moved and the count
|
|
96
|
+
* should already read `1/128`, not `0/128`; after Enter the selection sits
|
|
97
|
+
* exactly on a match and the lower bound IS that match. Past the last one the
|
|
98
|
+
* panel wraps, so the answer wraps with it.
|
|
99
|
+
*/
|
|
100
|
+
export function ordinalAt(index: MatchIndex, from: number): number {
|
|
101
|
+
const { offsets } = index
|
|
102
|
+
if (offsets.length === 0) return 0
|
|
103
|
+
let low = 0
|
|
104
|
+
let high = offsets.length
|
|
105
|
+
while (low < high) {
|
|
106
|
+
const mid = (low + high) >> 1
|
|
107
|
+
if (offsets[mid]! < from) low = mid + 1
|
|
108
|
+
else high = mid
|
|
109
|
+
}
|
|
110
|
+
return low === offsets.length ? 1 : low + 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The label: `3/128`, `3/5000+` when the walk stopped early, `0/0` for a query
|
|
115
|
+
* that finds nothing.
|
|
116
|
+
*
|
|
117
|
+
* No words, in a panel whose own labels are the library's English and are not
|
|
118
|
+
* translated either — a pair of numbers says it in every language the drawer
|
|
119
|
+
* ships (see `locales.ts` for the strings that do need both).
|
|
120
|
+
*/
|
|
121
|
+
export function formatCount(index: MatchIndex, ordinal: number): string {
|
|
122
|
+
const total = index.offsets.length
|
|
123
|
+
if (total === 0) return '0/0'
|
|
124
|
+
return `${ordinal}/${total}${index.capped ? '+' : ''}`
|
|
125
|
+
}
|
package/src/client/side-rows.ts
CHANGED
|
@@ -174,6 +174,17 @@ export function blockLines(rows: readonly SideRow[], block: number): readonly nu
|
|
|
174
174
|
return [...indices].sort((a, b) => a - b)
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/** Every changed hunk-line index, for a whole-layer Stage/Unstage action. */
|
|
178
|
+
export function allBlockLines(rows: readonly SideRow[]): readonly number[] {
|
|
179
|
+
const indices = new Set<number>()
|
|
180
|
+
for (const row of rows) {
|
|
181
|
+
if (row.block < 0) continue
|
|
182
|
+
if (row.leftIndex !== -1) indices.add(row.leftIndex)
|
|
183
|
+
if (row.rightIndex !== -1) indices.add(row.rightIndex)
|
|
184
|
+
}
|
|
185
|
+
return [...indices].sort((a, b) => a - b)
|
|
186
|
+
}
|
|
187
|
+
|
|
177
188
|
/**
|
|
178
189
|
* How many change blocks the rows hold.
|
|
179
190
|
*
|
|
@@ -186,6 +197,49 @@ export function blockCount(rows: readonly SideRow[]): number {
|
|
|
186
197
|
return max + 1
|
|
187
198
|
}
|
|
188
199
|
|
|
200
|
+
export type BlockEdge = 'single' | 'first' | 'middle' | 'last'
|
|
201
|
+
|
|
202
|
+
/** Where one present side-cell sits on its block's visible perimeter. Absent
|
|
203
|
+
* cells return null: an addition-only block must not draw a blue cage through
|
|
204
|
+
* the empty left pane, and a deletion-only block does the symmetric thing. */
|
|
205
|
+
export function blockEdge(rows: readonly SideRow[], index: number, side: 'left' | 'right'): BlockEdge | null {
|
|
206
|
+
const row = rows[index]
|
|
207
|
+
if (row === undefined || row.block < 0 || row[side] === null) return null
|
|
208
|
+
const before = index > 0 && rows[index - 1]!.block === row.block && rows[index - 1]![side] !== null
|
|
209
|
+
const after = index + 1 < rows.length && rows[index + 1]!.block === row.block && rows[index + 1]![side] !== null
|
|
210
|
+
if (!before && !after) return 'single'
|
|
211
|
+
if (!before) return 'first'
|
|
212
|
+
return after ? 'middle' : 'last'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The selected Git block exposed by the fixed pane toolbar. Visibility is
|
|
216
|
+
* independent of buffer dirtiness: the component keeps actions mounted and
|
|
217
|
+
* disables unsafe ones. A refreshed diff may carry fewer blocks, so an invalid
|
|
218
|
+
* selection safely falls back to the first one. */
|
|
219
|
+
export function currentActionBlock(totalBlocks: number, actionsVisible: boolean, selectedBlock: number): number | null {
|
|
220
|
+
if (!actionsVisible || totalBlocks <= 0) return null
|
|
221
|
+
return Number.isInteger(selectedBlock) && selectedBlock >= 0 && selectedBlock < totalBlocks ? selectedBlock : 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Git hunk operations wait for a clean editor buffer and for the prior Git
|
|
225
|
+
* operation to settle. This rule disables controls; it never hides them. */
|
|
226
|
+
export function blockActionsDisabled(dirty: boolean, pendingBlock: number | null): boolean {
|
|
227
|
+
return dirty || pendingBlock !== null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Whether the first rendered row is already inside a change block.
|
|
232
|
+
*
|
|
233
|
+
* The block action bar normally floats one row above its first cell. When the
|
|
234
|
+
* file itself starts with a change there is no preceding row, so the columns
|
|
235
|
+
* must reserve that bar's clearance inside their own clipping boxes. This is
|
|
236
|
+
* derived from the full row model rather than hover state, avoiding a layout
|
|
237
|
+
* jump when the pointer enters line 1.
|
|
238
|
+
*/
|
|
239
|
+
export function needsFirstBlockClearance(rows: readonly SideRow[]): boolean {
|
|
240
|
+
return rows.length > 0 && rows[0]!.block >= 0
|
|
241
|
+
}
|
|
242
|
+
|
|
189
243
|
/**
|
|
190
244
|
* Whether a block is the file's ENTIRE content: the one block of a diff with
|
|
191
245
|
* no context row at all.
|
|
@@ -256,3 +310,15 @@ export function blockTally(rows: readonly SideRow[], block: number): { readonly
|
|
|
256
310
|
}
|
|
257
311
|
return { added, deleted }
|
|
258
312
|
}
|
|
313
|
+
|
|
314
|
+
/** Whole-layer tallies, paired with {@link allBlockLines}. */
|
|
315
|
+
export function allBlockTally(rows: readonly SideRow[]): { readonly added: number; readonly deleted: number } {
|
|
316
|
+
let added = 0
|
|
317
|
+
let deleted = 0
|
|
318
|
+
for (const row of rows) {
|
|
319
|
+
if (row.block < 0) continue
|
|
320
|
+
if (row.left !== null) deleted += 1
|
|
321
|
+
if (row.right !== null) added += 1
|
|
322
|
+
}
|
|
323
|
+
return { added, deleted }
|
|
324
|
+
}
|