/** * Highlight Command - Generate Tree-sitter highlights and editor scaffolds */ import { Command } from 'commander'; import chalk from 'chalk'; import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; import { join } from 'path'; /** * Create the highlight command */ export function createHighlightCommand(): Command { const highlightCommand = new Command('highlight'); highlightCommand .description('Generate Tree-sitter highlights and editor scaffolds (tree-sitter, neovim, emacs, vscode)') .action(async () => { if (!existsSync('grammar.js')) { console.error(chalk.red('❌ No grammar.js found. Are you in a DSL project directory?')); process.exit(1); } const languageName = getProjectName(); // Tree-sitter highlights const tsOutDir = 'generated/editors/tree-sitter'; mkdirSync(tsOutDir, { recursive: true }); const highlights = ` ; Minimal highlights scaffold. Extend as needed. ; Comments (comment) @comment ; Strings (string) @string ; Numbers (number) @number ; Keywords (example) [("if") ("else") ("let") ("function")] @keyword `; const tsFile = join(tsOutDir, 'highlights.scm'); writeFileSync(tsFile, highlights.trim() + '\n', 'utf-8'); console.log(chalk.green(`✅ Generated ${tsFile}`)); // Neovim instructions const nvimDir = 'generated/editors/neovim'; mkdirSync(nvimDir, { recursive: true }); const nvimMd = `# Neovim setup for ${languageName} 1. Copy queries to your runtimepath: queries/${languageName}/highlights.scm 2. Configure nvim-treesitter: \`\`\`lua require('nvim-treesitter.configs').setup({ ensure_installed = {}, highlight = { enable = true }, }) \`\`\` `; writeFileSync(join(nvimDir, 'setup-instructions.md'), nvimMd, 'utf-8'); console.log(chalk.green(`✅ Generated ${join(nvimDir, 'setup-instructions.md')}`)); // Emacs major mode (minimal) const emacsDir = 'generated/editors/emacs'; mkdirSync(emacsDir, { recursive: true }); const emacsEl = `;;; ${languageName}-mode.el --- ${languageName} mode -*- lexical-binding: t; -*- (require 'treesit) (define-derived-mode ${languageName}-mode prog-mode "${languageName}" "Major mode for ${languageName} using Tree-sitter." (when (treesit-ready-p '${languageName}) (treesit-parser-create '${languageName}))) (add-to-list 'auto-mode-alist '("\\.${languageName}$" . ${languageName}-mode)) (provide '${languageName}-mode) `; writeFileSync(join(emacsDir, `${languageName}-mode.el`), emacsEl, 'utf-8'); console.log(chalk.green(`✅ Generated ${join(emacsDir, `${languageName}-mode.el`)}`)); // VS Code: TextMate grammar + language configuration (basic placeholders) const vscodeSyntaxDir = 'generated/editors/vscode/syntaxes'; const vscodeDir = 'generated/editors/vscode'; mkdirSync(vscodeSyntaxDir, { recursive: true }); mkdirSync(vscodeDir, { recursive: true }); const scope = `source.${languageName}`; const tmLanguage = { name: languageName, scopeName: scope, patterns: [ { include: '#comment' }, { include: '#string' }, { include: '#number' }, { include: '#keyword' } ], repository: { comment: { patterns: [{ name: 'comment.line.double-slash', match: '//.*$' }] }, string: { patterns: [{ name: 'string.quoted.double', begin: '"', end: '"' }] }, number: { patterns: [{ name: 'constant.numeric', match: '-?\\b[0-9]+(\\.[0-9]+)?\\b' }] }, keyword: { patterns: [{ name: 'keyword.control', match: '\\b(if|else|let|function)\\b' }] } } } as const; writeFileSync(join(vscodeSyntaxDir, `${languageName}.tmLanguage.json`), JSON.stringify(tmLanguage, null, 2)); const langConfig = { comments: { lineComment: '//' }, brackets: [["{","}"],["[","]"],["(",")"]], autoClosingPairs: [ { open: '"', close: '"' }, { open: '{', close: '}' }, { open: '(', close: ')' }, { open: '[', close: ']' } ], surroundingPairs: [ { open: '"', close: '"' }, { open: '{', close: '}' }, { open: '(', close: ')' }, { open: '[', close: ']' } ] }; writeFileSync(join(vscodeDir, 'language-configuration.json'), JSON.stringify(langConfig, null, 2)); console.log(chalk.green(`✅ Generated VS Code syntax and configuration`)); }); return highlightCommand; } function getProjectName(): string { try { const grammarContent = readFileSync('grammar.js', 'utf-8'); const nameMatch = grammarContent.match(/name:\s*['"]([^'"]+)['"]/); if (nameMatch) return nameMatch[1]; } catch {} const parts = process.cwd().split(/[\\/]/); return parts[parts.length - 1] || 'dsl'; }