import Ansi, { KeyCommand } from './ansi.ts'; import Buffer from './buffer.ts'; import Document, { Row } from './document.ts'; import { IPoint, ITerminalSize, logToFile, maxAdd, truncate, VERSION, } from './mod.ts'; import { ctrlKey, posSub } from './utils.ts'; export class Editor { /** * The output buffer for the terminal * @private */ #buffer: Buffer; /** * The size of the screen in rows/columns * @private */ #screen: ITerminalSize; /** * The current location of the mouse cursor * @private */ #cursor: IPoint; /** * The current scrolling offset */ #offset: IPoint; /** * The document being edited * @private */ #document: Document; /** * The scrolling offset for the rendered row * @private */ #render: IPoint; /** * The name of the currently open file * @private */ #filename: string; constructor(terminalSize: ITerminalSize) { this.#buffer = new Buffer(); this.#screen = terminalSize; this.#screen.rows -= 1; this.#cursor = { x: 0, y: 0, }; this.#offset = { x: 0, y: 0, }; this.#render = { x: 0, y: 0, }; this.#document = Document.empty(); this.#filename = ''; } private get currentRow(): Row | null { return this.#document.row(this.#cursor.y); } public async open(filename: string): Promise { await this.#document.open(filename); this.#filename = filename; return this; } // -------------------------------------------------------------------------- // Command/input mapping // -------------------------------------------------------------------------- /** * Determine what to do based on input * @param input - the decoded chunk of stdin */ public processKeyPress(input: string): boolean { switch (input) { case ctrlKey('q'): this.clearScreen().then(() => {}); return false; case KeyCommand.Home: this.#cursor.x = 0; break; case KeyCommand.End: if (this.currentRow !== null) { this.#cursor.x = this.currentRow.size - 1; } break; case KeyCommand.PageUp: case KeyCommand.PageDown: { if (input === KeyCommand.PageUp) { this.#cursor.y = this.#offset.y; } else if (input === KeyCommand.PageDown) { this.#cursor.y = maxAdd( this.#offset.y, this.#screen.rows - 1, this.#document.numRows, ); } let times = this.#screen.rows; while (times--) { this.moveCursor( input === KeyCommand.PageUp ? KeyCommand.ArrowUp : KeyCommand.ArrowDown, ); } } break; case KeyCommand.ArrowUp: case KeyCommand.ArrowDown: case KeyCommand.ArrowRight: case KeyCommand.ArrowLeft: this.moveCursor(input); break; } return true; } private moveCursor(char: string): void { switch (char) { case KeyCommand.ArrowLeft: if (this.#cursor.x > 0) { this.#cursor.x--; } else if (this.#cursor.y > 0) { this.#cursor.y--; this.#cursor.x = (this.currentRow !== null) ? this.currentRow.size - 1 : 0; } break; case KeyCommand.ArrowRight: if ( this.currentRow !== null && this.#cursor.x < this.currentRow.size - 1 ) { this.#cursor.x++; } else if ( this.currentRow !== null && this.#cursor.x === this.currentRow.size - 1 ) { this.#cursor.y++; this.#cursor.x = 0; } break; case KeyCommand.ArrowUp: if (this.#cursor.y > 0) { this.#cursor.y--; } break; case KeyCommand.ArrowDown: if (this.#cursor.y < this.#document.numRows) { this.#cursor.y++; } break; } const rowLen = this.currentRow?.size ?? 0; if (this.#cursor.x > rowLen) { this.#cursor.x = rowLen; } } private scroll(): void { this.#render.x = 0; if (this.currentRow !== null) { this.#render.x = this.currentRow.cxToRx(this.#cursor.x); } if (this.#cursor.y < this.#offset.y) { this.#offset.y = this.#cursor.y; } if (this.#cursor.y >= this.#offset.y + this.#screen.rows) { this.#offset.y = this.#cursor.y - this.#screen.rows + 1; } if (this.#render.x < this.#offset.x) { this.#offset.x = this.#render.x; } if (this.#render.x >= this.#offset.x + this.#screen.cols) { this.#offset.x = this.#render.x - this.#screen.cols + 1; } } // -------------------------------------------------------------------------- // Terminal Output / Drawing // -------------------------------------------------------------------------- /** * Clear the screen and write out the buffer */ public async refreshScreen(): Promise { this.scroll(); this.#buffer.append(Ansi.HideCursor); this.#buffer.append(Ansi.ResetCursor); this.drawRows(); this.drawStatusBar(); this.#buffer.append( Ansi.moveCursor( this.#cursor.y - this.#offset.y, this.#render.x - this.#offset.x, ), ); this.#buffer.append(Ansi.ShowCursor); await this.#buffer.flush(); } private async clearScreen(): Promise { this.#buffer.append(Ansi.ClearScreen); this.#buffer.append(Ansi.ResetCursor); await this.#buffer.flush(); } private drawRows(): void { for (let y = 0; y < this.#screen.rows; y++) { this.#buffer.append(Ansi.ClearLine); const filerow = y + this.#offset.y; if (filerow >= this.#document.numRows) { this.drawPlaceholderRow(filerow); } else { this.drawFileRow(filerow); } this.#buffer.appendLine(); } } private drawFileRow(y: number): void { const row = this.#document.row(y); if (row === null) { logToFile(`Warning: trying to draw non-existent row '${y}'`); return this.drawPlaceholderRow(y); } const len = Math.min( posSub(row.rsize, this.#offset.x), this.#screen.cols, ); this.#buffer.append(row.rstring(this.#offset.x), len); } private drawPlaceholderRow(y: number): void { if (y === Math.trunc(this.#screen.rows / 2) && this.#document.isEmpty()) { const message = `Kilo editor -- version ${VERSION}`; const messageLen = (message.length > this.#screen.cols) ? this.#screen.cols : message.length; let padding = Math.trunc((this.#screen.cols - messageLen) / 2); if (padding > 0) { this.#buffer.append('~'); padding -= 1; this.#buffer.append(' '.repeat(padding)); } this.#buffer.append(message, messageLen); } else { this.#buffer.append('~'); } } private drawStatusBar(): void { this.#buffer.append(Ansi.InvertColor); const name = (this.#filename !== '') ? this.#filename : '[No Name]'; const status = `${truncate(name, 20)} - ${this.#document.numRows} lines`; const rstatus = `${this.#cursor.y + 1}/${this.#document.numRows}`; let len = Math.min(status.length, this.#screen.cols); this.#buffer.append(status, len); while (len < this.#screen.cols) { if (this.#screen.cols - len === rstatus.length) { this.#buffer.append(rstatus); break; } else { this.#buffer.append(' '); len++; } } this.#buffer.append(Ansi.ResetFormatting); } }