diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-utils.ts b/arduino-ide-extension/src/browser/serial/monitor/monitor-utils.ts index 41cb4f450..d11292769 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-utils.ts +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-utils.ts @@ -1,47 +1,81 @@ -import { Line, SerialMonitorOutput } from './serial-monitor-send-output'; +import {Line, SerialMonitorOutput} from './serial-monitor-send-output'; + +function writeOverLine(line: Line, insert: string, cursorPosition: number): [number, number] { + var lenBefore = line.message.length; + line.message = line.message.substring(0, cursorPosition) + insert + line.message.substring(cursorPosition + insert.length) + cursorPosition = cursorPosition + insert.length; + line.lineLen = line.message.length; + return [line.lineLen - lenBefore, cursorPosition]; +} + +const escapeSequenceGoHome = '\x1B[H'; +const escapeSequenceClearScreen = '\x1B[2J'; export function messagesToLines( messages: string[], prevLines: Line[] = [], charCount = 0, - separator = '\n' -): [Line[], number] { - const linesToAdd: Line[] = prevLines.length - ? [prevLines[prevLines.length - 1]] - : [{ message: '', lineLen: 0 }]; - if (!(Symbol.iterator in Object(messages))) return [prevLines, charCount]; + currentLineIndex: number | null, + currentCursorPosition: number, + separator = '\n', +): [Line[], number, number | null, number, string | null] { + if (!prevLines.length) { + prevLines = [{message: '', lineLen: 0, timestamp: new Date()}]; + } + + currentLineIndex = currentLineIndex || 0; - for (const message of messages) { - const messageLen = message.length; - charCount += messageLen; - const lastLine = linesToAdd[linesToAdd.length - 1]; + let allMessages = messages.join(''); + let overflow = null; - // if the previous messages ends with "separator" add a new line - if (lastLine.message.charAt(lastLine.message.length - 1) === separator) { - linesToAdd.push({ - message, - timestamp: new Date(), - lineLen: messageLen, - }); - } else { - // concatenate to the last line - linesToAdd[linesToAdd.length - 1].message += message; - linesToAdd[linesToAdd.length - 1].lineLen += messageLen; - if (!linesToAdd[linesToAdd.length - 1].timestamp) { - linesToAdd[linesToAdd.length - 1].timestamp = new Date(); + if (allMessages.indexOf(escapeSequenceGoHome) >= 0) { + const before = allMessages.substring(0, allMessages.indexOf(escapeSequenceGoHome)); + const after = allMessages.substring(allMessages.indexOf(escapeSequenceGoHome) + escapeSequenceGoHome.length); + const [_lines, _charCount] = messagesToLines([before], prevLines, charCount, currentLineIndex, currentCursorPosition, separator); + return messagesToLines([after], _lines, _charCount, 0, 0, separator); + } else if (allMessages.indexOf(escapeSequenceClearScreen) >= 0) { + const after = allMessages.substring(allMessages.lastIndexOf(escapeSequenceClearScreen) + escapeSequenceClearScreen.length); + return messagesToLines([after], [], 0, 0, 0, separator); + } else if (allMessages.lastIndexOf('\x1B') >= 0) { + overflow = allMessages.substring(allMessages.lastIndexOf('\x1B')); + const result = messagesToLines([allMessages.substring(0, allMessages.lastIndexOf('\x1B'))], prevLines, charCount, currentLineIndex, currentCursorPosition, separator); + result[4] = overflow; + return result; + } + + const chunks = allMessages.split(separator); + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + if (chunk !== '') { + if (prevLines[currentLineIndex].message[currentCursorPosition - 1] === '\n') { + currentLineIndex++; + currentCursorPosition = 0; + } + if (currentLineIndex > prevLines.length - 1) { + prevLines.push({message: '', lineLen: 0, timestamp: new Date()}); } + let [_addedCharacters, _currentCursorPosition] = writeOverLine(prevLines[currentLineIndex], chunk, currentCursorPosition) + charCount += _addedCharacters; + currentCursorPosition = _currentCursorPosition; + } + + if (i < chunks.length - 1) { + let [_addedCharacters, _currentCursorPosition] = writeOverLine(prevLines[currentLineIndex], separator, currentCursorPosition) + charCount += _addedCharacters; + currentCursorPosition = _currentCursorPosition; } } - prevLines.splice(prevLines.length - 1, 1, ...linesToAdd); - return [prevLines, charCount]; + return [prevLines, charCount, currentLineIndex, currentCursorPosition, overflow] } export function truncateLines( lines: Line[], charCount: number, + currentLineIndex: number | null, + currentCursorPosition: number, maxCharacters: number = SerialMonitorOutput.MAX_CHARACTERS -): [Line[], number] { +): [Line[], number, number | null, number] { let charsToDelete = charCount - maxCharacters; let lineIndex = 0; while (charsToDelete > 0 || lineIndex > 0) { @@ -65,5 +99,5 @@ export function truncateLines( charsToDelete -= deletedCharsCount; lines[0].message = newFirstLine; } - return [lines, charCount]; + return [lines, charCount, currentLineIndex, currentCursorPosition]; } diff --git a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx index 2ddd2c565..df3c7fb48 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx @@ -26,6 +26,9 @@ export class SerialMonitorOutput extends React.Component< lines: [], timestamp: this.props.monitorModel.timestamp, charCount: 0, + lineIndex: null, + cursorPosition: 0, + overflow: null }; } @@ -57,22 +60,32 @@ export class SerialMonitorOutput extends React.Component< this.scrollToBottom(); this.toDisposeBeforeUnmount.pushAll([ this.props.monitorManagerProxy.onMessagesReceived(({ messages }) => { - const [newLines, totalCharCount] = messagesToLines( - messages, - this.state.lines, - this.state.charCount - ); - const [lines, charCount] = truncateLines(newLines, totalCharCount); - this.setState( - { - lines, - charCount, - }, - () => this.scrollToBottom() - ); + if(Symbol.iterator in Object(messages)) { + if (this.state.overflow) { + messages[0] = this.state.overflow + messages[0]; + } + const [newLines, totalCharCount, cLineIndex, cCursorPosition, overflow] = messagesToLines( + messages, + this.state.lines, + this.state.charCount, + this.state.lineIndex, + this.state.cursorPosition, + ); + const [lines, charCount, lineIndex, cursorPosition] = truncateLines(newLines, totalCharCount, cLineIndex, cCursorPosition); + this.setState( + { + lines, + charCount, + lineIndex, + cursorPosition, + overflow + }, + () => this.scrollToBottom() + ); + } }), this.props.clearConsoleEvent(() => - this.setState({ lines: [], charCount: 0 }) + this.setState({ lines: [], charCount: 0, lineIndex: null, cursorPosition: 0, overflow: null }) ), this.props.monitorModel.onChange(({ property }) => { if (property === 'timestamp') { @@ -137,6 +150,9 @@ export namespace SerialMonitorOutput { lines: Line[]; timestamp: boolean; charCount: number; + lineIndex: number | null; + cursorPosition: number; + overflow: string | null; } export interface SelectOption { diff --git a/arduino-ide-extension/src/test/browser/monitor-utils.test.ts b/arduino-ide-extension/src/test/browser/monitor-utils.test.ts index cf1025740..f414bd12e 100644 --- a/arduino-ide-extension/src/test/browser/monitor-utils.test.ts +++ b/arduino-ide-extension/src/test/browser/monitor-utils.test.ts @@ -15,17 +15,20 @@ type TestLine = { charCount: number; maxCharacters?: number; }; + lineIndex?: number; + cursorPosition?: number; + overflow?: string; }; const date = new Date(); const testLines: TestLine[] = [ { messages: ['Hello'], - expected: { lines: [{ message: 'Hello', lineLen: 5 }], charCount: 5 }, + expected: { lines: [{ message: 'Hello', lineLen: 5 }], charCount: 5 } }, { messages: ['Hello', 'Dog!'], - expected: { lines: [{ message: 'HelloDog!', lineLen: 9 }], charCount: 9 }, + expected: { lines: [{ message: 'HelloDog!', lineLen: 9 }], charCount: 9 } }, { messages: ['Hello\n', 'Dog!'], @@ -35,7 +38,7 @@ const testLines: TestLine[] = [ { message: 'Dog!', lineLen: 4 }, ], charCount: 10, - }, + } }, { messages: ['Dog!'], @@ -46,7 +49,7 @@ const testLines: TestLine[] = [ { message: 'Dog!', lineLen: 4 }, ], charCount: 10, - }, + } }, { messages: [' Dog!\n', " Who's a good ", 'boy?\n', "You're a good boy!"], @@ -66,7 +69,7 @@ const testLines: TestLine[] = [ { message: '?\n', lineLen: 2 }, { message: "You're a good boy!", lineLen: 8 }, ], - }, + } }, { messages: ['boy?\n', "You're a good boy!"], @@ -92,7 +95,7 @@ const testLines: TestLine[] = [ { message: '?\n', lineLen: 2 }, { message: "You're a good boy!", lineLen: 8 }, ], - }, + } }, { messages: ["Who's a good boy?\n", 'Yo'], @@ -115,7 +118,138 @@ const testLines: TestLine[] = [ { message: "Who's a good boy?\n", lineLen: 18 }, { message: 'Yo', lineLen: 2 }, ], - }, + } + }, + + { + messages: ['Dog!'], + prevLines: { lines: [{ message: 'Hello\n', lineLen: 6 }], charCount: 6 }, + expected: { + lines: [ + { message: 'Hello\n', lineLen: 6 }, + { message: 'Dog!', lineLen: 4 }, + ], + charCount: 10, + } + }, + { + messages: ['\n'], + prevLines: { lines: [ + { message: 'Hello', lineLen: 5 }, + ], charCount: 5 }, + expected: { + lines: [ + { message: 'Hello\n', lineLen: 6 }, + ], + charCount: 6, + } + }, + { + messages: ['\n', '\x1B[H', 'Are', '\nYou'], + prevLines: { lines: [ + { message: 'Hello\n', lineLen: 6 }, + { message: 'Dog!\n', lineLen: 5 }, + { message: 'How', lineLen: 3 }, + ], charCount: 14 }, + expected: { + lines: [ + { message: 'Are\no\n', lineLen: 6 }, + { message: 'You!\n', lineLen: 5 }, + { message: 'How\n', lineLen: 4 }, + ], + charCount: 15, + } + }, + { + messages: ['Yes\x1B[HNo'], + prevLines: { lines: [ + { message: 'Hello\n', lineLen: 6 }, + { message: 'Dog!\n', lineLen: 5 }, + { message: 'How', lineLen: 3 }, + ], charCount: 14 }, + cursorPosition: 1, + lineIndex: 2, + expected: { + lines: [ + { message: 'Nollo\n', lineLen: 6 }, + { message: 'Dog!\n', lineLen: 5 }, + { message: 'HYes', lineLen: 4 }, + ], + charCount: 15, + } + }, + { + messages: ['dy', '\x1B', '[H', 'Reset'], + prevLines: { lines: [ + { message: 'Hello\n', lineLen: 6 }, + { message: 'Dog!\n', lineLen: 5 }, + { message: 'How', lineLen: 3 }, + ], charCount: 14 }, + expected: { + lines: [ + { message: 'Reset\n', lineLen: 6 }, + { message: 'Dog!\n', lineLen: 5 }, + { message: 'Howdy', lineLen: 5 }, + ], + charCount: 16, + } + }, + { + messages: ['HReset'], + prevLines: { lines: [ + { message: 'Hello\n', lineLen: 6 }, + { message: 'Dog!\n', lineLen: 5 }, + { message: 'How', lineLen: 3 }, + ], charCount: 14 }, + overflow: '\x1B[', + expected: { + lines: [ + { message: 'Reset\n', lineLen: 6 }, + { message: 'Dog!\n', lineLen: 5 }, + { message: 'How', lineLen: 3 }, + ], + charCount: 14, + } + }, + { + messages: ['\x1B[H', 'Reset', '\x1B[H', 'Me', '\x1B'], + prevLines: { lines: [ + { message: 'Hello', lineLen: 6 }, + ], charCount: 6 }, + expected: { + lines: [ + { message: 'Meset', lineLen: 6 }, + ], + charCount: 6, + } + }, + { + messages: ['HReset', 'Clear \x1B[2J', 'Me'], + prevLines: { lines: [ + { message: 'Hello\n', lineLen: 6 }, + { message: 'Dog!\n', lineLen: 5 }, + { message: 'How', lineLen: 3 }, + ], charCount: 14 }, + overflow: '\x1B[', + expected: { + lines: [ + { message: 'Me', lineLen: 2 }, + ], + charCount: 2, + } + }, + { + messages: ['2JReset'], + prevLines: { lines: [ + { message: 'How', lineLen: 3 }, + ], charCount: 3 }, + overflow: '\x1B[', + expected: { + lines: [ + { message: 'Reset', lineLen: 5 }, + ], + charCount: 5, + } }, ]; @@ -137,10 +271,18 @@ describe('Monitor Utils', () => { testLines.forEach((testLine) => { context('when converting messages', () => { it('should give the right result', () => { - const [newLines, addedCharCount] = messagesToLines( + const lineIndex = testLine.lineIndex || testLine.prevLines ? testLine.prevLines!.lines.length - 1 : null + const cursorPosition = testLine.cursorPosition || testLine.prevLines?.lines[testLine.prevLines?.lines.length - 1].message.length || 0; + + if (testLine.overflow) { + testLine.messages[0] = testLine.overflow + testLine.messages[0] + } + const [newLines, addedCharCount, cLineIndex, cCursorPosition] = messagesToLines( testLine.messages, testLine.prevLines?.lines, - testLine.prevLines?.charCount + testLine.prevLines?.charCount, + lineIndex, + cursorPosition ); newLines.forEach((line, index) => { expect(line.message).to.equal(testLine.expected.lines[index].message); @@ -153,6 +295,8 @@ describe('Monitor Utils', () => { const [truncatedLines, totalCharCount] = truncateLines( newLines, addedCharCount, + cLineIndex, + cCursorPosition, testLine.expectedTruncated?.maxCharacters ); let charCount = 0;