@tooee/renderers 0.1.11 → 0.1.12

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.
Files changed (43) hide show
  1. package/dist/CodeView.d.ts +3 -7
  2. package/dist/CodeView.d.ts.map +1 -1
  3. package/dist/CodeView.js +5 -31
  4. package/dist/CodeView.js.map +1 -1
  5. package/dist/CommandPalette.d.ts.map +1 -1
  6. package/dist/CommandPalette.js +1 -23
  7. package/dist/CommandPalette.js.map +1 -1
  8. package/dist/DecorationLayer.d.ts +14 -0
  9. package/dist/DecorationLayer.d.ts.map +1 -0
  10. package/dist/DecorationLayer.js +2 -0
  11. package/dist/DecorationLayer.js.map +1 -0
  12. package/dist/MarkdownView.d.ts +3 -9
  13. package/dist/MarkdownView.d.ts.map +1 -1
  14. package/dist/MarkdownView.js +11 -32
  15. package/dist/MarkdownView.js.map +1 -1
  16. package/dist/RowDocumentRenderable.d.ts +14 -26
  17. package/dist/RowDocumentRenderable.d.ts.map +1 -1
  18. package/dist/RowDocumentRenderable.js +74 -78
  19. package/dist/RowDocumentRenderable.js.map +1 -1
  20. package/dist/Table.d.ts +5 -9
  21. package/dist/Table.d.ts.map +1 -1
  22. package/dist/Table.js +17 -46
  23. package/dist/Table.js.map +1 -1
  24. package/dist/index.d.ts +5 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/parsers.d.ts.map +1 -1
  29. package/dist/parsers.js.map +1 -1
  30. package/dist/useGutterPalette.d.ts +3 -0
  31. package/dist/useGutterPalette.d.ts.map +1 -0
  32. package/dist/useGutterPalette.js +10 -0
  33. package/dist/useGutterPalette.js.map +1 -0
  34. package/package.json +18 -16
  35. package/src/CodeView.tsx +11 -54
  36. package/src/CommandPalette.tsx +1 -25
  37. package/src/DecorationLayer.ts +11 -0
  38. package/src/MarkdownView.tsx +19 -51
  39. package/src/RowDocumentRenderable.ts +91 -135
  40. package/src/Table.tsx +35 -71
  41. package/src/index.ts +4 -7
  42. package/src/parsers.ts +4 -1
  43. package/src/useGutterPalette.ts +15 -0
package/package.json CHANGED
@@ -1,7 +1,18 @@
1
1
  {
2
2
  "name": "@tooee/renderers",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Content renderers (markdown, code, image, table) for Tooee",
5
+ "keywords": [
6
+ "cli",
7
+ "image",
8
+ "markdown",
9
+ "opentui",
10
+ "syntax-highlighting",
11
+ "terminal",
12
+ "tui"
13
+ ],
14
+ "homepage": "https://github.com/gingerhendrix/tooee",
15
+ "bugs": "https://github.com/gingerhendrix/tooee/issues",
5
16
  "license": "MIT",
6
17
  "author": "Gareth Andrew",
7
18
  "repository": {
@@ -9,16 +20,9 @@
9
20
  "url": "https://github.com/gingerhendrix/tooee.git",
10
21
  "directory": "packages/renderers"
11
22
  },
12
- "homepage": "https://github.com/gingerhendrix/tooee",
13
- "bugs": "https://github.com/gingerhendrix/tooee/issues",
14
- "keywords": [
15
- "tui",
16
- "terminal",
17
- "cli",
18
- "opentui",
19
- "markdown",
20
- "syntax-highlighting",
21
- "image"
23
+ "files": [
24
+ "dist",
25
+ "src"
22
26
  ],
23
27
  "type": "module",
24
28
  "exports": {
@@ -29,15 +33,13 @@
29
33
  }
30
34
  }
31
35
  },
32
- "files": [
33
- "dist",
34
- "src"
35
- ],
36
36
  "scripts": {
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@tooee/themes": "0.1.11",
40
+ "@tooee/fuzzy": "0.1.12",
41
+ "@tooee/marks": "0.1.12",
42
+ "@tooee/themes": "0.1.12",
41
43
  "marked": "^17.0.4"
42
44
  },
43
45
  "devDependencies": {
package/src/CodeView.tsx CHANGED
@@ -1,75 +1,32 @@
1
- import { useEffect, useRef, type RefObject } from "react"
1
+ import { type RefObject } from "react"
2
2
  import { useTheme } from "@tooee/themes"
3
- import type { RowDocumentRenderable, RowDocumentPalette, RowDocumentDecorations } from "./RowDocumentRenderable.js"
3
+ import type { MarkState } from "@tooee/marks"
4
+ import type { RowDocumentRenderable } from "./RowDocumentRenderable.js"
5
+ import { useGutterPalette } from "./useGutterPalette.js"
4
6
  import "./row-document.js"
5
7
 
6
8
  interface CodeViewProps {
7
9
  content: string
8
10
  language?: string
9
11
  showLineNumbers?: boolean
10
- cursor?: number
11
- selectionStart?: number
12
- selectionEnd?: number
13
- matchingLines?: Set<number>
14
- currentMatchLine?: number
15
- toggledLines?: Set<number>
12
+ marks?: MarkState
16
13
  docRef?: RefObject<RowDocumentRenderable | null>
17
14
  }
18
15
 
19
- export function CodeView({
20
- content,
21
- language,
22
- showLineNumbers = true,
23
- cursor,
24
- selectionStart,
25
- selectionEnd,
26
- matchingLines,
27
- currentMatchLine,
28
- toggledLines,
29
- docRef,
30
- }: CodeViewProps) {
31
- const { syntax, theme } = useTheme()
32
- const internalRef = useRef<RowDocumentRenderable>(null)
33
- const effectiveRef = docRef ?? internalRef
34
-
35
- const palette: RowDocumentPalette = {
36
- gutterFg: theme.textMuted,
37
- gutterBg: theme.backgroundElement,
38
- cursorBg: theme.cursorLine,
39
- selectionBg: theme.selection,
40
- matchBg: theme.warning,
41
- currentMatchBg: theme.primary,
42
- toggledBg: theme.backgroundPanel,
43
- cursorSignFg: theme.primary,
44
- matchSignFg: theme.warning,
45
- currentMatchSignFg: theme.primary,
46
- }
47
-
48
- useEffect(() => {
49
- const decorations: RowDocumentDecorations = {
50
- cursorRow: cursor,
51
- selection: selectionStart != null && selectionEnd != null
52
- ? { start: selectionStart, end: selectionEnd }
53
- : null,
54
- matchingRows: matchingLines,
55
- currentMatchRow: currentMatchLine,
56
- toggledRows: toggledLines,
57
- }
58
- effectiveRef.current?.setDecorations(decorations)
59
- }, [cursor, selectionStart, selectionEnd, matchingLines, currentMatchLine, toggledLines])
60
-
61
- const codeElement = <code content={content} filetype={language} syntaxStyle={syntax} />
16
+ export function CodeView({ content, language, showLineNumbers = true, marks, docRef }: CodeViewProps) {
17
+ const { syntax } = useTheme()
18
+ const palette = useGutterPalette()
62
19
 
63
20
  return (
64
21
  <row-document
65
- ref={effectiveRef}
66
- key={theme.textMuted + theme.backgroundElement}
22
+ ref={docRef}
67
23
  showLineNumbers={showLineNumbers}
68
24
  palette={palette}
25
+ decorations={marks?.sets}
69
26
  signColumnWidth={1}
70
27
  style={{ flexGrow: 1 }}
71
28
  >
72
- {codeElement}
29
+ <code content={content} filetype={language} syntaxStyle={syntax} />
73
30
  </row-document>
74
31
  )
75
32
  }
@@ -1,6 +1,7 @@
1
1
  import { useState, useMemo, useCallback } from "react"
2
2
  import { useKeyboard } from "@opentui/react"
3
3
  import { useTheme } from "@tooee/themes"
4
+ import { fuzzyMatch } from "@tooee/fuzzy"
4
5
 
5
6
  export interface CommandPaletteEntry {
6
7
  id: string
@@ -16,31 +17,6 @@ interface CommandPaletteProps {
16
17
  onClose: () => void
17
18
  }
18
19
 
19
- function fuzzyMatch(query: string, text: string): number | null {
20
- const lowerQuery = query.toLowerCase()
21
- const lowerText = text.toLowerCase()
22
-
23
- let qi = 0
24
- let score = 0
25
- let lastMatchIndex = -2
26
-
27
- for (let ti = 0; ti < lowerText.length && qi < lowerQuery.length; ti++) {
28
- if (lowerText[ti] === lowerQuery[qi]) {
29
- // Start of string bonus
30
- if (ti === 0) score += 3
31
- // Word boundary bonus (after space, hyphen, dot, slash)
32
- else if (" -./".includes(lowerText[ti - 1]!)) score += 2
33
- // Consecutive bonus
34
- else if (ti === lastMatchIndex + 1) score += 1
35
-
36
- lastMatchIndex = ti
37
- qi++
38
- }
39
- }
40
-
41
- return qi === lowerQuery.length ? score : null
42
- }
43
-
44
20
  export function CommandPalette({ commands, onSelect, onClose }: CommandPaletteProps) {
45
21
  const { theme } = useTheme()
46
22
  const [filter, setFilter] = useState("")
@@ -0,0 +1,11 @@
1
+ export interface RowDecoration {
2
+ row: number
3
+ background?: string
4
+ gutterBackground?: string
5
+ sign?: { text: string; fg?: string }
6
+ }
7
+
8
+ export interface DecorationLayer {
9
+ readonly priority: number
10
+ forVisibleRows(from: number, to: number): Iterable<RowDecoration>
11
+ }
@@ -1,73 +1,37 @@
1
1
  import { marked, type Token, type Tokens } from "marked"
2
- import { useEffect, useMemo, useRef, type ReactNode, type RefObject } from "react"
2
+ import { useMemo, type ReactNode, type RefObject } from "react"
3
3
  import { useTheme, type ResolvedTheme } from "@tooee/themes"
4
4
  import { bold as boldChunk } from "@opentui/core"
5
5
  import type { SyntaxStyle, TextTableContent, TextTableCellContent } from "@opentui/core"
6
- import type { RowDocumentRenderable, RowDocumentPalette, RowDocumentDecorations } from "./RowDocumentRenderable.js"
6
+ import type { MarkState } from "@tooee/marks"
7
+ import type { RowDocumentRenderable } from "./RowDocumentRenderable.js"
8
+ import { useGutterPalette } from "./useGutterPalette.js"
7
9
  import "./row-document.js"
8
10
  import "./text-table.js"
9
11
 
10
12
  interface MarkdownViewProps {
11
13
  content: string
12
14
  showLineNumbers?: boolean
13
- activeBlock?: number
14
- selectedBlocks?: { start: number; end: number }
15
- matchingBlocks?: Set<number>
16
- currentMatchBlock?: number
17
- toggledBlocks?: Set<number>
15
+ marks?: MarkState
18
16
  docRef?: RefObject<RowDocumentRenderable | null>
19
17
  }
20
18
 
21
- export function MarkdownView({
22
- content,
23
- showLineNumbers = true,
24
- activeBlock,
25
- selectedBlocks,
26
- matchingBlocks,
27
- currentMatchBlock,
28
- toggledBlocks,
29
- docRef,
30
- }: MarkdownViewProps) {
19
+ export function MarkdownView({ content, showLineNumbers = true, marks, docRef }: MarkdownViewProps) {
31
20
  const { theme, syntax } = useTheme()
32
- const internalRef = useRef<RowDocumentRenderable>(null)
33
- const effectiveRef = docRef ?? internalRef
21
+ const palette = useGutterPalette()
34
22
  const tokens = marked.lexer(content)
35
23
  const blocks = tokens.filter((t) => t.type !== "space")
36
24
 
37
- const palette: RowDocumentPalette = {
38
- gutterFg: theme.textMuted,
39
- gutterBg: theme.backgroundElement,
40
- cursorBg: theme.cursorLine,
41
- selectionBg: theme.selection,
42
- matchBg: theme.warning,
43
- currentMatchBg: theme.primary,
44
- toggledBg: theme.backgroundPanel,
45
- cursorSignFg: theme.primary,
46
- matchSignFg: theme.warning,
47
- currentMatchSignFg: theme.primary,
48
- }
49
-
50
- useEffect(() => {
51
- const decorations: RowDocumentDecorations = {
52
- cursorRow: activeBlock,
53
- selection: selectedBlocks ? { start: selectedBlocks.start, end: selectedBlocks.end } : null,
54
- matchingRows: matchingBlocks,
55
- currentMatchRow: currentMatchBlock,
56
- toggledRows: toggledBlocks,
57
- }
58
- effectiveRef.current?.setDecorations(decorations)
59
- }, [activeBlock, selectedBlocks, matchingBlocks, currentMatchBlock, toggledBlocks])
60
-
61
25
  const blockElements = blocks.map((token, index) => (
62
26
  <TokenRenderer key={index} token={token} theme={theme} syntax={syntax} />
63
27
  ))
64
28
 
65
29
  return (
66
30
  <row-document
67
- ref={effectiveRef}
68
- key={theme.textMuted + theme.backgroundElement}
31
+ ref={docRef}
69
32
  showLineNumbers={showLineNumbers}
70
33
  palette={palette}
34
+ decorations={marks?.sets}
71
35
  signColumnWidth={1}
72
36
  style={{ flexGrow: 1 }}
73
37
  >
@@ -265,6 +229,9 @@ function ListItemRenderer({
265
229
  </text>
266
230
  )
267
231
  }
232
+ if (token.type === "list") {
233
+ return <ListRenderer key={idx} token={token as Tokens.List} theme={theme} />
234
+ }
268
235
  if ("text" in token && typeof token.text === "string") {
269
236
  return <text key={idx} style={{ fg: theme.markdownText }} content={token.text} />
270
237
  }
@@ -279,13 +246,14 @@ function MarkdownTableRenderer({ token }: { token: Tokens.Table }) {
279
246
  const { theme } = useTheme()
280
247
 
281
248
  const content: TextTableContent = useMemo(() => {
282
- const headerRow: TextTableCellContent[] = token.header.map(cell => [
283
- boldChunk(getPlainText(cell.tokens).trim())
249
+ const headerRow: TextTableCellContent[] = token.header.map((cell) => [
250
+ boldChunk(getPlainText(cell.tokens).trim()),
284
251
  ])
285
- const dataRows = token.rows.map(row =>
286
- row.map(cell => [
287
- { __isChunk: true as const, text: getPlainText(cell.tokens) }
288
- ] as TextTableCellContent)
252
+ const dataRows = token.rows.map((row) =>
253
+ row.map(
254
+ (cell) =>
255
+ [{ __isChunk: true as const, text: getPlainText(cell.tokens) }] as TextTableCellContent,
256
+ ),
289
257
  )
290
258
  return [headerRow, ...dataRows]
291
259
  }, [token])
@@ -8,6 +8,7 @@ import {
8
8
  RGBA,
9
9
  } from "@opentui/core"
10
10
  import type { OptimizedBuffer } from "@opentui/core"
11
+ import type { DecorationLayer, RowDecoration } from "./DecorationLayer.js"
11
12
 
12
13
  // ---------------------------------------------------------------------------
13
14
  // Types
@@ -16,25 +17,6 @@ import type { OptimizedBuffer } from "@opentui/core"
16
17
  export interface RowDocumentPalette {
17
18
  gutterFg?: string
18
19
  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
20
  }
39
21
 
40
22
  export interface RowDocumentOptions extends ScrollBoxOptions {
@@ -52,6 +34,7 @@ export interface RowDocumentOptions extends ScrollBoxOptions {
52
34
 
53
35
  // Colors
54
36
  palette?: RowDocumentPalette
37
+ decorations?: readonly DecorationLayer[]
55
38
  }
56
39
 
57
40
  // ---------------------------------------------------------------------------
@@ -60,11 +43,7 @@ export interface RowDocumentOptions extends ScrollBoxOptions {
60
43
 
61
44
  function isRowContentProvider(x: unknown): x is LineInfoProvider {
62
45
  return (
63
- !!x &&
64
- typeof x === "object" &&
65
- "lineInfo" in x &&
66
- "lineCount" in x &&
67
- "virtualLineCount" in x
46
+ !!x && typeof x === "object" && "lineInfo" in x && "lineCount" in x && "virtualLineCount" in x
68
47
  )
69
48
  }
70
49
 
@@ -83,6 +62,37 @@ function cachedColor(hex: string): RGBA {
83
62
  return c
84
63
  }
85
64
 
65
+ function normalizePalette(palette: RowDocumentPalette = {}): Required<RowDocumentPalette> {
66
+ return {
67
+ gutterFg: palette.gutterFg ?? "#6e7681",
68
+ gutterBg: palette.gutterBg ?? "#0d1117",
69
+ }
70
+ }
71
+
72
+ function normalizeDecorationLayers(
73
+ layers: readonly DecorationLayer[] | undefined,
74
+ ): readonly DecorationLayer[] {
75
+ if (!layers || layers.length === 0) return []
76
+ return [...layers].sort((a, b) => a.priority - b.priority)
77
+ }
78
+
79
+ export function computeRowDocumentGutterWidth(opts: {
80
+ showLineNumbers: boolean
81
+ rowCount: number
82
+ lineNumberStart?: number
83
+ signColumnWidth?: number
84
+ gutterPaddingRight?: number
85
+ }): number {
86
+ let width = 0
87
+ if (opts.showLineNumbers) {
88
+ const maxLineNum = (opts.lineNumberStart ?? 1) + opts.rowCount - 1
89
+ width += Math.max(String(maxLineNum).length, 1)
90
+ }
91
+ width += opts.signColumnWidth ?? 0
92
+ width += opts.gutterPaddingRight ?? 1
93
+ return width
94
+ }
95
+
86
96
  // ---------------------------------------------------------------------------
87
97
  // RowDocumentRenderable
88
98
  // ---------------------------------------------------------------------------
@@ -97,9 +107,9 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
97
107
  private _gutterPaddingRight: number
98
108
  private _rowChildOffset: number
99
109
  private _palette: Required<RowDocumentPalette>
100
-
101
- // -- Decorations --
102
- private _deco: RowDocumentDecorations = {}
110
+ private _layers: readonly DecorationLayer[] = []
111
+ private _layerGutterBgs = new Map<number, string>()
112
+ private _layerSigns = new Map<number, NonNullable<RowDecoration["sign"]>>()
103
113
 
104
114
  // -- Geometry arrays --
105
115
  private _rowVirtualStarts: number[] = []
@@ -120,29 +130,21 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
120
130
  this._gutterPaddingRight = options.gutterPaddingRight ?? 1
121
131
  this._rowChildOffset = options.rowChildOffset ?? 0
122
132
 
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
- }
133
+ this._palette = normalizePalette(options.palette)
134
+ this._layers = normalizeDecorationLayers(options.decorations)
138
135
  }
139
136
 
140
137
  // -----------------------------------------------------------------------
141
138
  // Decorations
142
139
  // -----------------------------------------------------------------------
143
140
 
144
- setDecorations(decorations: RowDocumentDecorations): void {
145
- this._deco = decorations
141
+ set decorations(layers: readonly DecorationLayer[] | undefined) {
142
+ this._layers = normalizeDecorationLayers(layers)
143
+ this.requestRender()
144
+ }
145
+
146
+ set palette(palette: RowDocumentPalette | undefined) {
147
+ this._palette = normalizePalette(palette)
146
148
  this.requestRender()
147
149
  }
148
150
 
@@ -178,9 +180,7 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
178
180
  return this._virtualRowToRow.length
179
181
  }
180
182
 
181
- getRowMetrics(
182
- row: number,
183
- ): { row: number; virtualTop: number; virtualHeight: number } | null {
183
+ getRowMetrics(row: number): { row: number; virtualTop: number; virtualHeight: number } | null {
184
184
  if (row < 0 || row >= this._rowCount) return null
185
185
  return {
186
186
  row,
@@ -221,10 +221,7 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
221
221
  // scrollToRow
222
222
  // -----------------------------------------------------------------------
223
223
 
224
- scrollToRow(
225
- row: number,
226
- align: "nearest" | "start" | "center" | "end" = "nearest",
227
- ): void {
224
+ scrollToRow(row: number, align: "nearest" | "start" | "center" | "end" = "nearest"): void {
228
225
  const metrics = this.getRowMetrics(row)
229
226
  if (!metrics) return
230
227
 
@@ -399,8 +396,7 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
399
396
 
400
397
  // Finalize last row
401
398
  if (currentRow >= 0) {
402
- rowVirtualHeights[currentRow] =
403
- lineSources.length - rowVirtualStarts[currentRow]
399
+ rowVirtualHeights[currentRow] = lineSources.length - rowVirtualStarts[currentRow]
404
400
  }
405
401
 
406
402
  this._finishGeometry(
@@ -439,17 +435,13 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
439
435
  private _computeGutterWidth(): number {
440
436
  if (!this._showGutter) return 0
441
437
 
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
438
+ return computeRowDocumentGutterWidth({
439
+ showLineNumbers: this._showLineNumbers,
440
+ rowCount: this._rowCount,
441
+ lineNumberStart: this._lineNumberStart,
442
+ signColumnWidth: this._signColumnWidth,
443
+ gutterPaddingRight: this._gutterPaddingRight,
444
+ })
453
445
  }
454
446
 
455
447
  private _applyGutterPadding(): void {
@@ -462,22 +454,10 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
462
454
  // -----------------------------------------------------------------------
463
455
 
464
456
  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
457
+ this._layerGutterBgs = new Map()
458
+ this._layerSigns = new Map()
459
+
460
+ if (this._layers.length === 0) return
481
461
 
482
462
  const vpX = this.viewport.x
483
463
  const vpY = this.viewport.y
@@ -485,35 +465,32 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
485
465
  const vpWidth = this.viewport.width
486
466
  const top = Math.floor(this.scrollTop)
487
467
 
468
+ const { firstRow, lastRow } = this.getVisibleRange()
469
+ const rowBgs = new Map<number, string>()
470
+ const rowGutterBgs = new Map<number, string>()
471
+ const rowSigns = new Map<number, NonNullable<RowDecoration["sign"]>>()
472
+
473
+ for (const layer of this._layers) {
474
+ for (const deco of layer.forVisibleRows(firstRow, lastRow)) {
475
+ if (deco.background) rowBgs.set(deco.row, deco.background)
476
+ if (deco.gutterBackground) rowGutterBgs.set(deco.row, deco.gutterBackground)
477
+ if (deco.sign) rowSigns.set(deco.row, deco.sign)
478
+ }
479
+ }
480
+
488
481
  for (let screenY = 0; screenY < vpHeight; screenY++) {
489
482
  const vRow = top + screenY
490
483
  if (vRow >= this._virtualRowToRow.length) break
491
484
 
492
485
  const row = this._virtualRowToRow[vRow]
493
486
  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
- }
487
+ const bg = rowBgs.get(row)
488
+ if (!bg) continue
489
+ buffer.fillRect(vpX, vpY + screenY, vpWidth, 1, cachedColor(bg))
516
490
  }
491
+
492
+ this._layerGutterBgs = rowGutterBgs
493
+ this._layerSigns = rowSigns
517
494
  }
518
495
 
519
496
  // -----------------------------------------------------------------------
@@ -552,53 +529,32 @@ export class RowDocumentRenderable extends ScrollBoxRenderable {
552
529
  const drawX = vpX
553
530
  let col = 0
554
531
 
532
+ const rowGutterHex = this._layerGutterBgs.get(row)
533
+ const effectiveGutterBg = rowGutterHex ? cachedColor(rowGutterHex) : gutterBg
534
+ if (rowGutterHex) {
535
+ buffer.fillRect(drawX, vpY + screenY, gutterWidth, 1, effectiveGutterBg)
536
+ }
537
+
555
538
  // Line number
556
539
  if (this._showLineNumbers) {
557
540
  const lineNum = String(this._lineNumberStart + row)
558
541
  const padded = lineNum.padStart(lineNumWidth, " ")
559
- buffer.drawText(padded, drawX + col, vpY + screenY, gutterFg, gutterBg)
542
+ buffer.drawText(padded, drawX + col, vpY + screenY, gutterFg, effectiveGutterBg)
560
543
  col += lineNumWidth
561
544
  }
562
545
 
563
546
  // Sign column
564
547
  if (this._signColumnWidth > 0) {
565
- const { cursorRow, matchingRows, currentMatchRow } = this._deco
566
- const customSign = this._deco.signs?.get(row)
548
+ const sign = this._layerSigns.get(row)
567
549
 
568
- if (customSign) {
569
- const signFg = customSign.fg
570
- ? cachedColor(customSign.fg)
571
- : gutterFg
550
+ if (sign) {
551
+ const signFg = sign.fg ? cachedColor(sign.fg) : gutterFg
572
552
  buffer.drawText(
573
- customSign.text.slice(0, this._signColumnWidth),
553
+ sign.text.slice(0, this._signColumnWidth),
574
554
  drawX + col,
575
555
  vpY + screenY,
576
556
  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,
557
+ effectiveGutterBg,
602
558
  )
603
559
  }
604
560
  }