Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 62 additions & 22 deletions src/components/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import React from 'react';
import { Modal, Switch } from 'antd';
import { Modal, Switch, Select } from 'antd';
import DarkModeToggle from 'react-dark-mode-toggle';
import useAppStore from '../store/store';

const FONT_SIZES = [12, 13, 14, 15, 16, 18, 20];

const SettingsModal: React.FC = () => {
const {
isSettingsOpen,
setSettingsOpen,
showLineNumbers,
const {
isSettingsOpen,
setSettingsOpen,
showLineNumbers,
setShowLineNumbers,
textColor,
backgroundColor,
toggleDarkMode
toggleDarkMode,
editorSettings,
setEditorSettings,
} = useAppStore((state) => ({
isSettingsOpen: state.isSettingsOpen,
setSettingsOpen: state.setSettingsOpen,
Expand All @@ -20,6 +24,8 @@ const SettingsModal: React.FC = () => {
textColor: state.textColor,
backgroundColor: state.backgroundColor,
toggleDarkMode: state.toggleDarkMode,
editorSettings: state.editorSettings,
setEditorSettings: state.setEditorSettings,
}));

const isDarkMode = backgroundColor === '#121212';
Expand All @@ -35,7 +41,8 @@ const SettingsModal: React.FC = () => {
style={{ maxWidth: 480 }}
>
<div className="space-y-6 py-4">
{/* Dark Mode Toggle */}

{/* Dark Mode */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm sm:text-base" style={{ color: textColor }}>
Expand All @@ -45,18 +52,12 @@ const SettingsModal: React.FC = () => {
Toggle between light and dark theme
</p>
</div>
<div className="flex-shrink-0">
<DarkModeToggle
onChange={toggleDarkMode}
checked={isDarkMode}
size={50}
/>
</div>
<DarkModeToggle onChange={toggleDarkMode} checked={isDarkMode} size={50} />
</div>

<hr className={isDarkMode ? 'border-gray-600' : 'border-gray-200'} />

{/* Line Numbers Toggle */}
{/* Line Numbers */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm sm:text-base" style={{ color: textColor }}>
Expand All @@ -66,17 +67,56 @@ const SettingsModal: React.FC = () => {
Display line numbers in code editors
</p>
</div>
<div className="flex-shrink-0">
<Switch
checked={showLineNumbers}
onChange={setShowLineNumbers}
aria-label="Toggle line numbers"
/>
<Switch checked={showLineNumbers} onChange={setShowLineNumbers} />
</div>

<hr className={isDarkMode ? 'border-gray-600' : 'border-gray-200'} />

{/* Font Size */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm sm:text-base" style={{ color: textColor }}>
Font Size
</h4>
<p className={`text-xs sm:text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Adjust editor font size
</p>
</div>
<Select
value={editorSettings.fontSize}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
onChange={(value) => setEditorSettings({ fontSize: value })}
Comment on lines +87 to +88
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The eslint-disable comment for unsafe-assignment is unnecessary. The Select onChange handler receives a number value which matches the fontSize type. Consider removing this comment to keep the codebase clean.

Suggested change
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
onChange={(value) => setEditorSettings({ fontSize: value })}
onChange={(value: number) => setEditorSettings({ fontSize: value })}

Copilot uses AI. Check for mistakes.
style={{ width: 110 }}
options={FONT_SIZES.map((size) => ({
label: `${size}px`,
value: size,
}))}
/>
</div>

<hr className={isDarkMode ? 'border-gray-600' : 'border-gray-200'} />

{/* Word Wrap */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm sm:text-base" style={{ color: textColor }}>
Word Wrap
</h4>
<p className={`text-xs sm:text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Wrap long lines in editors
</p>
</div>
<Switch
checked={editorSettings.wordWrap === 'on'}
onChange={(checked) =>
setEditorSettings({ wordWrap: checked ? 'on' : 'off' })
}
/>
</div>
Comment on lines +75 to +115
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Font Size and Word Wrap settings in the SettingsModal lack test coverage. Based on the existing test pattern in src/tests/components/SettingsModal.test.tsx, tests should be added to verify: 1) the Font Size and Word Wrap settings are rendered with correct labels and descriptions, 2) the Font Size dropdown shows the correct options (12-20px), 3) the Word Wrap switch reflects the correct state, and 4) the controls are interactive and update the store correctly.

Copilot uses AI. Check for mistakes.

</div>
</Modal>
);
};

export default SettingsModal;
export default SettingsModal;
116 changes: 68 additions & 48 deletions src/editors/ConcertoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const concertoKeywords = [
"enum",
"scalar",
"extends",
"default",
"participant",
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "default" keyword has been removed from the concertoKeywords array. This appears to be an unintentional change unrelated to the font size and word wrap feature. The "default" keyword is a valid Concerto language keyword used to specify default values for optional properties. This removal will break syntax highlighting for the "default" keyword in Concerto models.

Copilot uses AI. Check for mistakes.
"asset",
"o",
Expand Down Expand Up @@ -60,22 +59,21 @@ const handleEditorWillMount = (monacoInstance: typeof monaco) => {
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: "\"", close: "\"" },
{ open: `"`, close: `"` },
],
surroundingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: "\"", close: "\"" },
{ open: `"`, close: `"` },
],
});

monacoInstance.languages.setMonarchTokensProvider("concerto", {
keywords: concertoKeywords,
typeKeywords: concertoTypes,
operators: ["=", "{", "}", "@", '"'],
operators: ["=", "{", "}", "@", `"`],
symbols: /[=}{@"]+/,
escapes: /\\(?:[btnfru"'\\]|\\u[0-9A-Fa-f]{4})/,
tokenizer: {
root: [
{ include: "@whitespace" },
Expand All @@ -89,12 +87,11 @@ const handleEditorWillMount = (monacoInstance: typeof monaco) => {
},
},
],
[/"([^"\\]|\\.)*$/, "string.invalid"], // non-terminated string
[/"([^"\\]|\\.)*$/, "string.invalid"],
[/"/, "string", "@string"],
],
string: [
[/[^\\"]+/, "string"],
[/@escapes/, "string.escape"],
[/\\./, "string.escape.invalid"],
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The escapes pattern and its usage in the string tokenizer have been removed. The escapes pattern was used to properly tokenize escape sequences in strings. Without it, valid escape sequences like \n, \t, ", etc. will not be properly highlighted as escape sequences in the Concerto editor. This is an unintentional change that degrades the editor's syntax highlighting capabilities.

Copilot uses AI. Check for mistakes.
[/"/, "string", "@pop"],
],
Expand All @@ -105,71 +102,93 @@ const handleEditorWillMount = (monacoInstance: typeof monaco) => {
},
});

if (monacoInstance) {
registerAutocompletion('concerto', monacoInstance);
}
registerAutocompletion("concerto", monacoInstance);
};

interface ConcertoEditorProps {
value: string;
onChange?: (value: string | undefined) => void;
}

export default function ConcertoEditor({
value,
onChange,
}: ConcertoEditorProps) {
export default function ConcertoEditor({ value, onChange }: ConcertoEditorProps) {
const { handleSelection, MenuComponent } = useCodeSelection("concerto");
const monacoInstance = useMonaco();
const { error, backgroundColor, aiConfig, showLineNumbers } = useAppStore((state) => ({

const {
error,
backgroundColor,
aiConfig,
showLineNumbers,
editorSettings,
} = useAppStore((state) => ({
error: state.error,
backgroundColor: state.backgroundColor,
aiConfig: state.aiConfig,
showLineNumbers: state.showLineNumbers,
editorSettings: state.editorSettings,
}));

const ctoErr = error?.startsWith("c:") ? error : undefined;

const themeName = useMemo(
() => (backgroundColor ? "darkTheme" : "lightTheme"),
() => (backgroundColor === "#121212" ? "darkTheme" : "lightTheme"),
[backgroundColor]
);

const options: monaco.editor.IStandaloneEditorConstructionOptions = useMemo(() => ({
minimap: { enabled: false },
wordWrap: "on",
automaticLayout: true,
scrollBeyondLastLine: false,
lineNumbers: showLineNumbers ? 'on' : 'off',
autoClosingBrackets: "languageDefined",
autoSurround: "languageDefined",
bracketPairColorization: { enabled: true },
inlineSuggest: {
enabled: aiConfig?.enableInlineSuggestions !== false,
mode: "prefix",
suppressSuggestions: false,
fontFamily: "inherit",
keepOnBlur: true,
},
suggest: {
preview: true,
showInlineDetails: true,
},
quickSuggestions: false,
suggestOnTriggerCharacters: false,
acceptSuggestionOnCommitCharacter: false,
acceptSuggestionOnEnter: "off",
tabCompletion: "off",
}), [aiConfig?.enableInlineSuggestions, showLineNumbers]);

const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
const options: monaco.editor.IStandaloneEditorConstructionOptions = useMemo(
() => ({
minimap: { enabled: false },
automaticLayout: true,
scrollBeyondLastLine: false,

// 🔑 NEW — editor settings
fontSize: editorSettings.fontSize,
wordWrap: editorSettings.wordWrap,

lineNumbers: showLineNumbers ? "on" : "off",

autoClosingBrackets: "languageDefined",
autoSurround: "languageDefined",
bracketPairColorization: { enabled: true },

inlineSuggest: {
enabled: aiConfig?.enableInlineSuggestions !== false,
mode: "prefix",
suppressSuggestions: false,
fontFamily: "inherit",
keepOnBlur: true,
},

suggest: {
preview: true,
showInlineDetails: true,
},

quickSuggestions: false,
suggestOnTriggerCharacters: false,
acceptSuggestionOnCommitCharacter: false,
acceptSuggestionOnEnter: "off",
tabCompletion: "off",
}),
[
aiConfig?.enableInlineSuggestions,
showLineNumbers,
editorSettings.fontSize,
editorSettings.wordWrap,
]
);

const handleEditorDidMount = (
editor: monaco.editor.IStandaloneCodeEditor
) => {
editor.onDidChangeCursorSelection(() => {
handleSelection(editor);
});
};

const handleChange = useCallback(
(val: string | undefined) => {
if (onChange) onChange(val);
onChange?.(val);
},
[onChange]
);
Expand All @@ -183,8 +202,9 @@ export default function ConcertoEditor({
if (ctoErr) {
const match = ctoErr.match(/Line (\d+) column (\d+)/);
if (match) {
const lineNumber = parseInt(match[1], 10);
const columnNumber = parseInt(match[2], 10);
const lineNumber = Number(match[1]);
const columnNumber = Number(match[2]);

monacoInstance.editor.setModelMarkers(model, "customMarker", [
{
startLineNumber: lineNumber,
Expand Down Expand Up @@ -218,4 +238,4 @@ export default function ConcertoEditor({
{MenuComponent}
</div>
);
}
}
Loading