* feat(files): edit workspace files on web Keep source buffers synchronized with host file changes and require an explicit overwrite or reload when revisions diverge. * feat(panels): surface and protect modified tabs Expose tooltip and modification state through the generic panel boundary so tabs can show stable metadata and guard every close route consistently. * fix(tests): use portable fake timeout handle * fix(files): harden editor conflict handling Preserve modified panel state across tab eviction, use precise revisions for optimistic writes, coalesce concurrent file watchers, and localize the editor interface. * fix(files): close editor concurrency gaps Coalesce clean reloads, preserve subscriber identities and file permissions, suspend pending saves during close confirmation, and carry precise revisions through file reads. * test(files): expect read revision metadata
70 lines
2.0 KiB
TypeScript
70 lines
2.0 KiB
TypeScript
import { highlightTree } from "@lezer/highlight";
|
|
import type { HighlightStyle, HighlightToken } from "./types.js";
|
|
import { getParserForFile } from "./parsers.js";
|
|
|
|
import { staticSyntaxHighlighter } from "./syntax-roles.js";
|
|
|
|
export function highlightCode(code: string, filename: string): HighlightToken[][] {
|
|
const parser = getParserForFile(filename);
|
|
|
|
if (!parser) {
|
|
return code.split("\n").map((line) => [{ text: line, style: null }]);
|
|
}
|
|
|
|
const tree = parser.parse(code);
|
|
const lines = code.split("\n");
|
|
const result: HighlightToken[][] = [];
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
result.push([]);
|
|
}
|
|
|
|
// Build a map of character positions to styles
|
|
const styleMap: Array<HighlightStyle | null> = Array.from({ length: code.length }, () => null);
|
|
|
|
highlightTree(tree, staticSyntaxHighlighter, (from, to, classes) => {
|
|
for (let i = from; i < to && i < styleMap.length; i++) {
|
|
styleMap[i] = classes as HighlightStyle;
|
|
}
|
|
});
|
|
|
|
// Convert style map to tokens per line
|
|
let pos = 0;
|
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
const line = lines[lineIndex];
|
|
|
|
if (line.length === 0) {
|
|
result[lineIndex].push({ text: "", style: null });
|
|
pos++; // skip newline
|
|
continue;
|
|
}
|
|
|
|
let currentToken: HighlightToken = { text: "", style: styleMap[pos] };
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
const charStyle = styleMap[pos + i];
|
|
if (charStyle === currentToken.style) {
|
|
currentToken.text += line[i];
|
|
} else {
|
|
if (currentToken.text) {
|
|
result[lineIndex].push(currentToken);
|
|
}
|
|
currentToken = { text: line[i], style: charStyle };
|
|
}
|
|
}
|
|
|
|
if (currentToken.text) {
|
|
result[lineIndex].push(currentToken);
|
|
}
|
|
|
|
pos += line.length + 1; // +1 for newline
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function highlightLine(line: string, filename: string): HighlightToken[] {
|
|
const result = highlightCode(line, filename);
|
|
return result[0] ?? [{ text: line, style: null }];
|
|
}
|