@xprnvd/pi-context-view 0.7.2 → 0.8.0
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/README.md +14 -12
- package/package.json +3 -3
- package/src/capture/diff.ts +72 -13
- package/src/capture/request.ts +47 -14
- package/src/ui/injections-model.ts +114 -5
- package/src/ui/injections-view.ts +336 -42
- package/src/ui/layout.ts +14 -1
- package/src/ui/search.ts +427 -0
- package/src/ui/usage-preview.ts +8 -0
- package/src/ui/usage-view.ts +200 -27
package/src/ui/usage-view.ts
CHANGED
|
@@ -27,8 +27,10 @@ import {
|
|
|
27
27
|
capNoticeLines,
|
|
28
28
|
DEFAULT_TERMINAL_ROWS,
|
|
29
29
|
descriptionBlockRows,
|
|
30
|
+
fitHintRow,
|
|
30
31
|
fitLine,
|
|
31
32
|
fitToTerminalHeight,
|
|
33
|
+
type Hint,
|
|
32
34
|
hintRow,
|
|
33
35
|
isPageBackKey,
|
|
34
36
|
isPageForwardKey,
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
} from "./layout.ts";
|
|
42
44
|
import { forcedMarker } from "./markers.ts";
|
|
43
45
|
import { descriptionNoteLines, emptyPreviewNote, probingDisabledNote, thinkingNote, usageNote } from "./notes.ts";
|
|
46
|
+
import { MATCH_STEP_KEY_HINT, PreviewSearch, SEARCH_KEY } from "./search.ts";
|
|
44
47
|
import { previewBodyLines, previewLegendLines } from "./section-preview.ts";
|
|
45
48
|
import { splitSkillPreview } from "./skill-preview.ts";
|
|
46
49
|
import { buildUsageMap, calculateFitMapScale, type UsageMap, type UsageMapCell } from "./usage-map.ts";
|
|
@@ -87,6 +90,8 @@ const MAP_KEY_SIZE_LABEL = "Block Size";
|
|
|
87
90
|
const MAP_KEY_DETAILED_SPARE_ROWS = 5;
|
|
88
91
|
/** Rows the single-line key costs beside the complete legend: one separator plus one key row. */
|
|
89
92
|
const MAP_KEY_COMPACT_SPARE_ROWS = 2;
|
|
93
|
+
/** Lines kept above a match scrolled into view, so it is read in context. */
|
|
94
|
+
const SEARCH_CONTEXT_LINES = 2;
|
|
90
95
|
|
|
91
96
|
/** Everything the Usage view renders, classified once when the view opens. */
|
|
92
97
|
export interface UsageViewInput {
|
|
@@ -161,6 +166,29 @@ interface BlockBody {
|
|
|
161
166
|
readonly lines: readonly string[];
|
|
162
167
|
}
|
|
163
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Every block of a stream flattened for search: each entry header and its
|
|
171
|
+
* uncapped content, blocks separated by a blank line. Capped stream lines are
|
|
172
|
+
* a prefix of each block here, so a stream line maps to its search row.
|
|
173
|
+
*/
|
|
174
|
+
interface StreamSearchLines {
|
|
175
|
+
/** The wrapped entries these lines flatten; a rewrap invalidates them. */
|
|
176
|
+
readonly entries: readonly EntryLines[];
|
|
177
|
+
readonly lines: readonly string[];
|
|
178
|
+
/** First search row of each block. */
|
|
179
|
+
readonly starts: readonly number[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Full-content search lines: the entry header when it renders, then the body.
|
|
184
|
+
* Including the header keeps a stream match on it alive once its block opens.
|
|
185
|
+
*/
|
|
186
|
+
interface ContentSearchLines {
|
|
187
|
+
readonly body: readonly string[];
|
|
188
|
+
readonly header: string | undefined;
|
|
189
|
+
readonly lines: readonly string[];
|
|
190
|
+
}
|
|
191
|
+
|
|
164
192
|
/** Open the Usage view as a fullscreen overlay. */
|
|
165
193
|
export async function showUsageView(context: ExtensionCommandContext, input: UsageViewInput): Promise<void> {
|
|
166
194
|
await context.ui.custom<void>(
|
|
@@ -193,6 +221,10 @@ export class UsageView {
|
|
|
193
221
|
private readonly categoryColors: CategoryColors;
|
|
194
222
|
private readonly legendRows: readonly LegendRow[];
|
|
195
223
|
private readonly navigator: ListNavigator;
|
|
224
|
+
/** Search of the open category, shared by its block stream and the block it opens. */
|
|
225
|
+
private readonly search = new PreviewSearch();
|
|
226
|
+
private cachedStreamSearch: StreamSearchLines | undefined;
|
|
227
|
+
private cachedContentSearch: ContentSearchLines | undefined;
|
|
196
228
|
private readonly previewScroller = new PreviewScroller();
|
|
197
229
|
private readonly blockNavigator = new BlockNavigator();
|
|
198
230
|
private readonly fitMapScale: number | undefined;
|
|
@@ -234,7 +266,9 @@ export class UsageView {
|
|
|
234
266
|
public handleInput(data: string): void {
|
|
235
267
|
if (this.previewRow !== undefined) {
|
|
236
268
|
// Route on the same predicate the renderer uses, so keys always target the visible level.
|
|
237
|
-
|
|
269
|
+
const blockStream = this.getFullContentEntry(this.previewRow) === undefined;
|
|
270
|
+
if (this.search.prompt.editing) this.handleSearchInput(data, blockStream);
|
|
271
|
+
else if (blockStream) this.handlePreviewInput(data);
|
|
238
272
|
else this.handleContentInput(data);
|
|
239
273
|
return;
|
|
240
274
|
}
|
|
@@ -735,12 +769,59 @@ export class UsageView {
|
|
|
735
769
|
|
|
736
770
|
// === Preview mode ===
|
|
737
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Keys while the search prompt is open at either preview level: printable
|
|
774
|
+
* keys edit the query, the arrow and page keys still navigate, Enter keeps
|
|
775
|
+
* the query, and Escape drops it.
|
|
776
|
+
*/
|
|
777
|
+
private handleSearchInput(data: string, blockStream: boolean): void {
|
|
778
|
+
const search = this.search;
|
|
779
|
+
if (matchesKey(data, Key.escape)) {
|
|
780
|
+
search.clear();
|
|
781
|
+
} else if (matchesKey(data, Key.enter)) {
|
|
782
|
+
if (search.prompt.active) search.prompt.submit();
|
|
783
|
+
else search.clear();
|
|
784
|
+
} else if (
|
|
785
|
+
parseWheelDirection(data) !== undefined ||
|
|
786
|
+
matchesKey(data, Key.up) ||
|
|
787
|
+
matchesKey(data, Key.down) ||
|
|
788
|
+
matchesKey(data, Key.pageUp) ||
|
|
789
|
+
matchesKey(data, Key.pageDown)
|
|
790
|
+
) {
|
|
791
|
+
// Plain navigation keys never type into the prompt, so the level handles them as usual.
|
|
792
|
+
if (blockStream) this.handlePreviewInput(data);
|
|
793
|
+
else this.handleContentInput(data);
|
|
794
|
+
} else {
|
|
795
|
+
search.type(data);
|
|
796
|
+
}
|
|
797
|
+
this.clearCache();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** `/` opens the search prompt and `n`/`N` step through matches; false for any other key. */
|
|
801
|
+
private handleSearchKey(data: string, anchorRow: number): boolean {
|
|
802
|
+
if (data === SEARCH_KEY) {
|
|
803
|
+
this.search.edit(anchorRow);
|
|
804
|
+
} else if (data === "n" || data === "N") {
|
|
805
|
+
this.search.step(data === "n" ? 1 : -1);
|
|
806
|
+
} else {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
this.clearCache();
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
|
|
738
813
|
/** Block navigation, block opening, and return-to-list keys. */
|
|
739
814
|
private handlePreviewInput(data: string): void {
|
|
740
815
|
if (matchesKey(data, Key.escape) || data === "q") {
|
|
741
816
|
this.closePreview();
|
|
742
817
|
return;
|
|
743
818
|
}
|
|
819
|
+
// Decide from the entries, not render caches: keys can arrive before the stream first renders.
|
|
820
|
+
const row = this.previewRow;
|
|
821
|
+
if (row !== undefined && this.previewEntries(row).length > 0) {
|
|
822
|
+
const anchor = this.cachedStreamSearch?.starts[this.blockNavigator.selected] ?? 0;
|
|
823
|
+
if (this.handleSearchKey(data, anchor)) return;
|
|
824
|
+
}
|
|
744
825
|
// Blocks are selected rather than scrolled, so one notch steps one block.
|
|
745
826
|
const wheel = parseWheelDirection(data);
|
|
746
827
|
if (wheel !== undefined) {
|
|
@@ -772,6 +853,7 @@ export class UsageView {
|
|
|
772
853
|
else this.closeBlock();
|
|
773
854
|
return;
|
|
774
855
|
}
|
|
856
|
+
if (this.handleSearchKey(data, this.previewScroller.offset)) return;
|
|
775
857
|
const wheel = parseWheelDirection(data);
|
|
776
858
|
if (wheel !== undefined) {
|
|
777
859
|
if (this.previewScroller.scrollBy(wheel * this.wheelScrollLines)) this.clearCache();
|
|
@@ -801,6 +883,7 @@ export class UsageView {
|
|
|
801
883
|
this.clearPreviewContent();
|
|
802
884
|
this.blockNavigator.reset();
|
|
803
885
|
this.previewScroller.reset();
|
|
886
|
+
this.search.clear();
|
|
804
887
|
this.clearCache();
|
|
805
888
|
}
|
|
806
889
|
|
|
@@ -809,52 +892,77 @@ export class UsageView {
|
|
|
809
892
|
this.previewRow = undefined;
|
|
810
893
|
this.openBlockIndex = undefined;
|
|
811
894
|
this.cachedPreviewEntries = undefined;
|
|
895
|
+
this.search.clear();
|
|
812
896
|
this.clearPreviewContent();
|
|
813
897
|
this.clearCache();
|
|
814
898
|
}
|
|
815
899
|
|
|
816
|
-
/**
|
|
900
|
+
/**
|
|
901
|
+
* Open hidden content from the selected capped block; complete and empty
|
|
902
|
+
* blocks have nothing to open. A search continues at the current match when
|
|
903
|
+
* it lies in this block, so a match hidden by the cap is one Enter away.
|
|
904
|
+
*/
|
|
817
905
|
private openBlock(): void {
|
|
818
906
|
// The rendered stream carries the height-dependent cap the user sees, so it decides what Enter opens.
|
|
819
907
|
const index = this.blockNavigator.selected;
|
|
820
908
|
const block = this.cachedStream?.blocks[index];
|
|
821
909
|
if (block === undefined || block.hiddenLineCount === 0) return;
|
|
910
|
+
// The opened level searches the same header and content rows, so offsets carry over unchanged.
|
|
911
|
+
const start = this.cachedStreamSearch?.starts[index] ?? 0;
|
|
912
|
+
const blockLineCount = block.lines.length + block.hiddenLineCount;
|
|
913
|
+
const currentRow = this.currentMatchRow();
|
|
914
|
+
const blockRow = currentRow === undefined ? -1 : currentRow - start;
|
|
822
915
|
this.openBlockIndex = index;
|
|
823
916
|
this.cachedBlockBody = undefined;
|
|
824
917
|
this.previewScroller.reset();
|
|
918
|
+
this.search.restart(blockRow >= 0 && blockRow < blockLineCount ? blockRow : 0);
|
|
825
919
|
this.clearCache();
|
|
826
920
|
}
|
|
827
921
|
|
|
828
|
-
/** Return to the block stream with the same selected block. */
|
|
922
|
+
/** Return to the block stream with the same selected block, keeping the search on it. */
|
|
829
923
|
private closeBlock(): void {
|
|
924
|
+
const index = this.openBlockIndex ?? 0;
|
|
830
925
|
this.openBlockIndex = undefined;
|
|
831
926
|
this.cachedBlockBody = undefined;
|
|
927
|
+
this.search.restart(this.cachedStreamSearch?.starts[index] ?? 0);
|
|
832
928
|
this.clearCache();
|
|
833
929
|
}
|
|
834
930
|
|
|
931
|
+
/** First row of the current match in the lines the search last saw. */
|
|
932
|
+
private currentMatchRow(): number | undefined {
|
|
933
|
+
const current = this.search.current;
|
|
934
|
+
return current === undefined ? undefined : this.search.matches[current]?.segments[0]?.row;
|
|
935
|
+
}
|
|
936
|
+
|
|
835
937
|
/** Drop width- and theme-dependent preview rendering. */
|
|
836
938
|
private clearPreviewContent(): void {
|
|
837
939
|
this.cachedContent = undefined;
|
|
838
940
|
this.cachedStream = undefined;
|
|
839
941
|
this.cachedBlockBody = undefined;
|
|
942
|
+
this.cachedStreamSearch = undefined;
|
|
943
|
+
this.cachedContentSearch = undefined;
|
|
840
944
|
}
|
|
841
945
|
|
|
842
946
|
/** Scrollable chronological block stream for one category. */
|
|
843
947
|
private renderPreview(width: number, terminalRows: number, row: CategoryLegendRow): string[] {
|
|
844
948
|
const theme = this.theme;
|
|
845
949
|
const border = theme.fg("border", "─".repeat(Math.max(1, width)));
|
|
846
|
-
const
|
|
847
|
-
const
|
|
848
|
-
const
|
|
950
|
+
const searchRowCount = this.searchRowCount();
|
|
951
|
+
const descriptionLines = this.previewDescriptionLines(width, terminalRows - searchRowCount, row);
|
|
952
|
+
const reservedLineCount = descriptionBlockRows(descriptionLines) + searchRowCount;
|
|
953
|
+
const stream = this.previewStream(width, previewBlockMaxLines(terminalRows, reservedLineCount), row);
|
|
849
954
|
const viewport = calculateViewport(
|
|
850
955
|
Math.max(1, stream.layout.lines.length),
|
|
851
956
|
terminalRows,
|
|
852
957
|
PREVIEW_FIXED_LINE_COUNT,
|
|
853
|
-
|
|
958
|
+
reservedLineCount,
|
|
854
959
|
);
|
|
855
960
|
this.blockNavigator.setExtent(stream.layout, viewport.visibleCount);
|
|
961
|
+
const searchLines = this.streamSearch(width, row);
|
|
962
|
+
const revealRow = this.search.sync(searchLines.lines);
|
|
963
|
+
if (revealRow !== undefined) this.blockNavigator.select(blockAtRow(searchLines.starts, revealRow));
|
|
856
964
|
|
|
857
|
-
const lines: string[] = [border, "", this.categoryHeaderLine(row, width), ""];
|
|
965
|
+
const lines: string[] = [border, "", this.categoryHeaderLine(row, width), "", ...this.searchPromptLines(width)];
|
|
858
966
|
lines.push(...this.previewStreamLines(stream, viewport.visibleCount, width));
|
|
859
967
|
if (viewport.showScroll) {
|
|
860
968
|
lines.push(
|
|
@@ -866,11 +974,67 @@ export class UsageView {
|
|
|
866
974
|
}
|
|
867
975
|
if (descriptionLines.length > 0) lines.push("", ...descriptionLines);
|
|
868
976
|
lines.push("");
|
|
869
|
-
lines.push(
|
|
977
|
+
lines.push(fitHintRow(theme, this.streamHints(stream.blocks.length), width));
|
|
870
978
|
lines.push("", border);
|
|
871
979
|
return fitToTerminalHeight(lines, terminalRows, border);
|
|
872
980
|
}
|
|
873
981
|
|
|
982
|
+
/** Rows the search prompt and its trailing blank row take while a query is typed or kept. */
|
|
983
|
+
private searchRowCount(): number {
|
|
984
|
+
return this.search.prompt.editing || this.search.prompt.active ? 2 : 0;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Search prompt row and its trailing blank row, rendered after the frame's matches are known. */
|
|
988
|
+
private searchPromptLines(width: number): string[] {
|
|
989
|
+
if (this.searchRowCount() === 0) return [];
|
|
990
|
+
return [this.search.prompt.render(this.theme, width, "Search", this.search.status), ""];
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** Cached search lines of the open stream; rebuilt with the content they flatten. */
|
|
994
|
+
private streamSearch(width: number, row: CategoryLegendRow): StreamSearchLines {
|
|
995
|
+
const entries = this.previewContent(width, row);
|
|
996
|
+
if (this.cachedStreamSearch?.entries === entries) return this.cachedStreamSearch;
|
|
997
|
+
const lines: string[] = [];
|
|
998
|
+
const starts: number[] = [];
|
|
999
|
+
this.previewEntries(row).forEach((entry, index) => {
|
|
1000
|
+
if (index > 0) lines.push("");
|
|
1001
|
+
starts.push(lines.length);
|
|
1002
|
+
lines.push(this.entryHeader(entry), ...entries[index] ?? []);
|
|
1003
|
+
});
|
|
1004
|
+
this.cachedStreamSearch = { entries, lines, starts };
|
|
1005
|
+
return this.cachedStreamSearch;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/** Cached full-content search lines, rebuilt with the body they cover. */
|
|
1009
|
+
private contentSearch(body: readonly string[], header: string | undefined): ContentSearchLines {
|
|
1010
|
+
const cached = this.cachedContentSearch;
|
|
1011
|
+
if (cached?.body === body && cached.header === header) return cached;
|
|
1012
|
+
const lines = header === undefined ? body : [header, ...body];
|
|
1013
|
+
this.cachedContentSearch = { body, header, lines };
|
|
1014
|
+
return this.cachedContentSearch;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Block stream hints; an empty stream moves nowhere and has nothing to search. */
|
|
1018
|
+
private streamHints(blockCount: number): Hint[] {
|
|
1019
|
+
if (blockCount === 0) return [["Esc", "Back"]];
|
|
1020
|
+
return this.searchHints("Navigate");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** Hints for plain navigation, an open search prompt, or a kept search. */
|
|
1024
|
+
private searchHints(stepLabel: string): Hint[] {
|
|
1025
|
+
const prompt = this.search.prompt;
|
|
1026
|
+
if (prompt.editing) return [["↑↓", stepLabel], ["Enter", "Done"], ["Esc", "Clear"]];
|
|
1027
|
+
if (prompt.active) {
|
|
1028
|
+
return [
|
|
1029
|
+
[STEP_KEY_HINT, stepLabel],
|
|
1030
|
+
[MATCH_STEP_KEY_HINT, "Next/Prev"],
|
|
1031
|
+
[SEARCH_KEY, "Search", true],
|
|
1032
|
+
["Esc", "Back"],
|
|
1033
|
+
];
|
|
1034
|
+
}
|
|
1035
|
+
return [[STEP_KEY_HINT, stepLabel], ["PgUp/PgDn", "Page"], [SEARCH_KEY, "Search", true], ["Esc", "Back"]];
|
|
1036
|
+
}
|
|
1037
|
+
|
|
874
1038
|
/** Full content with an identity header unless the System Prompt category already identifies it. */
|
|
875
1039
|
private renderContentView(
|
|
876
1040
|
width: number,
|
|
@@ -882,7 +1046,8 @@ export class UsageView {
|
|
|
882
1046
|
const border = theme.fg("border", "─".repeat(Math.max(1, width)));
|
|
883
1047
|
const body = this.blockBodyLines(width, row, entry);
|
|
884
1048
|
const showEntryHeader = row.rootId !== "system-prompt" || this.openBlockIndex !== undefined;
|
|
885
|
-
const fixedLineCount = showEntryHeader ? BLOCK_FIXED_LINE_COUNT : PREVIEW_FIXED_LINE_COUNT
|
|
1049
|
+
const fixedLineCount = (showEntryHeader ? BLOCK_FIXED_LINE_COUNT : PREVIEW_FIXED_LINE_COUNT) +
|
|
1050
|
+
this.searchRowCount();
|
|
886
1051
|
const descriptionLines = row.rootId === "assistant-thinking" && this.openBlockIndex === undefined
|
|
887
1052
|
? this.thinkingDescriptionLines(width, row)
|
|
888
1053
|
: previewLegendLines(theme, [entry], {
|
|
@@ -894,17 +1059,27 @@ export class UsageView {
|
|
|
894
1059
|
body.length, terminalRows, fixedLineCount, descriptionBlockRows(descriptionLines),
|
|
895
1060
|
);
|
|
896
1061
|
this.previewScroller.setExtent(body.length, viewport.visibleCount);
|
|
1062
|
+
const searchLines = this.contentSearch(body, showEntryHeader ? this.entryHeader(entry) : undefined);
|
|
1063
|
+
// Body rows follow the header row when it is searched.
|
|
1064
|
+
const bodyRow = searchLines.header === undefined ? 0 : 1;
|
|
1065
|
+
const revealLine = this.search.sync(searchLines.lines);
|
|
1066
|
+
if (revealLine !== undefined) {
|
|
1067
|
+
this.previewScroller.reveal(Math.max(0, revealLine - bodyRow), SEARCH_CONTEXT_LINES);
|
|
1068
|
+
}
|
|
897
1069
|
|
|
898
1070
|
const lines: string[] = [
|
|
899
1071
|
border,
|
|
900
1072
|
"",
|
|
901
1073
|
this.categoryHeaderLine(row, width),
|
|
902
1074
|
"",
|
|
903
|
-
...
|
|
1075
|
+
...this.searchPromptLines(width),
|
|
1076
|
+
...(searchLines.header === undefined
|
|
1077
|
+
? []
|
|
1078
|
+
: [this.fit(`${BODY_INDENT}${this.search.highlight(theme, searchLines.header, 0)}`, width), ""]),
|
|
904
1079
|
];
|
|
905
1080
|
const start = this.previewScroller.offset;
|
|
906
1081
|
for (let index = start; index < start + viewport.visibleCount; index++) {
|
|
907
|
-
lines.push(body[index] ?? "");
|
|
1082
|
+
lines.push(this.search.highlight(theme, body[index] ?? "", index + bodyRow));
|
|
908
1083
|
}
|
|
909
1084
|
if (viewport.showScroll) {
|
|
910
1085
|
lines.push(
|
|
@@ -913,16 +1088,7 @@ export class UsageView {
|
|
|
913
1088
|
}
|
|
914
1089
|
if (descriptionLines.length > 0) lines.push("", ...descriptionLines);
|
|
915
1090
|
lines.push("");
|
|
916
|
-
lines.push(
|
|
917
|
-
this.fit(
|
|
918
|
-
hintRow(theme, [
|
|
919
|
-
[STEP_KEY_HINT, "Scroll"],
|
|
920
|
-
["PgUp/PgDn", "Page"],
|
|
921
|
-
["Esc", "Back"],
|
|
922
|
-
]),
|
|
923
|
-
width,
|
|
924
|
-
),
|
|
925
|
-
);
|
|
1091
|
+
lines.push(fitHintRow(theme, this.searchHints("Scroll"), width));
|
|
926
1092
|
lines.push("", border);
|
|
927
1093
|
return fitToTerminalHeight(lines, terminalRows, border);
|
|
928
1094
|
}
|
|
@@ -963,13 +1129,19 @@ export class UsageView {
|
|
|
963
1129
|
const block = stream.blocks[ref.blockIndex];
|
|
964
1130
|
if (block === undefined) return "";
|
|
965
1131
|
const selected = ref.blockIndex === this.blockNavigator.selected;
|
|
1132
|
+
const start = this.cachedStreamSearch?.starts[ref.blockIndex];
|
|
966
1133
|
const line = ref.lineIndex < block.lines.length
|
|
967
|
-
? block.lines[ref.lineIndex] ?? ""
|
|
1134
|
+
? this.highlightStreamLine(block.lines[ref.lineIndex] ?? "", start, ref.lineIndex)
|
|
968
1135
|
: this.truncationMarker(block.hiddenLineCount, selected);
|
|
969
1136
|
const gutter = this.blockGutter(selected, line === "");
|
|
970
1137
|
return this.fit(`${gutter}${line}`, width);
|
|
971
1138
|
}
|
|
972
1139
|
|
|
1140
|
+
/** Highlight one visible stream line through its row in the flattened search lines. */
|
|
1141
|
+
private highlightStreamLine(line: string, blockStart: number | undefined, lineIndex: number): string {
|
|
1142
|
+
return blockStart === undefined ? line : this.search.highlight(this.theme, line, blockStart + lineIndex);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
973
1145
|
/** Accent bar marking the selected block, or the plain two-column indent. */
|
|
974
1146
|
private blockGutter(selected: boolean, blankLine: boolean): string {
|
|
975
1147
|
if (!selected) return blankLine ? "" : BODY_INDENT;
|
|
@@ -1205,10 +1377,11 @@ function previewBlockMaxLines(terminalRows: number, descriptionLineCount: number
|
|
|
1205
1377
|
return Math.max(PREVIEW_BLOCK_MIN_LINES, Math.min(PREVIEW_BLOCK_MAX_LINES, fitted));
|
|
1206
1378
|
}
|
|
1207
1379
|
|
|
1208
|
-
/** Block
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1211
|
-
|
|
1380
|
+
/** Block holding one row of the flattened stream search lines. */
|
|
1381
|
+
function blockAtRow(starts: readonly number[], row: number): number {
|
|
1382
|
+
let index = 0;
|
|
1383
|
+
while (index + 1 < starts.length && (starts[index + 1] ?? Infinity) <= row) index++;
|
|
1384
|
+
return index;
|
|
1212
1385
|
}
|
|
1213
1386
|
|
|
1214
1387
|
/** Stream lines one block occupies, including its truncation marker row. */
|