From aeed5a087324c27084820075b53b381812a46594 Mon Sep 17 00:00:00 2001 From: iliya Date: Wed, 25 Mar 2026 14:05:53 +0200 Subject: [PATCH] feat: add syntax highlighting for R, Ruby, PHP, and SQL Cherry-picked from upstream 022c75da with conflict resolution. Merged our codeClassName support with upstream per-line highlighting. --- .../chat/viewers/MarkdownViewer.tsx | 12 +- .../chat/viewers/syntaxHighlighter.ts | 216 +++++++++++++++++- 2 files changed, 224 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/chat/viewers/MarkdownViewer.tsx b/src/renderer/components/chat/viewers/MarkdownViewer.tsx index 703c07d9..00c5da3f 100644 --- a/src/renderer/components/chat/viewers/MarkdownViewer.tsx +++ b/src/renderer/components/chat/viewers/MarkdownViewer.tsx @@ -39,6 +39,7 @@ import { highlightSearchInChildren, type SearchContext, } from '../searchHighlightUtils'; +import { highlightLine } from '../viewers/syntaxHighlighter'; import { FileLink, isRelativeUrl } from './FileLink'; import { MermaidDiagram } from './MermaidDiagram'; @@ -535,12 +536,21 @@ function createViewerMarkdownComponents( const isBlock = (hasLanguage ?? false) || isMultiLine; if (isBlock) { + const lang = codeClassName?.replace('language-', '') ?? ''; + const raw = typeof children === 'string' ? children : ''; + const text = raw.replace(/\n$/, ''); + const lines = text.split('\n'); return ( - {hl(children)} + {lines.map((line, i) => ( + + {hl(highlightLine(line, lang))} + {i < lines.length - 1 ? '\n' : null} + + ))} ); } diff --git a/src/renderer/components/chat/viewers/syntaxHighlighter.ts b/src/renderer/components/chat/viewers/syntaxHighlighter.ts index 2271d08b..e6e2f6b3 100644 --- a/src/renderer/components/chat/viewers/syntaxHighlighter.ts +++ b/src/renderer/components/chat/viewers/syntaxHighlighter.ts @@ -208,6 +208,200 @@ const KEYWORDS: Record> = { 'true', 'false', ]), + r: new Set([ + 'if', + 'else', + 'for', + 'while', + 'repeat', + 'function', + 'return', + 'next', + 'break', + 'in', + 'library', + 'require', + 'source', + 'TRUE', + 'FALSE', + 'NULL', + 'NA', + 'Inf', + 'NaN', + 'NA_integer_', + 'NA_real_', + 'NA_complex_', + 'NA_character_', + ]), + ruby: new Set([ + 'def', + 'class', + 'module', + 'end', + 'do', + 'if', + 'elsif', + 'else', + 'unless', + 'while', + 'until', + 'for', + 'in', + 'begin', + 'rescue', + 'ensure', + 'raise', + 'return', + 'yield', + 'block_given?', + 'require', + 'require_relative', + 'include', + 'extend', + 'attr_accessor', + 'attr_reader', + 'attr_writer', + 'self', + 'super', + 'nil', + 'true', + 'false', + 'and', + 'or', + 'not', + 'then', + 'when', + 'case', + 'lambda', + 'proc', + 'puts', + 'print', + ]), + php: new Set([ + 'function', + 'class', + 'interface', + 'trait', + 'extends', + 'implements', + 'namespace', + 'use', + 'public', + 'private', + 'protected', + 'static', + 'abstract', + 'final', + 'const', + 'var', + 'new', + 'return', + 'if', + 'elseif', + 'else', + 'for', + 'foreach', + 'while', + 'do', + 'switch', + 'case', + 'break', + 'continue', + 'default', + 'try', + 'catch', + 'finally', + 'throw', + 'as', + 'echo', + 'print', + 'require', + 'require_once', + 'include', + 'include_once', + 'true', + 'false', + 'null', + 'array', + 'isset', + 'unset', + 'empty', + 'self', + 'this', + ]), + sql: new Set([ + 'SELECT', + 'FROM', + 'WHERE', + 'INSERT', + 'INTO', + 'UPDATE', + 'SET', + 'DELETE', + 'CREATE', + 'ALTER', + 'DROP', + 'TABLE', + 'INDEX', + 'VIEW', + 'DATABASE', + 'JOIN', + 'INNER', + 'LEFT', + 'RIGHT', + 'OUTER', + 'FULL', + 'CROSS', + 'ON', + 'AND', + 'OR', + 'NOT', + 'IN', + 'EXISTS', + 'BETWEEN', + 'LIKE', + 'IS', + 'NULL', + 'AS', + 'ORDER', + 'BY', + 'GROUP', + 'HAVING', + 'LIMIT', + 'OFFSET', + 'UNION', + 'ALL', + 'DISTINCT', + 'COUNT', + 'SUM', + 'AVG', + 'MIN', + 'MAX', + 'CASE', + 'WHEN', + 'THEN', + 'ELSE', + 'END', + 'BEGIN', + 'COMMIT', + 'ROLLBACK', + 'TRANSACTION', + 'PRIMARY', + 'KEY', + 'FOREIGN', + 'REFERENCES', + 'CONSTRAINT', + 'DEFAULT', + 'VALUES', + 'TRUE', + 'FALSE', + 'INTEGER', + 'VARCHAR', + 'TEXT', + 'BOOLEAN', + 'DATE', + 'TIMESTAMP', + ]), }; // Extend tsx/jsx to use typescript/javascript keywords @@ -296,8 +490,23 @@ export function highlightLine(line: string, language: string): React.ReactNode[] break; } - // Check for comment (# style for Python/Shell) - if ((language === 'python' || language === 'bash') && remaining.startsWith('#')) { + // Check for comment (# style for Python/Shell/R/Ruby/PHP) + if ( + (language === 'python' || language === 'bash' || language === 'r' || language === 'ruby' || language === 'php') && + remaining.startsWith('#') + ) { + segments.push( + React.createElement( + 'span', + { key: currentPos, style: { color: 'var(--syntax-comment)', fontStyle: 'italic' } }, + remaining + ) + ); + break; + } + + // Check for comment (-- style for SQL) + if (language === 'sql' && remaining.startsWith('--')) { segments.push( React.createElement( 'span', @@ -326,7 +535,8 @@ export function highlightLine(line: string, language: string): React.ReactNode[] const wordMatch = /^([a-zA-Z_$][a-zA-Z0-9_$]*)/.exec(remaining); if (wordMatch) { const word = wordMatch[1]; - if (keywords.has(word)) { + // SQL keywords are case-insensitive + if (keywords.has(word) || (language === 'sql' && keywords.has(word.toUpperCase()))) { segments.push( React.createElement( 'span',