@oh-my-pi/pi-tui 14.5.9 → 14.5.11
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/package.json +4 -3
- package/src/components/markdown.ts +55 -5
- package/src/utils.ts +9 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "14.5.
|
|
4
|
+
"version": "14.5.11",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "14.5.
|
|
41
|
-
"@oh-my-pi/pi-utils": "14.5.
|
|
40
|
+
"@oh-my-pi/pi-natives": "14.5.11",
|
|
41
|
+
"@oh-my-pi/pi-utils": "14.5.11",
|
|
42
|
+
"lru-cache": "11.3.5",
|
|
42
43
|
"marked": "^18.0.2"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
+
import { LRUCache } from "lru-cache/raw";
|
|
1
2
|
import { marked, type Token, type Tokens } from "marked";
|
|
2
3
|
import type { SymbolTheme } from "../symbols";
|
|
3
4
|
import { TERMINAL } from "../terminal-capabilities";
|
|
4
5
|
import type { Component } from "../tui";
|
|
5
6
|
import { applyBackgroundToLine, padding, replaceTabs, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
6
7
|
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Module-level LRU render cache
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Each session-tree navigation discards and recreates Markdown component
|
|
12
|
+
// instances, so the per-instance #cachedLines field is always cold on first
|
|
13
|
+
// render of a fresh component. This module-level cache survives across
|
|
14
|
+
// component lifetimes and eliminates redundant marked.lexer + highlightCode
|
|
15
|
+
// (Rust FFI) work for content/layout combinations already seen this session.
|
|
16
|
+
|
|
17
|
+
const RENDER_CACHE_MAX = 256; // sane cap: ~256 distinct message × width combos
|
|
18
|
+
const renderCache = new LRUCache<string, string[]>({ max: RENDER_CACHE_MAX });
|
|
19
|
+
|
|
20
|
+
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
|
21
|
+
export function clearRenderCache(): void {
|
|
22
|
+
renderCache.clear();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Stable numeric IDs for structural theme/style objects (no ID field on type).
|
|
26
|
+
// WeakMap so GC can collect orphaned themes/styles without a leak.
|
|
27
|
+
const _objectIds = new WeakMap<object, number>();
|
|
28
|
+
let _nextObjectId = 0;
|
|
29
|
+
function objectId(o: object): number {
|
|
30
|
+
let id = _objectIds.get(o);
|
|
31
|
+
if (id === undefined) {
|
|
32
|
+
id = _nextObjectId++;
|
|
33
|
+
_objectIds.set(o, id);
|
|
34
|
+
}
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
|
|
7
38
|
/**
|
|
8
39
|
* Default text styling for markdown content.
|
|
9
40
|
* Applied to all text unless overridden by markdown formatting.
|
|
@@ -116,7 +147,8 @@ export class Markdown implements Component {
|
|
|
116
147
|
}
|
|
117
148
|
|
|
118
149
|
render(width: number): string[] {
|
|
119
|
-
//
|
|
150
|
+
// L1: per-instance cache — fastest path for repeated renders of the same
|
|
151
|
+
// instance at the same width (e.g. resize debounce, repeated redraws).
|
|
120
152
|
if (this.#cachedLines && this.#cachedText === this.#text && this.#cachedWidth === width) {
|
|
121
153
|
return this.#cachedLines;
|
|
122
154
|
}
|
|
@@ -127,7 +159,7 @@ export class Markdown implements Component {
|
|
|
127
159
|
// Don't render anything if there's no actual text
|
|
128
160
|
if (!this.#text || this.#text.trim() === "") {
|
|
129
161
|
const result: string[] = [];
|
|
130
|
-
// Update cache
|
|
162
|
+
// Update per-instance cache
|
|
131
163
|
this.#cachedText = this.#text;
|
|
132
164
|
this.#cachedWidth = width;
|
|
133
165
|
this.#cachedLines = result;
|
|
@@ -137,6 +169,19 @@ export class Markdown implements Component {
|
|
|
137
169
|
// Replace tabs with 3 spaces for consistent rendering
|
|
138
170
|
const normalizedText = replaceTabs(this.#text);
|
|
139
171
|
|
|
172
|
+
// L2: module-level LRU — survives component disposal/recreation across
|
|
173
|
+
// session-tree navigations. Key encodes every dimension that affects the
|
|
174
|
+
// render output so different configurations never collide.
|
|
175
|
+
const cacheKey = `${normalizedText}\x00${width}\x00${this.#paddingX}\x00${this.#paddingY}\x00${this.#codeBlockIndent}\x00${objectId(this.#theme)}\x00${this.#defaultTextStyle ? objectId(this.#defaultTextStyle) : -1}`;
|
|
176
|
+
const cached = renderCache.get(cacheKey);
|
|
177
|
+
if (cached !== undefined) {
|
|
178
|
+
// Populate L1 so subsequent calls from this instance are O(1) map lookup.
|
|
179
|
+
this.#cachedText = this.#text;
|
|
180
|
+
this.#cachedWidth = width;
|
|
181
|
+
this.#cachedLines = cached;
|
|
182
|
+
return cached;
|
|
183
|
+
}
|
|
184
|
+
|
|
140
185
|
// Parse markdown to HTML-like tokens
|
|
141
186
|
const tokens = marked.lexer(normalizedText);
|
|
142
187
|
|
|
@@ -195,14 +240,19 @@ export class Markdown implements Component {
|
|
|
195
240
|
}
|
|
196
241
|
|
|
197
242
|
// Combine top padding, content, and bottom padding
|
|
198
|
-
const
|
|
243
|
+
const rawResult = [...emptyLines, ...contentLines, ...emptyLines];
|
|
244
|
+
const result = rawResult.length > 0 ? rawResult : [""];
|
|
199
245
|
|
|
200
|
-
// Update cache
|
|
246
|
+
// Update L1 per-instance cache
|
|
201
247
|
this.#cachedText = this.#text;
|
|
202
248
|
this.#cachedWidth = width;
|
|
203
249
|
this.#cachedLines = result;
|
|
204
250
|
|
|
205
|
-
|
|
251
|
+
// Update L2 module-level LRU so future instances with the same key skip
|
|
252
|
+
// the marked.lexer + highlightCode (Rust FFI) work entirely.
|
|
253
|
+
renderCache.set(cacheKey, result);
|
|
254
|
+
|
|
255
|
+
return result;
|
|
206
256
|
}
|
|
207
257
|
|
|
208
258
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { Ellipsis, ExtractSegmentsResult, SliceResult } from "@oh-my-pi/pi-natives";
|
|
2
1
|
import {
|
|
2
|
+
Ellipsis,
|
|
3
|
+
type ExtractSegmentsResult,
|
|
3
4
|
extractSegments as nativeExtractSegments,
|
|
4
5
|
sliceWithWidth as nativeSliceWithWidth,
|
|
5
6
|
truncateToWidth as nativeTruncateToWidth,
|
|
6
7
|
wrapTextWithAnsi as nativeWrapTextWithAnsi,
|
|
8
|
+
type SliceResult,
|
|
7
9
|
} from "@oh-my-pi/pi-natives";
|
|
8
10
|
import { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils";
|
|
9
11
|
|
|
@@ -21,7 +23,12 @@ export function truncateToWidth(
|
|
|
21
23
|
ellipsisKind?: Ellipsis | null,
|
|
22
24
|
pad?: boolean | null,
|
|
23
25
|
): string {
|
|
24
|
-
|
|
26
|
+
// Guard nullish napi inputs: napi-rs 3 on the Windows prebuilt rejects
|
|
27
|
+
// `null` for `Option<u8>` (Ellipsis) / `Option<bool>` (pad) (issue #848),
|
|
28
|
+
// and `maxWidth` is a required `u32` that throws on `null`/`undefined`
|
|
29
|
+
// everywhere. Pass concrete defaults that mirror the Rust `unwrap_or`s.
|
|
30
|
+
const safeWidth = Number.isFinite(maxWidth) ? Math.max(0, Math.trunc(maxWidth)) : 0;
|
|
31
|
+
return nativeTruncateToWidth(text, safeWidth, ellipsisKind ?? Ellipsis.Unicode, pad ?? false, getDefaultTabWidth());
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export function wrapTextWithAnsi(text: string, width: number): string[] {
|