Skip to content

Commit 947fa91

Browse files
committed
feat: Autodetect Syntax Language
1 parent 9bc9b08 commit 947fa91

2 files changed

Lines changed: 83 additions & 15 deletions

File tree

src/components/shared/Dialogs/MultilineTextInputDialog.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,10 @@ import {
1919
import { Textarea } from "@/components/ui/textarea";
2020
import { Paragraph } from "@/components/ui/typography";
2121
import { cn } from "@/lib/utils";
22+
import { detectLanguage, LANGUAGE_OPTIONS } from "@/utils/detectLanguage";
2223

2324
import CodeEditor from "../CodeViewer/CodeEditor";
2425

25-
const LANGUAGE_OPTIONS = [
26-
{ value: "plaintext", label: "Plain Text" },
27-
{ value: "yaml", label: "YAML" },
28-
{ value: "python", label: "Python" },
29-
{ value: "javascript", label: "JavaScript" },
30-
{ value: "json", label: "JSON" },
31-
{ value: "sql", label: "SQL" },
32-
];
33-
3426
interface MultilineTextInputDialogProps {
3527
title: ReactNode;
3628
description?: string;
@@ -57,7 +49,9 @@ export const MultilineTextInputDialog = ({
5749
onConfirm,
5850
}: MultilineTextInputDialogProps) => {
5951
const [value, setValue] = useState(initialValue);
60-
const [selectedLanguage, setSelectedLanguage] = useState("plaintext");
52+
const [selectedLanguage, setSelectedLanguage] = useState(() =>
53+
detectLanguage(initialValue),
54+
);
6155

6256
const handleConfirm = useCallback(() => {
6357
onConfirm(value);
@@ -83,8 +77,8 @@ export const MultilineTextInputDialog = ({
8377
}, [initialValue]);
8478

8579
useEffect(() => {
86-
setSelectedLanguage("plaintext");
87-
}, [highlightSyntax]);
80+
setSelectedLanguage(detectLanguage(initialValue));
81+
}, [initialValue]);
8882

8983
return (
9084
<Dialog open={open} onOpenChange={onCancel}>
@@ -112,7 +106,7 @@ export const MultilineTextInputDialog = ({
112106
</Select>
113107
)}
114108
</InlineStack>
115-
{highlightSyntax ? (
109+
{highlightSyntax && selectedLanguage !== "plaintext" ? (
116110
<div className="h-64">
117111
<CodeEditor
118112
value={value}
@@ -133,15 +127,15 @@ export const MultilineTextInputDialog = ({
133127
)}
134128
<DialogFooter>
135129
<InlineStack gap="2" align="space-between" className="w-full">
136-
{!highlightSyntax && maxLength && value.length >= maxLength && (
130+
{maxLength && value.length >= maxLength && (
137131
<Paragraph tone="warning" size="xs">
138132
Maximum length {maxLength} characters
139133
</Paragraph>
140134
)}
141135
<InlineStack
142136
gap="2"
143137
align="end"
144-
className={cn((highlightSyntax || !maxLength) && "w-full")}
138+
className={cn(!maxLength && "w-full")}
145139
>
146140
<Button variant="outline" onClick={handleCancel}>
147141
Cancel

src/utils/detectLanguage.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
export const LANGUAGE_OPTIONS = [
2+
{ value: "plaintext", label: "Plain Text" },
3+
{ value: "yaml", label: "YAML" },
4+
{ value: "python", label: "Python" },
5+
{ value: "javascript", label: "JavaScript" },
6+
{ value: "json", label: "JSON" },
7+
{ value: "sql", label: "SQL" },
8+
];
9+
10+
type LanguageOption = (typeof LANGUAGE_OPTIONS)[number]["value"];
11+
12+
/**
13+
* Heuristically detects the language of a string from the supported Monaco
14+
* language set: json, sql, python, javascript, yaml, plaintext.
15+
*
16+
* Detection is ordered from most-certain to least-certain.
17+
*/
18+
export function detectLanguage(value: string): LanguageOption {
19+
const trimmed = value.trim();
20+
21+
if (!trimmed) return "plaintext";
22+
23+
// JSON — most definitive: valid parse + structural start character
24+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
25+
try {
26+
JSON.parse(trimmed);
27+
return "json";
28+
} catch {
29+
// not valid json, fall through
30+
}
31+
}
32+
33+
// SQL — distinctive opening keywords
34+
if (
35+
/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|WITH|TRUNCATE|MERGE)\b/im.test(
36+
trimmed,
37+
)
38+
) {
39+
return "sql";
40+
}
41+
42+
// Python — function/class definitions, module imports, decorators, docstrings
43+
if (
44+
/^(def |async def |class \w+\s*[:(])/m.test(trimmed) ||
45+
/^from\s+\S+\s+import\s/m.test(trimmed) ||
46+
/^import\s+\w[\w., ]*$/m.test(trimmed) ||
47+
/^@\w+/m.test(trimmed) ||
48+
/^if\s+__name__\s*==\s*["']__main__["']/m.test(trimmed)
49+
) {
50+
return "python";
51+
}
52+
53+
// JavaScript — const/let/var declarations, arrow functions, CommonJS/ESM imports
54+
if (
55+
/^(const|let|var)\s+\w/m.test(trimmed) ||
56+
/^(export\s+(default\s+)?|import\s+.*\s+from\s+['"])/m.test(trimmed) ||
57+
/^function\s+\w/m.test(trimmed) ||
58+
/\brequire\s*\(/.test(trimmed) ||
59+
/=>/.test(trimmed)
60+
) {
61+
return "javascript";
62+
}
63+
64+
// YAML — document separator, key: value pairs, or list items
65+
if (
66+
/^---/.test(trimmed) ||
67+
/^\w[\w\s-]*:\s/m.test(trimmed) ||
68+
/^- /m.test(trimmed)
69+
) {
70+
return "yaml";
71+
}
72+
73+
return "plaintext";
74+
}

0 commit comments

Comments
 (0)