Skip to content

Commit 022c75d

Browse files
committed
feat: add syntax highlighting for R, Ruby, PHP, and SQL
Add keyword sets and comment detection for four new languages in the token-based syntax highlighter. Wire highlightLine into MarkdownViewer so fenced code blocks in assistant text also receive coloring. - R: keywords, # comments - Ruby: keywords, # comments - PHP: keywords, # comments - SQL: keywords: (case-insensitive), -- comments
1 parent 69d0356 commit 022c75d

File tree

2 files changed

+224
-4
lines changed

2 files changed

+224
-4
lines changed

src/renderer/components/chat/viewers/MarkdownViewer.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
highlightSearchInChildren,
3333
type SearchContext,
3434
} from '../searchHighlightUtils';
35+
import { highlightLine } from '../viewers/syntaxHighlighter';
3536

3637
// =============================================================================
3738
// Types
@@ -154,9 +155,18 @@ function createViewerMarkdownComponents(searchCtx: SearchContext | null): Compon
154155
const isBlock = (hasLanguage ?? false) || isMultiLine;
155156

156157
if (isBlock) {
158+
const lang = codeClassName?.replace('language-', '') ?? '';
159+
const raw = typeof children === 'string' ? children : '';
160+
const text = raw.replace(/\n$/, '');
161+
const lines = text.split('\n');
157162
return (
158163
<code className="font-mono text-xs" style={{ color: COLOR_TEXT }}>
159-
{hl(children)}
164+
{lines.map((line, i) => (
165+
<React.Fragment key={i}>
166+
{hl(highlightLine(line, lang))}
167+
{i < lines.length - 1 ? '\n' : null}
168+
</React.Fragment>
169+
))}
160170
</code>
161171
);
162172
}

src/renderer/components/chat/viewers/syntaxHighlighter.ts

Lines changed: 213 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,200 @@ const KEYWORDS: Record<string, Set<string>> = {
208208
'true',
209209
'false',
210210
]),
211+
r: new Set([
212+
'if',
213+
'else',
214+
'for',
215+
'while',
216+
'repeat',
217+
'function',
218+
'return',
219+
'next',
220+
'break',
221+
'in',
222+
'library',
223+
'require',
224+
'source',
225+
'TRUE',
226+
'FALSE',
227+
'NULL',
228+
'NA',
229+
'Inf',
230+
'NaN',
231+
'NA_integer_',
232+
'NA_real_',
233+
'NA_complex_',
234+
'NA_character_',
235+
]),
236+
ruby: new Set([
237+
'def',
238+
'class',
239+
'module',
240+
'end',
241+
'do',
242+
'if',
243+
'elsif',
244+
'else',
245+
'unless',
246+
'while',
247+
'until',
248+
'for',
249+
'in',
250+
'begin',
251+
'rescue',
252+
'ensure',
253+
'raise',
254+
'return',
255+
'yield',
256+
'block_given?',
257+
'require',
258+
'require_relative',
259+
'include',
260+
'extend',
261+
'attr_accessor',
262+
'attr_reader',
263+
'attr_writer',
264+
'self',
265+
'super',
266+
'nil',
267+
'true',
268+
'false',
269+
'and',
270+
'or',
271+
'not',
272+
'then',
273+
'when',
274+
'case',
275+
'lambda',
276+
'proc',
277+
'puts',
278+
'print',
279+
]),
280+
php: new Set([
281+
'function',
282+
'class',
283+
'interface',
284+
'trait',
285+
'extends',
286+
'implements',
287+
'namespace',
288+
'use',
289+
'public',
290+
'private',
291+
'protected',
292+
'static',
293+
'abstract',
294+
'final',
295+
'const',
296+
'var',
297+
'new',
298+
'return',
299+
'if',
300+
'elseif',
301+
'else',
302+
'for',
303+
'foreach',
304+
'while',
305+
'do',
306+
'switch',
307+
'case',
308+
'break',
309+
'continue',
310+
'default',
311+
'try',
312+
'catch',
313+
'finally',
314+
'throw',
315+
'as',
316+
'echo',
317+
'print',
318+
'require',
319+
'require_once',
320+
'include',
321+
'include_once',
322+
'true',
323+
'false',
324+
'null',
325+
'array',
326+
'isset',
327+
'unset',
328+
'empty',
329+
'self',
330+
'this',
331+
]),
332+
sql: new Set([
333+
'SELECT',
334+
'FROM',
335+
'WHERE',
336+
'INSERT',
337+
'INTO',
338+
'UPDATE',
339+
'SET',
340+
'DELETE',
341+
'CREATE',
342+
'ALTER',
343+
'DROP',
344+
'TABLE',
345+
'INDEX',
346+
'VIEW',
347+
'DATABASE',
348+
'JOIN',
349+
'INNER',
350+
'LEFT',
351+
'RIGHT',
352+
'OUTER',
353+
'FULL',
354+
'CROSS',
355+
'ON',
356+
'AND',
357+
'OR',
358+
'NOT',
359+
'IN',
360+
'EXISTS',
361+
'BETWEEN',
362+
'LIKE',
363+
'IS',
364+
'NULL',
365+
'AS',
366+
'ORDER',
367+
'BY',
368+
'GROUP',
369+
'HAVING',
370+
'LIMIT',
371+
'OFFSET',
372+
'UNION',
373+
'ALL',
374+
'DISTINCT',
375+
'COUNT',
376+
'SUM',
377+
'AVG',
378+
'MIN',
379+
'MAX',
380+
'CASE',
381+
'WHEN',
382+
'THEN',
383+
'ELSE',
384+
'END',
385+
'BEGIN',
386+
'COMMIT',
387+
'ROLLBACK',
388+
'TRANSACTION',
389+
'PRIMARY',
390+
'KEY',
391+
'FOREIGN',
392+
'REFERENCES',
393+
'CONSTRAINT',
394+
'DEFAULT',
395+
'VALUES',
396+
'TRUE',
397+
'FALSE',
398+
'INTEGER',
399+
'VARCHAR',
400+
'TEXT',
401+
'BOOLEAN',
402+
'DATE',
403+
'TIMESTAMP',
404+
]),
211405
};
212406

213407
// Extend tsx/jsx to use typescript/javascript keywords
@@ -296,8 +490,23 @@ export function highlightLine(line: string, language: string): React.ReactNode[]
296490
break;
297491
}
298492

299-
// Check for comment (# style for Python/Shell)
300-
if ((language === 'python' || language === 'bash') && remaining.startsWith('#')) {
493+
// Check for comment (# style for Python/Shell/R/Ruby/PHP)
494+
if (
495+
(language === 'python' || language === 'bash' || language === 'r' || language === 'ruby' || language === 'php') &&
496+
remaining.startsWith('#')
497+
) {
498+
segments.push(
499+
React.createElement(
500+
'span',
501+
{ key: currentPos, style: { color: 'var(--syntax-comment)', fontStyle: 'italic' } },
502+
remaining
503+
)
504+
);
505+
break;
506+
}
507+
508+
// Check for comment (-- style for SQL)
509+
if (language === 'sql' && remaining.startsWith('--')) {
301510
segments.push(
302511
React.createElement(
303512
'span',
@@ -326,7 +535,8 @@ export function highlightLine(line: string, language: string): React.ReactNode[]
326535
const wordMatch = /^([a-zA-Z_$][a-zA-Z0-9_$]*)/.exec(remaining);
327536
if (wordMatch) {
328537
const word = wordMatch[1];
329-
if (keywords.has(word)) {
538+
// SQL keywords are case-insensitive
539+
if (keywords.has(word) || (language === 'sql' && keywords.has(word.toUpperCase()))) {
330540
segments.push(
331541
React.createElement(
332542
'span',

0 commit comments

Comments
 (0)