Highlight single-line comments, and refactor highlighting method
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-07-16 17:18:09 -04:00
parent 32e3676b02
commit 359e739fe8
3 changed files with 124 additions and 67 deletions

View File

@ -1,4 +1,5 @@
import { node_path as path } from '../runtime/mod.ts'; import { node_path as path } from '../runtime/mod.ts';
import Option, { None, Some } from '../option.ts';
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// File-related types // File-related types
@ -18,6 +19,7 @@ export interface HighlightingOptions {
interface IFileType { interface IFileType {
readonly name: FileLang; readonly name: FileLang;
readonly singleLineComment: Option<string>;
readonly hlOptions: HighlightingOptions; readonly hlOptions: HighlightingOptions;
get flags(): HighlightingOptions; get flags(): HighlightingOptions;
} }
@ -27,6 +29,7 @@ interface IFileType {
*/ */
export abstract class AbstractFileType implements IFileType { export abstract class AbstractFileType implements IFileType {
public readonly name: FileLang = FileLang.Plain; public readonly name: FileLang = FileLang.Plain;
public readonly singleLineComment = None;
public readonly hlOptions: HighlightingOptions = { public readonly hlOptions: HighlightingOptions = {
numbers: false, numbers: false,
strings: false, strings: false,
@ -47,6 +50,7 @@ const defaultHighlightOptions: HighlightingOptions = {
class TypeScriptFile extends AbstractFileType { class TypeScriptFile extends AbstractFileType {
public readonly name: FileLang = FileLang.TypeScript; public readonly name: FileLang = FileLang.TypeScript;
public readonly singleLineComment = Some('//');
public readonly hlOptions: HighlightingOptions = { public readonly hlOptions: HighlightingOptions = {
...defaultHighlightOptions, ...defaultHighlightOptions,
}; };
@ -54,6 +58,7 @@ class TypeScriptFile extends AbstractFileType {
class JavaScriptFile extends AbstractFileType { class JavaScriptFile extends AbstractFileType {
public readonly name: FileLang = FileLang.JavaScript; public readonly name: FileLang = FileLang.JavaScript;
public readonly singleLineComment = Some('//');
public readonly hlOptions: HighlightingOptions = { public readonly hlOptions: HighlightingOptions = {
...defaultHighlightOptions, ...defaultHighlightOptions,
}; };
@ -61,6 +66,7 @@ class JavaScriptFile extends AbstractFileType {
class CSSFile extends AbstractFileType { class CSSFile extends AbstractFileType {
public readonly name: FileLang = FileLang.CSS; public readonly name: FileLang = FileLang.CSS;
public readonly singleLineComment = None;
public readonly hlOptions: HighlightingOptions = { public readonly hlOptions: HighlightingOptions = {
...defaultHighlightOptions, ...defaultHighlightOptions,
}; };

View File

@ -5,6 +5,7 @@ export enum HighlightType {
Number, Number,
Match, Match,
String, String,
SingleLineComment,
} }
export function highlightToColor(type: HighlightType): string { export function highlightToColor(type: HighlightType): string {
@ -18,6 +19,9 @@ export function highlightToColor(type: HighlightType): string {
case HighlightType.String: case HighlightType.String:
return Ansi.color256(201); return Ansi.color256(201);
case HighlightType.SingleLineComment:
return Ansi.color256(45);
default: default:
return Ansi.ResetFormatting; return Ansi.ResetFormatting;
} }

View File

@ -225,94 +225,141 @@ export class Row {
} }
public highlight(word: Option<string>, syntax: FileType): void { public highlight(word: Option<string>, 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<string>): void {
let searchIndex = 0; let searchIndex = 0;
const matches = [];
// Find matches for the current search // Find matches for the current search
if (word.isSome()) { if (word.isSome()) {
while (true) { while (true) {
const match = this.find(word.unwrap(), searchIndex); const match = this.find(
word.unwrap(),
searchIndex,
SearchDirection.Forward,
);
if (match.isNone()) { if (match.isNone()) {
break; break;
} }
matches.push(match.unwrap()); const index = match.unwrap();
const nextPossible = match.unwrap() + strlen(word.unwrap()); const nextPossible = index + strlen(word.unwrap());
if (nextPossible < this.rsize) { if (nextPossible < this.rsize) {
let i = index;
for (const _ in strChars(word.unwrap())) {
this.hl[i] = HighlightType.Match;
i += 1;
}
searchIndex = nextPossible; searchIndex = nextPossible;
} else { } else {
break; break;
} }
} }
} }
}
let prevIsSeparator = true; protected highlightComment(
let inString: string | boolean = false; i: number,
let i = 0; syntax: FileType,
for (; i < this.rsize;) { _ch: string,
const ch = this.rchars[i]; ): Option<number> {
const prevHighlight = (i > 0) ? highlighting[i - 1] : HighlightType.None; // Highlight single-line comments
if (syntax.singleLineComment.isSome()) {
// Highlight search matches const commentStart = syntax.singleLineComment.unwrap();
if (word.isSome()) { if (
if (matches.includes(i)) { this.toString().indexOf(commentStart) === this.charIndexToByteIndex(i)
for (const _ in strChars(word.unwrap())) { ) {
i += 1; for (; i < this.rsize; i++) {
highlighting.push(HighlightType.Match); this.hl.push(HighlightType.SingleLineComment);
}
continue;
} }
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<number> {
// 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<number> {
// 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 { public render(offset: number, len: number): string {