From 8af6baa2d1df591a5113f41635f78faec77dd4b5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Dec 2021 17:12:38 +0100 Subject: [PATCH] core-components: refactor AnsiProcessor to keep track of line numbers Signed-off-by: Patrik Oldsberg --- .../LogViewer/AnsiProcessor.test.ts | 289 ++++++++++-------- .../src/components/LogViewer/AnsiProcessor.ts | 59 +++- .../src/components/LogViewer/LogViewer.tsx | 2 +- 3 files changed, 204 insertions(+), 146 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts index b2ceee78b0..9ef0353eca 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts @@ -20,37 +20,43 @@ describe('AnsiProcessor', () => { it('should process a single line', () => { const processor = new AnsiProcessor(); expect(processor.process('foo\x1b[31mbar\x1b[39mbaz')).toEqual([ - [ - { - text: 'foo', - modifiers: {}, - }, - { - text: 'bar', - modifiers: { foreground: 'red' }, - }, - { - text: 'baz', - modifiers: {}, - }, - ], + { + chunks: [ + { + text: 'foo', + modifiers: {}, + }, + { + text: 'bar', + modifiers: { foreground: 'red' }, + }, + { + text: 'baz', + modifiers: {}, + }, + ], + lineNumber: 1, + }, ]); expect(processor.process(`foo bar: baz`)).toEqual([ - [ - { - text: 'foo ', - modifiers: {}, - }, - { - text: 'bar', - modifiers: { foreground: 'green' }, - }, - { - text: ': baz', - modifiers: {}, - }, - ], + { + chunks: [ + { + text: 'foo ', + modifiers: {}, + }, + { + text: 'bar', + modifiers: { foreground: 'green' }, + }, + { + text: ': baz', + modifiers: {}, + }, + ], + lineNumber: 1, + }, ]); }); @@ -62,36 +68,42 @@ a\x1b[34mb\x1b[39mc x\x1b[44my\x1b[49mz `), ).toEqual([ - [{ text: '', modifiers: {} }], - [ - { - text: 'a', - modifiers: {}, - }, - { - text: 'b', - modifiers: { foreground: 'blue' }, - }, - { - text: 'c', - modifiers: {}, - }, - ], - [ - { - text: 'x', - modifiers: {}, - }, - { - text: 'y', - modifiers: { background: 'blue' }, - }, - { - text: 'z', - modifiers: {}, - }, - ], - [{ text: '', modifiers: {} }], + { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'blue' }, + }, + { + text: 'c', + modifiers: {}, + }, + ], + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: {}, + }, + { + text: 'y', + modifiers: { background: 'blue' }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + lineNumber: 3, + }, + { chunks: [{ text: '', modifiers: {} }], lineNumber: 4 }, ]); }); @@ -102,35 +114,41 @@ x\x1b[44my\x1b[49mz a\x1b[45mb\x1b[35mc x\x1b[39my\x1b[49mz`), ).toEqual([ - [{ text: '', modifiers: {} }], - [ - { - text: 'a', - modifiers: {}, - }, - { - text: 'b', - modifiers: { background: 'magenta' }, - }, - { - text: 'c', - modifiers: { foreground: 'magenta', background: 'magenta' }, - }, - ], - [ - { - text: 'x', - modifiers: { foreground: 'magenta', background: 'magenta' }, - }, - { - text: 'y', - modifiers: { background: 'magenta' }, - }, - { - text: 'z', - modifiers: {}, - }, - ], + { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { background: 'magenta' }, + }, + { + text: 'c', + modifiers: { foreground: 'magenta', background: 'magenta' }, + }, + ], + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: { foreground: 'magenta', background: 'magenta' }, + }, + { + text: 'y', + modifiers: { background: 'magenta' }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + lineNumber: 3, + }, ]); }); @@ -139,56 +157,65 @@ x\x1b[39my\x1b[49mz`), const out1 = processor.process(` a\x1b[36mb\x1b[3mc`); expect(out1).toEqual([ - [{ text: '', modifiers: {} }], - [ - { - text: 'a', - modifiers: {}, - }, - { - text: 'b', - modifiers: { foreground: 'cyan' }, - }, - { - text: 'c', - modifiers: { foreground: 'cyan', italic: true }, - }, - ], + { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'cyan' }, + }, + { + text: 'c', + modifiers: { foreground: 'cyan', italic: true }, + }, + ], + lineNumber: 2, + }, ]); const out2 = processor.process(` a\x1b[36mb\x1b[3mc x\x1b[39my\x1b[23mz`); expect(out2).toEqual([ - [{ text: '', modifiers: {} }], - [ - { - text: 'a', - modifiers: {}, - }, - { - text: 'b', - modifiers: { foreground: 'cyan' }, - }, - { - text: 'c', - modifiers: { foreground: 'cyan', italic: true }, - }, - ], - [ - { - text: 'x', - modifiers: { foreground: 'cyan', italic: true }, - }, - { - text: 'y', - modifiers: { italic: true }, - }, - { - text: 'z', - modifiers: {}, - }, - ], + { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'cyan' }, + }, + { + text: 'c', + modifiers: { foreground: 'cyan', italic: true }, + }, + ], + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: { foreground: 'cyan', italic: true }, + }, + { + text: 'y', + modifiers: { italic: true }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + lineNumber: 3, + }, ]); // Verifies that we appended rather than reprocessed diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts index f148005355..f3ea37615c 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts @@ -71,34 +71,56 @@ export interface ChunkModifiers { underline?: boolean; } -export interface Chunk { +// export interface AnsiLine { +// lineNumber: number; +// chunks: AnsiChunk[]; +// } + +export interface AnsiChunk { text: string; modifiers: ChunkModifiers; } +export class AnsiLine { + constructor( + readonly lineNumber: number = 1, + readonly chunks: AnsiChunk[] = [], + ) {} + + lastChunk(): AnsiChunk | undefined { + return this.chunks[this.chunks.length - 1]; + } +} + export class AnsiProcessor { private text: string = ''; - private lines: Chunk[][] = []; + private lines: AnsiLine[] = []; /** * Processes a chunk of text while keeping internal state that optimizes * subsequent processing that appends to the text. */ - process(text: string): Chunk[][] { + process(text: string): AnsiLine[] { if (this.text === text) { return this.lines; } if (text.startsWith(this.text)) { const lastLineIndex = this.lines.length > 0 ? this.lines.length - 1 : 0; - const lastLine = this.lines[lastLineIndex] ?? []; - const lastChunk = lastLine[lastLine.length - 1] as Chunk | undefined; + const lastLine = this.lines[lastLineIndex] ?? new AnsiLine(); + const lastChunk = lastLine.lastChunk(); + const newLines = this.processLines( (lastChunk?.text ?? '') + text.slice(this.text.length), lastChunk?.modifiers, + lastLine?.lineNumber, ); this.text = text; - lastLine.splice(lastLine.length - 1, 1, ...newLines[0]); + lastLine.chunks.splice( + lastLine.chunks.length - 1, + 1, + ...newLines[0]?.chunks, + ); this.lines[lastLineIndex] = lastLine; this.lines.push(...newLines.slice(1)); } else { @@ -113,16 +135,23 @@ export class AnsiProcessor { private processLines = ( text: string, modifiers: ChunkModifiers = {}, - ): Chunk[][] => { - const lines: Chunk[][] = []; + startingLineNumber: number = 1, + ): AnsiLine[] => { + const lines: AnsiLine[] = []; + + let currentModifiers = modifiers; + let currentLineNumber = startingLineNumber; let prevIndex = 0; - let currentModifiers = modifiers; newlineRegex.lastIndex = 0; for (;;) { const match = newlineRegex.exec(text); if (!match) { - lines.push(this.processText(text.slice(prevIndex), currentModifiers)); + const chunks = this.processText( + text.slice(prevIndex), + currentModifiers, + ); + lines.push(new AnsiLine(currentLineNumber, chunks)); return lines; } @@ -130,11 +159,12 @@ export class AnsiProcessor { prevIndex = match.index + match[0].length; const chunks = this.processText(line, currentModifiers); - lines.push(chunks); + lines.push(new AnsiLine(currentLineNumber, chunks)); // Modifiers that are active in the last chunk are carried over to the next line currentModifiers = chunks[chunks.length - 1].modifiers ?? currentModifiers; + currentLineNumber += 1; } }; @@ -142,11 +172,12 @@ export class AnsiProcessor { private processText = ( fullText: string, modifiers: ChunkModifiers, - ): Chunk[] => { - const chunks: Chunk[] = []; + ): AnsiChunk[] => { + const chunks: AnsiChunk[] = []; + + let currentModifiers = modifiers; let prevIndex = 0; - let currentModifiers = modifiers; ansiRegex.lastIndex = 0; for (;;) { const match = ansiRegex.exec(fullText); diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 21d42a1862..a77990f734 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -179,7 +179,7 @@ export function LogViewer(props: LogViewerProps) { {!noLineNumbers && ( {index + 1} )} - {data[index].map(({ text, modifiers }, i) => ( + {data[index].chunks.map(({ text, modifiers }, i) => (