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',