feat(editor): Shiki syntax highlighting in the file editor (CodeMirror)

Bring the CodeMirror file editor up to the same rich highlighting as the Shiki
file view, so toggling edit <-> view is visually consistent. lezer collapses far
more tokens than TextMate (import/from/const are all "keyword"), so a theme
remap can't reach parity — instead, project real Shiki tokens onto decorations.

- Worker: add highlightTokens — tokenize with an arbitrary registered TextMate
  theme and return per-line styled runs with offsets. The theme object ships to
  the worker once per name; later calls send only the name.
- New shikiHighlight CodeMirror extension: a StateField of mark decorations
  built from worker tokens. Re-tokenizes on a short idle (off the keystroke
  path) and maps decorations through edits so colors persist while typing.
- flexokiTheme: add { syntaxColors: false } to keep only the editor UI theme,
  so the lezer highlighter doesn't compete with the Shiki decorations.
- FilesView: enable Shiki highlighting (same language resolver as the file view
  → identical language) and drop lezer token colors when it's active. lezer
  language stays on for indentation/folding/brackets.
This commit is contained in:
Bohdan Triapitsyn
2026-06-16 01:08:54 +03:00
parent c22e1cbb15
commit 782982cdd3
6 changed files with 232 additions and 5 deletions
+16 -2
View File
@@ -29,6 +29,8 @@ import { JsonTreeView } from '@/components/ui/JsonTreeView';
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
import { languageByExtension, loadLanguageByExtension } from '@/lib/codemirror/languageByExtension';
import { createFlexokiCodeMirrorTheme } from '@/lib/codemirror/flexokiTheme';
import { shikiHighlightExtension } from '@/lib/codemirror/shikiHighlight';
import { getResolvedShikiTheme } from '@/lib/shiki/appThemeRegistry';
import { File as PierreFile } from '@pierre/diffs/react';
import {
Dialog,
@@ -2824,11 +2826,23 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
return [createFlexokiCodeMirrorTheme(currentTheme)];
}
const extensions = [createFlexokiCodeMirrorTheme(currentTheme)];
// Shiki token colors (worker-backed) match the Shiki file view exactly.
// Same language resolver as the view, so both agree on the language. When
// Shiki is the color source, drop the lezer token colors to avoid a
// competing highlighter (keep the lezer language for indentation/folding).
const shikiLanguage = getLanguageFromExtension(selectedFile.path);
const extensions = [createFlexokiCodeMirrorTheme(currentTheme, shikiLanguage ? { syntaxColors: false } : undefined)];
const language = staticLanguageExtension ?? dynamicLanguageExtension;
if (language) {
extensions.push(language);
}
if (shikiLanguage) {
extensions.push(shikiHighlightExtension({
language: shikiLanguage,
themeName: currentTheme.metadata.id,
theme: getResolvedShikiTheme(currentTheme),
}));
}
if (wrapLines) {
extensions.push(EditorView.lineWrapping);
}
@@ -2939,7 +2953,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}, [pdfAssetAuthKey, t]);
const isPdfAssetAuthLoading = Boolean(pdfAssetAuthKey && pdfAssetAuthReadyKey !== pdfAssetAuthKey);
const imageSrc = selectedFile?.path && isSelectedImage
? (runtime.isDesktop
? (isSelectedSvg