Skip to content

Commit ae25d5c

Browse files
faiyaz26Copilot
andcommitted
feat: add custom language support with preset and grammar-based highlighting
Add a Languages tab to Settings for enabling syntax highlighting for uncommon languages that Shiki doesn't support by default. - Add CustomLanguageConfig type to editor types with support for two modes: alias (map extensions to an existing Shiki language) and custom grammar (paste a TextMate JSON for full proper highlighting) - Add customLanguages[] to EditorSettingsManager (localStorage) - Add CustomLanguageManager singleton service that reactively syncs settings changes into the Shiki detectLanguage registry, loads custom TextMate grammars into the Shiki highlighter on demand - Simplify FileViewer ensureLanguageLoaded — remove hardcoded whitelist and instead try any language ID, falling back to text on error; this also enables all Shiki-bundled languages (terraform, nix, etc.) - Add Languages settings tab with: - Preset grid: Terraform/HCL, Protocol Buffers, GraphQL, Nix, Solidity, Elixir, Dart, LookML — one-click enable for alias presets - Custom language list with edit/delete actions - Add/Edit form with mode toggle (alias vs TextMate grammar), scopeName input, and JSON textarea with guidance links Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f63c91d commit ae25d5c

9 files changed

Lines changed: 676 additions & 20 deletions

File tree

frontend/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { terminalPool } from './services/TerminalPool';
1717
import { agentNotificationService } from './services/AgentNotificationService';
1818
import { agentStatusManager } from './services/AgentStatusManager';
1919
import { hookService } from './services/HookService';
20+
// Eagerly initialize so custom language detection is ready before any file opens
21+
import './services/CustomLanguageManager';
2022

2123
import { useClipboardFix } from './hooks/useClipboardFix';
2224
import { useGlobalContextMenuFix } from './hooks/useGlobalContextMenuFix';

frontend/src/components/editor/FileViewer.tsx

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getLanguageDisplayName, getShikiLanguage, isMarkdownFile } from './type
77
import { themeManager, getShikiTheme, getAllShikiThemes } from '../../services/ThemeManager';
88
import { keyboardShortcutManager } from '../../services/KeyboardShortcutManager';
99
import { editorStateManager } from '../../services/EditorStateManager';
10+
import { customLanguageManager } from '../../services/CustomLanguageManager';
1011

1112
// Lazy load MarkdownEditor to avoid circular dependency issues at startup
1213
const MarkdownEditor = lazy(() => import('./markdown').then(m => ({ default: m.MarkdownEditor })));
@@ -26,28 +27,26 @@ async function getHighlighter(): Promise<Highlighter> {
2627
return highlighterPromise;
2728
}
2829

29-
// Ensure a language is loaded
30+
// Ensure a language is loaded into the Shiki highlighter.
31+
// Tries to load any language ID — handles built-in Shiki languages, custom TextMate grammars,
32+
// and falls back to 'text' if the language can't be loaded.
3033
async function ensureLanguageLoaded(highlighter: Highlighter, lang: string): Promise<string> {
31-
const validLangs = [
32-
'javascript', 'jsx', 'typescript', 'tsx', 'html', 'css', 'scss', 'sass', 'less',
33-
'json', 'yaml', 'xml', 'toml', 'rust', 'python', 'go', 'java', 'c', 'cpp',
34-
'csharp', 'ruby', 'php', 'swift', 'kotlin', 'scala', 'shellscript', 'markdown',
35-
'sql', 'dockerfile', 'makefile', 'cmake', 'dotenv', 'text'
36-
];
34+
// Load any pending custom TextMate grammars first
35+
await customLanguageManager.loadGrammarsIntoHighlighter(highlighter);
3736

38-
const targetLang = validLangs.includes(lang) ? lang : 'text';
39-
40-
if (!loadedLanguages.has(targetLang) && targetLang !== 'text') {
41-
try {
42-
await highlighter.loadLanguage(targetLang as BundledLanguage);
43-
loadedLanguages.add(targetLang);
44-
} catch (e) {
45-
console.warn(`Failed to load language: ${targetLang}, falling back to text`);
46-
return 'text';
47-
}
37+
if (lang === 'text' || loadedLanguages.has(lang)) {
38+
return lang;
4839
}
4940

50-
return targetLang;
41+
try {
42+
// Works for BundledLanguage IDs and any custom grammar registered via loadLanguage()
43+
await highlighter.loadLanguage(lang as BundledLanguage);
44+
loadedLanguages.add(lang);
45+
return lang;
46+
} catch {
47+
console.warn(`[FileViewer] Language not available: ${lang}, falling back to text`);
48+
return 'text';
49+
}
5150
}
5251

5352
// ============ FOLDING ============

frontend/src/components/editor/types.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
// Editor module types
22

3+
// User-defined custom language configuration for uncommon syntaxes.
4+
// Stored in EditorSettings.customLanguages (localStorage).
5+
export interface CustomLanguageConfig {
6+
id: string; // Stable key: preset IDs start with "preset-", user IDs are UUIDs
7+
name: string; // Display name, e.g. "LookML"
8+
extensions: string[]; // File extensions without dot, e.g. ["lkml", "lookml"]
9+
aliasFor?: string; // Use an existing Shiki language (e.g. "terraform"). Mutually exclusive with grammar.
10+
scopeName?: string; // TextMate scope name, required when using grammar (e.g. "source.lookml")
11+
grammar?: string; // TextMate grammar JSON string, for languages Shiki doesn't bundle
12+
}
13+
14+
// Convert a language display name to a stable Shiki-safe slug for grammar-mode languages
15+
export function customLangSlug(name: string): string {
16+
return 'custom-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
17+
}
18+
19+
// Internal registry — updated by CustomLanguageManager when settings change.
20+
// Using a module-level variable avoids threading the list through every call site.
21+
let _customLangs: CustomLanguageConfig[] = [];
22+
23+
/** Called by CustomLanguageManager whenever the custom language list changes. */
24+
export function _setCustomLanguages(langs: CustomLanguageConfig[]): void {
25+
_customLangs = langs;
26+
}
27+
328
export interface OpenFile {
429
id: string;
530
path: string;
@@ -44,6 +69,13 @@ export interface EditorTab {
4469
export function detectLanguage(filename: string): string {
4570
const ext = filename.split('.').pop()?.toLowerCase() || '';
4671

72+
// Check user-defined custom languages before built-in mappings
73+
for (const lang of _customLangs) {
74+
if (lang.extensions.includes(ext)) {
75+
return lang.aliasFor ?? customLangSlug(lang.name);
76+
}
77+
}
78+
4779
const languageMap: Record<string, string> = {
4880
// JavaScript/TypeScript
4981
'js': 'javascript',
@@ -188,5 +220,5 @@ export function getShikiLanguage(language: string): string {
188220
'cmake': 'cmake',
189221
};
190222

191-
return shikiMap[language] || 'text';
223+
return shikiMap[language] || language; // Unknown IDs pass through — Shiki handles them or falls back
192224
}

0 commit comments

Comments
 (0)