@tooee/renderers 0.1.4 → 0.1.6

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.
@@ -0,0 +1,607 @@
1
+ import {
2
+ type RenderContext,
3
+ type LineInfo,
4
+ type LineInfoProvider,
5
+ type Renderable,
6
+ ScrollBoxRenderable,
7
+ type ScrollBoxOptions,
8
+ RGBA,
9
+ } from "@opentui/core"
10
+ import type { OptimizedBuffer } from "@opentui/core"
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface RowDocumentPalette {
17
+ gutterFg?: string
18
+ gutterBg?: string
19
+ cursorBg?: string
20
+ selectionBg?: string
21
+ matchBg?: string
22
+ currentMatchBg?: string
23
+ toggledBg?: string
24
+ cursorSign?: string
25
+ cursorSignFg?: string
26
+ matchSign?: string
27
+ matchSignFg?: string
28
+ currentMatchSignFg?: string
29
+ }
30
+
31
+ export interface RowDocumentDecorations {
32
+ cursorRow?: number
33
+ selection?: { start: number; end: number } | null
34
+ matchingRows?: Set<number>
35
+ currentMatchRow?: number
36
+ toggledRows?: Set<number>
37
+ signs?: Map<number, { text: string; fg?: string }>
38
+ }
39
+
40
+ export interface RowDocumentOptions extends ScrollBoxOptions {
41
+ mode?: "auto" | "multi" | "provider"
42
+
43
+ // Gutter
44
+ showGutter?: boolean
45
+ showLineNumbers?: boolean
46
+ lineNumberStart?: number
47
+ signColumnWidth?: number
48
+ gutterPaddingRight?: number
49
+
50
+ // Multi-child: skip N leading children that aren't logical rows
51
+ rowChildOffset?: number
52
+
53
+ // Colors
54
+ palette?: RowDocumentPalette
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Provider detection
59
+ // ---------------------------------------------------------------------------
60
+
61
+ function isRowContentProvider(x: unknown): x is LineInfoProvider {
62
+ return (
63
+ !!x &&
64
+ typeof x === "object" &&
65
+ "lineInfo" in x &&
66
+ "lineCount" in x &&
67
+ "virtualLineCount" in x
68
+ )
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Color cache — avoids re-parsing hex strings every frame
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const colorCache = new Map<string, RGBA>()
76
+
77
+ function cachedColor(hex: string): RGBA {
78
+ let c = colorCache.get(hex)
79
+ if (!c) {
80
+ c = RGBA.fromHex(hex)
81
+ colorCache.set(hex, c)
82
+ }
83
+ return c
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // RowDocumentRenderable
88
+ // ---------------------------------------------------------------------------
89
+
90
+ export class RowDocumentRenderable extends ScrollBoxRenderable {
91
+ // -- Options --
92
+ private _mode: "auto" | "multi" | "provider"
93
+ private _showGutter: boolean
94
+ private _showLineNumbers: boolean
95
+ private _lineNumberStart: number
96
+ private _signColumnWidth: number
97
+ private _gutterPaddingRight: number
98
+ private _rowChildOffset: number
99
+ private _palette: Required<RowDocumentPalette>
100
+
101
+ // -- Decorations --
102
+ private _deco: RowDocumentDecorations = {}
103
+
104
+ // -- Geometry arrays --
105
+ private _rowVirtualStarts: number[] = []
106
+ private _rowVirtualHeights: number[] = []
107
+ private _virtualRowToRow: number[] = []
108
+ private _virtualRowWraps: number[] = []
109
+ private _rowCount = 0
110
+ private _lastGeometryHash = ""
111
+
112
+ constructor(ctx: RenderContext, options: RowDocumentOptions) {
113
+ super(ctx, options)
114
+
115
+ this._mode = options.mode ?? "auto"
116
+ this._showGutter = options.showGutter ?? true
117
+ this._showLineNumbers = options.showLineNumbers ?? true
118
+ this._lineNumberStart = options.lineNumberStart ?? 1
119
+ this._signColumnWidth = options.signColumnWidth ?? 0
120
+ this._gutterPaddingRight = options.gutterPaddingRight ?? 1
121
+ this._rowChildOffset = options.rowChildOffset ?? 0
122
+
123
+ const p = options.palette ?? {}
124
+ this._palette = {
125
+ gutterFg: p.gutterFg ?? "#6e7681",
126
+ gutterBg: p.gutterBg ?? "#0d1117",
127
+ cursorBg: p.cursorBg ?? "#1c2128",
128
+ selectionBg: p.selectionBg ?? "#1f3a5f",
129
+ matchBg: p.matchBg ?? "#3b2e00",
130
+ currentMatchBg: p.currentMatchBg ?? "#5a4a00",
131
+ toggledBg: p.toggledBg ?? "#1c2128",
132
+ cursorSign: p.cursorSign ?? "▸",
133
+ cursorSignFg: p.cursorSignFg ?? "#58a6ff",
134
+ matchSign: p.matchSign ?? "●",
135
+ matchSignFg: p.matchSignFg ?? "#d29922",
136
+ currentMatchSignFg: p.currentMatchSignFg ?? "#f0c000",
137
+ }
138
+ }
139
+
140
+ // -----------------------------------------------------------------------
141
+ // Decorations
142
+ // -----------------------------------------------------------------------
143
+
144
+ setDecorations(decorations: RowDocumentDecorations): void {
145
+ this._deco = decorations
146
+ this.requestRender()
147
+ }
148
+
149
+ // -----------------------------------------------------------------------
150
+ // Gutter config setters
151
+ // -----------------------------------------------------------------------
152
+
153
+ set showGutter(value: boolean) {
154
+ if (this._showGutter !== value) {
155
+ this._showGutter = value
156
+ this._applyGutterPadding()
157
+ this.requestRender()
158
+ }
159
+ }
160
+
161
+ set showLineNumbers(value: boolean) {
162
+ if (this._showLineNumbers !== value) {
163
+ this._showLineNumbers = value
164
+ this._applyGutterPadding()
165
+ this.requestRender()
166
+ }
167
+ }
168
+
169
+ // -----------------------------------------------------------------------
170
+ // Geometry — public API
171
+ // -----------------------------------------------------------------------
172
+
173
+ get rowCount(): number {
174
+ return this._rowCount
175
+ }
176
+
177
+ get virtualRowCount(): number {
178
+ return this._virtualRowToRow.length
179
+ }
180
+
181
+ getRowMetrics(
182
+ row: number,
183
+ ): { row: number; virtualTop: number; virtualHeight: number } | null {
184
+ if (row < 0 || row >= this._rowCount) return null
185
+ return {
186
+ row,
187
+ virtualTop: this._rowVirtualStarts[row],
188
+ virtualHeight: this._rowVirtualHeights[row],
189
+ }
190
+ }
191
+
192
+ getRowAtVirtualY(y: number): number {
193
+ const clamped = Math.max(0, Math.min(y, this._virtualRowToRow.length - 1))
194
+ const row = this._virtualRowToRow[clamped]
195
+ if (row != null && row >= 0) return row
196
+ // Gap row — search backward for nearest valid row
197
+ for (let i = clamped - 1; i >= 0; i--) {
198
+ const r = this._virtualRowToRow[i]
199
+ if (r != null && r >= 0) return r
200
+ }
201
+ return 0
202
+ }
203
+
204
+ getVisibleRange(): {
205
+ virtualTop: number
206
+ virtualBottom: number
207
+ firstRow: number
208
+ lastRow: number
209
+ } {
210
+ const top = Math.floor(this.scrollTop)
211
+ const bottom = top + this.viewport.height
212
+ return {
213
+ virtualTop: top,
214
+ virtualBottom: bottom,
215
+ firstRow: this.getRowAtVirtualY(top),
216
+ lastRow: this.getRowAtVirtualY(bottom - 1),
217
+ }
218
+ }
219
+
220
+ // -----------------------------------------------------------------------
221
+ // scrollToRow
222
+ // -----------------------------------------------------------------------
223
+
224
+ scrollToRow(
225
+ row: number,
226
+ align: "nearest" | "start" | "center" | "end" = "nearest",
227
+ ): void {
228
+ const metrics = this.getRowMetrics(row)
229
+ if (!metrics) return
230
+
231
+ const vpHeight = this.viewport.height
232
+ const { virtualTop, virtualHeight } = metrics
233
+
234
+ let target = this.scrollTop
235
+
236
+ switch (align) {
237
+ case "start":
238
+ target = virtualTop
239
+ break
240
+ case "center":
241
+ target = virtualTop - (vpHeight - virtualHeight) / 2
242
+ break
243
+ case "end":
244
+ target = virtualTop + virtualHeight - vpHeight
245
+ break
246
+ case "nearest": {
247
+ if (virtualTop < this.scrollTop) {
248
+ target = virtualTop
249
+ } else if (virtualTop + virtualHeight > this.scrollTop + vpHeight) {
250
+ target = virtualTop + virtualHeight - vpHeight
251
+ }
252
+ break
253
+ }
254
+ }
255
+
256
+ // When scrolling to the first row, include header content (pre-offset children)
257
+ if (row === 0 && this._rowChildOffset > 0) {
258
+ target = Math.min(target, 0)
259
+ }
260
+
261
+ // When the next row is the last logical row (e.g. bottom border),
262
+ // extend the scroll to include it
263
+ if (row + 1 === this._rowCount - 1) {
264
+ const next = this.getRowMetrics(row + 1)
265
+ if (next) {
266
+ const bottomEdge = next.virtualTop + next.virtualHeight
267
+ if (bottomEdge > target + vpHeight) {
268
+ target = bottomEdge - vpHeight
269
+ }
270
+ }
271
+ }
272
+
273
+ this.scrollTop = Math.max(0, Math.round(target))
274
+ }
275
+
276
+ // -----------------------------------------------------------------------
277
+ // Rendering
278
+ // -----------------------------------------------------------------------
279
+
280
+ protected renderSelf(buffer: OptimizedBuffer): void {
281
+ // Let ScrollBox do its own rendering first
282
+ super.renderSelf(buffer)
283
+
284
+ // Recompute geometry every frame (cheap — just iterating children)
285
+ this._computeGeometry()
286
+
287
+ // Apply gutter padding to content area
288
+ this._applyGutterPadding()
289
+
290
+ // Paint decorations and gutter on top
291
+ this._paintDecorations(buffer)
292
+ if (this._showGutter) {
293
+ this._paintGutter(buffer)
294
+ }
295
+ }
296
+
297
+ // -----------------------------------------------------------------------
298
+ // Geometry computation
299
+ // -----------------------------------------------------------------------
300
+
301
+ private _detectMode(): "multi" | "provider" {
302
+ if (this._mode === "multi") return "multi"
303
+ if (this._mode === "provider") return "provider"
304
+
305
+ // Auto-detect: single child with LineInfoProvider → provider mode
306
+ const children = this.getChildren()
307
+ if (children.length === 1 && isRowContentProvider(children[0])) {
308
+ return "provider"
309
+ }
310
+ return "multi"
311
+ }
312
+
313
+ private _computeGeometry(): void {
314
+ const mode = this._detectMode()
315
+ if (mode === "provider") {
316
+ this._computeFromProvider()
317
+ } else {
318
+ this._computeFromChildren()
319
+ }
320
+ }
321
+
322
+ private _computeFromChildren(): void {
323
+ const children = this.getChildren()
324
+ const offset = this._rowChildOffset
325
+ const contentY = this.content.y
326
+ const totalHeight = this.scrollHeight
327
+
328
+ if (totalHeight === 0 || children.length <= offset) {
329
+ this._finishGeometry([], [], [], [], 0)
330
+ return
331
+ }
332
+
333
+ const rowCount = children.length - offset
334
+ const rowVirtualStarts: number[] = []
335
+ const rowVirtualHeights: number[] = []
336
+ const virtualRowToRow: number[] = new Array(totalHeight).fill(-1)
337
+ const virtualRowWraps: number[] = new Array(totalHeight).fill(0)
338
+
339
+ for (let i = offset; i < children.length; i++) {
340
+ const row = i - offset
341
+ const child = children[i]
342
+ const childStart = child.y - contentY
343
+ const h = Math.max(1, child.height)
344
+
345
+ rowVirtualStarts[row] = childStart
346
+ rowVirtualHeights[row] = h
347
+
348
+ for (let r = 0; r < h; r++) {
349
+ const vRow = childStart + r
350
+ if (vRow >= 0 && vRow < totalHeight) {
351
+ virtualRowToRow[vRow] = row
352
+ virtualRowWraps[vRow] = r === 0 ? 0 : 1
353
+ }
354
+ }
355
+ }
356
+
357
+ this._finishGeometry(
358
+ rowVirtualStarts,
359
+ rowVirtualHeights,
360
+ virtualRowToRow,
361
+ virtualRowWraps,
362
+ rowCount,
363
+ )
364
+ }
365
+
366
+ private _computeFromProvider(): void {
367
+ const children = this.getChildren()
368
+ if (children.length === 0) return
369
+
370
+ const provider = children[0] as unknown as LineInfoProvider
371
+ if (!isRowContentProvider(provider)) return
372
+
373
+ const info = provider.lineInfo
374
+ const { lineSources, lineWraps } = info
375
+
376
+ const rowVirtualStarts: number[] = []
377
+ const rowVirtualHeights: number[] = []
378
+ const virtualRowToRow: number[] = []
379
+ const virtualRowWraps: number[] = []
380
+
381
+ let currentRow = -1
382
+ let rowCount = 0
383
+
384
+ for (let v = 0; v < lineSources.length; v++) {
385
+ const row = lineSources[v]
386
+ virtualRowToRow[v] = row
387
+ virtualRowWraps[v] = lineWraps[v]
388
+
389
+ if (row !== currentRow) {
390
+ // New logical row
391
+ if (currentRow >= 0) {
392
+ rowVirtualHeights[currentRow] = v - rowVirtualStarts[currentRow]
393
+ }
394
+ rowVirtualStarts[row] = v
395
+ currentRow = row
396
+ rowCount = Math.max(rowCount, row + 1)
397
+ }
398
+ }
399
+
400
+ // Finalize last row
401
+ if (currentRow >= 0) {
402
+ rowVirtualHeights[currentRow] =
403
+ lineSources.length - rowVirtualStarts[currentRow]
404
+ }
405
+
406
+ this._finishGeometry(
407
+ rowVirtualStarts,
408
+ rowVirtualHeights,
409
+ virtualRowToRow,
410
+ virtualRowWraps,
411
+ rowCount,
412
+ )
413
+ }
414
+
415
+ private _finishGeometry(
416
+ rowVirtualStarts: number[],
417
+ rowVirtualHeights: number[],
418
+ virtualRowToRow: number[],
419
+ virtualRowWraps: number[],
420
+ rowCount: number,
421
+ ): void {
422
+ // Change detection via hash
423
+ const hash = rowVirtualStarts.join(",")
424
+ if (hash !== this._lastGeometryHash) {
425
+ this._rowVirtualStarts = rowVirtualStarts
426
+ this._rowVirtualHeights = rowVirtualHeights
427
+ this._virtualRowToRow = virtualRowToRow
428
+ this._virtualRowWraps = virtualRowWraps
429
+ this._rowCount = rowCount
430
+ this._lastGeometryHash = hash
431
+ this.emit("row-geometry-change")
432
+ }
433
+ }
434
+
435
+ // -----------------------------------------------------------------------
436
+ // Gutter width calculation and padding
437
+ // -----------------------------------------------------------------------
438
+
439
+ private _computeGutterWidth(): number {
440
+ if (!this._showGutter) return 0
441
+
442
+ let width = 0
443
+
444
+ if (this._showLineNumbers) {
445
+ const maxLineNum = this._lineNumberStart + this._rowCount - 1
446
+ width += Math.max(String(maxLineNum).length, 1)
447
+ }
448
+
449
+ width += this._signColumnWidth
450
+ width += this._gutterPaddingRight
451
+
452
+ return width
453
+ }
454
+
455
+ private _applyGutterPadding(): void {
456
+ const gutterWidth = this._computeGutterWidth()
457
+ this.content.paddingLeft = gutterWidth
458
+ }
459
+
460
+ // -----------------------------------------------------------------------
461
+ // Decoration painting (row backgrounds)
462
+ // -----------------------------------------------------------------------
463
+
464
+ private _paintDecorations(buffer: OptimizedBuffer): void {
465
+ const {
466
+ cursorRow,
467
+ selection,
468
+ matchingRows,
469
+ currentMatchRow,
470
+ toggledRows,
471
+ } = this._deco
472
+
473
+ const hasDecorations =
474
+ cursorRow != null ||
475
+ selection != null ||
476
+ (matchingRows && matchingRows.size > 0) ||
477
+ currentMatchRow != null ||
478
+ (toggledRows && toggledRows.size > 0)
479
+
480
+ if (!hasDecorations) return
481
+
482
+ const vpX = this.viewport.x
483
+ const vpY = this.viewport.y
484
+ const vpHeight = this.viewport.height
485
+ const vpWidth = this.viewport.width
486
+ const top = Math.floor(this.scrollTop)
487
+
488
+ for (let screenY = 0; screenY < vpHeight; screenY++) {
489
+ const vRow = top + screenY
490
+ if (vRow >= this._virtualRowToRow.length) break
491
+
492
+ const row = this._virtualRowToRow[vRow]
493
+ if (row < 0) continue // Skip gap rows (margins)
494
+ let bg: RGBA | null = null
495
+
496
+ // Priority order: currentMatch > match > selection > toggled > cursor
497
+ if (cursorRow != null && row === cursorRow) {
498
+ bg = cachedColor(this._palette.cursorBg)
499
+ }
500
+ if (toggledRows && toggledRows.has(row)) {
501
+ bg = cachedColor(this._palette.toggledBg)
502
+ }
503
+ if (selection && row >= selection.start && row <= selection.end) {
504
+ bg = cachedColor(this._palette.selectionBg)
505
+ }
506
+ if (matchingRows && matchingRows.has(row)) {
507
+ bg = cachedColor(this._palette.matchBg)
508
+ }
509
+ if (currentMatchRow != null && row === currentMatchRow) {
510
+ bg = cachedColor(this._palette.currentMatchBg)
511
+ }
512
+
513
+ if (bg) {
514
+ buffer.fillRect(vpX, vpY + screenY, vpWidth, 1, bg)
515
+ }
516
+ }
517
+ }
518
+
519
+ // -----------------------------------------------------------------------
520
+ // Gutter painting
521
+ // -----------------------------------------------------------------------
522
+
523
+ private _paintGutter(buffer: OptimizedBuffer): void {
524
+ const gutterWidth = this._computeGutterWidth()
525
+ if (gutterWidth === 0) return
526
+
527
+ const vpX = this.viewport.x
528
+ const vpY = this.viewport.y
529
+ const vpHeight = this.viewport.height
530
+ const top = Math.floor(this.scrollTop)
531
+
532
+ const gutterBg = cachedColor(this._palette.gutterBg)
533
+ const gutterFg = cachedColor(this._palette.gutterFg)
534
+
535
+ // Fill gutter background
536
+ buffer.fillRect(vpX, vpY, gutterWidth, vpHeight, gutterBg)
537
+
538
+ const lineNumWidth = this._showLineNumbers
539
+ ? Math.max(String(this._lineNumberStart + this._rowCount - 1).length, 1)
540
+ : 0
541
+
542
+ for (let screenY = 0; screenY < vpHeight; screenY++) {
543
+ const vRow = top + screenY
544
+ if (vRow >= this._virtualRowToRow.length) break
545
+
546
+ const row = this._virtualRowToRow[vRow]
547
+ if (row < 0) continue // Skip gap rows (margins)
548
+ const isFirstLine = this._virtualRowWraps[vRow] === 0
549
+
550
+ if (!isFirstLine) continue
551
+
552
+ const drawX = vpX
553
+ let col = 0
554
+
555
+ // Line number
556
+ if (this._showLineNumbers) {
557
+ const lineNum = String(this._lineNumberStart + row)
558
+ const padded = lineNum.padStart(lineNumWidth, " ")
559
+ buffer.drawText(padded, drawX + col, vpY + screenY, gutterFg, gutterBg)
560
+ col += lineNumWidth
561
+ }
562
+
563
+ // Sign column
564
+ if (this._signColumnWidth > 0) {
565
+ const { cursorRow, matchingRows, currentMatchRow } = this._deco
566
+ const customSign = this._deco.signs?.get(row)
567
+
568
+ if (customSign) {
569
+ const signFg = customSign.fg
570
+ ? cachedColor(customSign.fg)
571
+ : gutterFg
572
+ buffer.drawText(
573
+ customSign.text.slice(0, this._signColumnWidth),
574
+ drawX + col,
575
+ vpY + screenY,
576
+ signFg,
577
+ gutterBg,
578
+ )
579
+ } else if (cursorRow != null && row === cursorRow) {
580
+ buffer.drawText(
581
+ this._palette.cursorSign,
582
+ drawX + col,
583
+ vpY + screenY,
584
+ cachedColor(this._palette.cursorSignFg),
585
+ gutterBg,
586
+ )
587
+ } else if (currentMatchRow != null && row === currentMatchRow) {
588
+ buffer.drawText(
589
+ this._palette.matchSign,
590
+ drawX + col,
591
+ vpY + screenY,
592
+ cachedColor(this._palette.currentMatchSignFg),
593
+ gutterBg,
594
+ )
595
+ } else if (matchingRows && matchingRows.has(row)) {
596
+ buffer.drawText(
597
+ this._palette.matchSign,
598
+ drawX + col,
599
+ vpY + screenY,
600
+ cachedColor(this._palette.matchSignFg),
601
+ gutterBg,
602
+ )
603
+ }
604
+ }
605
+ }
606
+ }
607
+ }
package/src/Table.tsx CHANGED
@@ -1,6 +1,9 @@
1
1
  import { useTerminalDimensions } from "@opentui/react"
2
2
  import { useTheme } from "@tooee/themes"
3
+ import { useEffect, useRef, type RefObject } from "react"
3
4
  import type { ColumnDef, TableRow } from "./table-types.js"
5
+ import type { RowDocumentRenderable, RowDocumentPalette, RowDocumentDecorations } from "./RowDocumentRenderable.js"
6
+ import "./row-document.js"
4
7
 
5
8
  export interface TableProps {
6
9
  columns: ColumnDef[]
@@ -19,6 +22,7 @@ export interface TableProps {
19
22
  matchingRows?: Set<number>
20
23
  currentMatchRow?: number
21
24
  toggledRows?: Set<number>
25
+ docRef?: RefObject<RowDocumentRenderable | null>
22
26
  }
23
27
 
24
28
  const PADDING = 1
@@ -171,6 +175,7 @@ export function Table({
171
175
  matchingRows,
172
176
  currentMatchRow,
173
177
  toggledRows,
178
+ docRef,
174
179
  }: TableProps) {
175
180
  const { theme } = useTheme()
176
181
  const { width: terminalWidth } = useTerminalDimensions()
@@ -203,50 +208,47 @@ export function Table({
203
208
  const headerLine = buildDataLine(headers, colWidths, alignments)
204
209
  const dataLines = normalizedRows.map((row) => buildDataLine(row, colWidths, alignments))
205
210
 
206
- const getRowStyle = (rowIndex: number): { fg?: string; bg?: string } => {
207
- const isCursor = cursor === rowIndex
208
- const isSelected =
209
- selectionStart != null &&
210
- selectionEnd != null &&
211
- rowIndex >= selectionStart &&
212
- rowIndex <= selectionEnd
213
- const isMatch = matchingRows?.has(rowIndex)
214
- const isCurrentMatch = currentMatchRow === rowIndex
215
- const isToggled = toggledRows?.has(rowIndex)
211
+ const internalRef = useRef<RowDocumentRenderable>(null)
212
+ const effectiveRef = docRef ?? internalRef
216
213
 
217
- let bg: string | undefined
218
- let fg: string | undefined = theme.text
219
-
220
- // Determine background: selection < cursor (cursor overwrites)
221
- if (isSelected) {
222
- bg = theme.selection
223
- }
224
- if (isToggled && !isCursor && !isSelected) {
225
- bg = theme.backgroundPanel
226
- }
227
- if (isCursor) {
228
- bg = theme.cursorLine
229
- }
214
+ const palette: RowDocumentPalette = {
215
+ cursorBg: theme.cursorLine,
216
+ selectionBg: theme.selection,
217
+ matchBg: theme.warning,
218
+ currentMatchBg: theme.primary,
219
+ toggledBg: theme.backgroundPanel,
220
+ }
230
221
 
231
- // Highlight match indicator on matching rows
232
- if (isMatch && !isCursor) {
233
- fg = isCurrentMatch ? theme.primary : theme.warning
222
+ useEffect(() => {
223
+ const decorations: RowDocumentDecorations = {
224
+ cursorRow: cursor,
225
+ selection: selectionStart != null && selectionEnd != null
226
+ ? { start: selectionStart, end: selectionEnd }
227
+ : null,
228
+ matchingRows: matchingRows,
229
+ currentMatchRow: currentMatchRow,
230
+ toggledRows: toggledRows,
234
231
  }
235
-
236
- return { fg, bg }
237
- }
232
+ effectiveRef.current?.setDecorations(decorations)
233
+ }, [cursor, selectionStart, selectionEnd, matchingRows, currentMatchRow, toggledRows])
238
234
 
239
235
  return (
240
- <box style={{ flexDirection: "column", marginLeft: 1, marginRight: 1, marginBottom: 1 }}>
236
+ <row-document
237
+ ref={effectiveRef}
238
+ mode="multi"
239
+ rowChildOffset={3}
240
+ showGutter={false}
241
+ palette={palette}
242
+ style={{ flexGrow: 1, marginLeft: 1, marginRight: 1, marginBottom: 1 }}
243
+ >
241
244
  <text content={topBorder} fg={theme.border} />
242
245
  <text content={headerLine} fg={theme.primary} />
243
246
  <text content={headerSep} fg={theme.border} />
244
- {dataLines.map((line, i) => {
245
- const style = getRowStyle(i)
246
- return <text key={i} content={line} fg={style.fg} bg={style.bg} />
247
- })}
247
+ {dataLines.map((line, i) => (
248
+ <text key={i} content={line} fg={theme.text} />
249
+ ))}
248
250
  <text content={bottomBorder} fg={theme.border} />
249
- </box>
251
+ </row-document>
250
252
  )
251
253
  }
252
254