diff --git a/src/common/filetype/filetype.ts b/src/common/filetype/filetype.ts index 730f3b1..dd4c8de 100644 --- a/src/common/filetype/filetype.ts +++ b/src/common/filetype/filetype.ts @@ -1,4 +1,5 @@ import { node_path as path } from '../runtime/mod.ts'; +import Option, { None, Some } from '../option.ts'; // ---------------------------------------------------------------------------- // File-related types @@ -18,6 +19,7 @@ export interface HighlightingOptions { interface IFileType { readonly name: FileLang; + readonly singleLineComment: Option; readonly hlOptions: HighlightingOptions; get flags(): HighlightingOptions; } @@ -27,6 +29,7 @@ interface IFileType { */ export abstract class AbstractFileType implements IFileType { public readonly name: FileLang = FileLang.Plain; + public readonly singleLineComment = None; public readonly hlOptions: HighlightingOptions = { numbers: false, strings: false, @@ -47,6 +50,7 @@ const defaultHighlightOptions: HighlightingOptions = { class TypeScriptFile extends AbstractFileType { public readonly name: FileLang = FileLang.TypeScript; + public readonly singleLineComment = Some('//'); public readonly hlOptions: HighlightingOptions = { ...defaultHighlightOptions, }; @@ -54,6 +58,7 @@ class TypeScriptFile extends AbstractFileType { class JavaScriptFile extends AbstractFileType { public readonly name: FileLang = FileLang.JavaScript; + public readonly singleLineComment = Some('//'); public readonly hlOptions: HighlightingOptions = { ...defaultHighlightOptions, }; @@ -61,6 +66,7 @@ class JavaScriptFile extends AbstractFileType { class CSSFile extends AbstractFileType { public readonly name: FileLang = FileLang.CSS; + public readonly singleLineComment = None; public readonly hlOptions: HighlightingOptions = { ...defaultHighlightOptions, }; diff --git a/src/common/highlight.ts b/src/common/highlight.ts index d4393b9..ce7221a 100644 --- a/src/common/highlight.ts +++ b/src/common/highlight.ts @@ -5,6 +5,7 @@ export enum HighlightType { Number, Match, String, + SingleLineComment, } export function highlightToColor(type: HighlightType): string { @@ -18,6 +19,9 @@ export function highlightToColor(type: HighlightType): string { case HighlightType.String: return Ansi.color256(201); + case HighlightType.SingleLineComment: + return Ansi.color256(45); + default: return Ansi.ResetFormatting; } diff --git a/src/common/row.ts b/src/common/row.ts index 44add61..3dc4242 100644 --- a/src/common/row.ts +++ b/src/common/row.ts @@ -225,94 +225,141 @@ export class Row { } public highlight(word: Option, syntax: FileType): void { - const highlighting = []; + this.hl = []; + + let i = 0; + for (; i < this.rsize;) { + const ch = this.rchars[i]; + + const maybeNext = this.highlightComment(i, syntax, ch) + .orElse(() => this.highlightString(i, syntax, ch)) + .orElse(() => this.highlightNumber(i, syntax, ch)); + + if (maybeNext.isSome()) { + const next = maybeNext.unwrap(); + if (next < this.rsize) { + i = maybeNext.unwrap(); + continue; + } + + break; + } + this.hl.push(HighlightType.None); + i += 1; + } + + this.highlightMatch(word); + } + + protected highlightMatch(word: Option): void { let searchIndex = 0; - const matches = []; // Find matches for the current search if (word.isSome()) { while (true) { - const match = this.find(word.unwrap(), searchIndex); + const match = this.find( + word.unwrap(), + searchIndex, + SearchDirection.Forward, + ); if (match.isNone()) { break; } - matches.push(match.unwrap()); - const nextPossible = match.unwrap() + strlen(word.unwrap()); + const index = match.unwrap(); + const nextPossible = index + strlen(word.unwrap()); if (nextPossible < this.rsize) { + let i = index; + for (const _ in strChars(word.unwrap())) { + this.hl[i] = HighlightType.Match; + i += 1; + } + searchIndex = nextPossible; } else { break; } } } + } - let prevIsSeparator = true; - let inString: string | boolean = false; - let i = 0; - for (; i < this.rsize;) { - const ch = this.rchars[i]; - const prevHighlight = (i > 0) ? highlighting[i - 1] : HighlightType.None; - - // Highlight search matches - if (word.isSome()) { - if (matches.includes(i)) { - for (const _ in strChars(word.unwrap())) { - i += 1; - highlighting.push(HighlightType.Match); - } - - continue; + protected highlightComment( + i: number, + syntax: FileType, + _ch: string, + ): Option { + // Highlight single-line comments + if (syntax.singleLineComment.isSome()) { + const commentStart = syntax.singleLineComment.unwrap(); + if ( + this.toString().indexOf(commentStart) === this.charIndexToByteIndex(i) + ) { + for (; i < this.rsize; i++) { + this.hl.push(HighlightType.SingleLineComment); } + + return Some(i); } - - // Highlight strings - if (syntax.flags.strings) { - if (inString) { - highlighting.push(HighlightType.String); - - // Handle escaped characters in strings - if (ch === '\\' && i < this.rsize + 1) { - highlighting.push(HighlightType.String); - i += 2; - continue; - } - - if (ch === inString) { - inString = false; - } - i += 1; - prevIsSeparator = true; - continue; - } else if (prevIsSeparator && ch === '"' || ch === "'") { - highlighting.push(HighlightType.String); - inString = ch; - prevIsSeparator = true; - i += 1; - continue; - } - } - - // Highlight numbers - if (syntax.flags.numbers) { - const isNumeric = isAsciiDigit(ch) && (prevIsSeparator || - prevHighlight === HighlightType.Number); - const isDecimalNumeric = ch === '.' && - prevHighlight === HighlightType.Number; - const isHexNumeric = ch === 'x' && - prevHighlight === HighlightType.Number; - if (isNumeric || isDecimalNumeric || isHexNumeric) { - highlighting.push(HighlightType.Number); - } else { - highlighting.push(HighlightType.None); - } - } - - prevIsSeparator = isSeparator(ch); - i += 1; } - this.hl = highlighting; + return None; + } + + protected highlightString( + i: number, + syntax: FileType, + ch: string, + ): Option { + // Highlight strings + if (syntax.flags.strings && ch === '"' || ch === "'") { + while (true) { + this.hl.push(HighlightType.String); + i += 1; + if (i === this.rsize) { + break; + } + + const nextChar = this.rchars[i]; + if (nextChar === ch) { + break; + } + } + this.hl.push(HighlightType.String); + i += 1; + return Some(i); + } + + return None; + } + + protected highlightNumber( + i: number, + syntax: FileType, + ch: string, + ): Option { + // Highlight numbers + if (syntax.flags.numbers && isAsciiDigit(ch)) { + if (i > 0 && !isSeparator(this.rchars[i - 1])) { + return None; + } + + while (true) { + this.hl.push(HighlightType.Number); + i += 1; + if (i < this.rsize) { + const nextChar = this.rchars[i]; + if (nextChar !== '.' && nextChar !== 'x' && !isAsciiDigit(nextChar)) { + break; + } + } else { + break; + } + } + + return Some(i); + } + + return None; } public render(offset: number, len: number): string {