Skip to content
14 changes: 8 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Navbar from "./components/Navbar";
import tour from "./components/Tour";
import LearnNow from "./pages/LearnNow";
import useAppStore from "./store/store";
import { shallow } from "zustand/shallow";
import LearnContent from "./components/Content";
import MainContainer from "./pages/MainContainer";
import PlaygroundSidebar from "./components/PlaygroundSidebar";
Expand All @@ -18,22 +19,24 @@ const App = () => {
const navigate = useNavigate();
const init = useAppStore((state) => state.init);
const loadFromLink = useAppStore((state) => state.loadFromLink);
const { isAIConfigOpen, setAIConfigOpen } =
useAppStore((state) => ({

const { isAIConfigOpen, setAIConfigOpen } = useAppStore(
(state) => ({
isAIConfigOpen: state.isAIConfigOpen,
setAIConfigOpen: state.setAIConfigOpen,
}));
}),
shallow
);

const backgroundColor = useAppStore((state) => state.backgroundColor);
const textColor = useAppStore((state) => state.textColor);
const [loading, setLoading] = useState(true);
const [searchParams] = useSearchParams();


useEffect(() => {
const initializeApp = async () => {
try {
setLoading(true);
// Prioritize hash for new links, fallback to searchParams for old links
let compressedData: string | null = null;
if (window.location.hash.startsWith("#data=")) {
compressedData = window.location.hash.substring(6);
Expand Down Expand Up @@ -93,7 +96,6 @@ const App = () => {
}
}, [searchParams]);

// Set data-theme attribute on initial load and when theme changes
useEffect(() => {
const theme = backgroundColor === "#121212" ? "dark" : "light";
document.documentElement.setAttribute("data-theme", theme);
Expand Down
145 changes: 60 additions & 85 deletions src/editors/ConcertoEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useMonaco } from "@monaco-editor/react";
import { lazy, Suspense, useCallback, useEffect, useMemo } from "react";
import { lazy, Suspense, useEffect, useMemo, useRef } from "react";
import * as monaco from "monaco-editor";
import useAppStore from "../store/store";
import { shallow } from "zustand/shallow";
import { useCodeSelection } from "../components/CodeSelectionMenu";
import { registerAutocompletion } from "../ai-assistant/autocompletion";

Expand All @@ -10,36 +11,14 @@ const MonacoEditor = lazy(() =>
);

const concertoKeywords = [
"map",
"concept",
"from",
"optional",
"default",
"range",
"regex",
"length",
"abstract",
"namespace",
"import",
"enum",
"scalar",
"extends",
"default",
"participant",
"asset",
"o",
"identified by",
"transaction",
"event",
"map", "concept", "from", "optional", "default", "range",
"regex", "length", "abstract", "namespace", "import", "enum",
"scalar", "extends", "default", "participant", "asset", "o",
"identified by", "transaction", "event",
];

const concertoTypes = [
"String",
"Integer",
"Double",
"DateTime",
"Long",
"Boolean",
"String", "Integer", "Double", "DateTime", "Long", "Boolean",
];

const handleEditorWillMount = (monacoInstance: typeof monaco) => {
Expand All @@ -51,11 +30,7 @@ const handleEditorWillMount = (monacoInstance: typeof monaco) => {
});

monacoInstance.languages.setLanguageConfiguration("concerto", {
brackets: [
["{", "}"],
["[", "]"],
["(", ")"],
],
brackets: [["{", "}"], ["[", "]"], ["(", ")"]],
autoClosingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
Expand All @@ -79,17 +54,14 @@ const handleEditorWillMount = (monacoInstance: typeof monaco) => {
tokenizer: {
root: [
{ include: "@whitespace" },
[
/[a-zA-Z_]\w*/,
{
cases: {
"@keywords": "keyword",
"@typeKeywords": "type",
"@default": "identifier",
},
[/[a-zA-Z_]\w*/, {
cases: {
"@keywords": "keyword",
"@typeKeywords": "type",
"@default": "identifier",
},
],
[/"([^"\\]|\\.)*$/, "string.invalid"], // non-terminated string
}],
[/"([^"\\]|\\.)*$/, "string.invalid"],
[/"/, "string", "@string"],
],
string: [
Expand All @@ -115,22 +87,24 @@ interface ConcertoEditorProps {
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) => ({
error: state.error,
backgroundColor: state.backgroundColor,
aiConfig: state.aiConfig,
showLineNumbers: state.showLineNumbers,
}));
const ctoErr = error?.startsWith("c:") ? error : undefined;
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const decorationsCollectionRef = useRef<monaco.editor.IEditorDecorationsCollection | null>(null);

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

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

Expand All @@ -150,56 +124,57 @@ export default function ConcertoEditor({
fontFamily: "inherit",
keepOnBlur: true,
},
suggest: {
preview: true,
showInlineDetails: true,
},
suggest: { preview: true, showInlineDetails: true },
quickSuggestions: false,
suggestOnTriggerCharacters: false,
acceptSuggestionOnCommitCharacter: false,
acceptSuggestionOnEnter: "off",
tabCompletion: "off",
}), [aiConfig?.enableInlineSuggestions, showLineNumbers]);
}), [showLineNumbers, aiConfig?.enableInlineSuggestions]);

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

const handleChange = useCallback(
(val: string | undefined) => {
if (onChange) onChange(val);
},
[onChange]
);

useEffect(() => {
if (!monacoInstance) return;

const model = monacoInstance.editor.getModels()?.[0];
if (!monacoInstance || !editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;

if (ctoErr) {
const match = ctoErr.match(/Line (\d+) column (\d+)/);
if (error) {
const match = error.match(/Line (\d+)(?::| )Col(?:umn)? (\d+)/i);
if (match) {
const lineNumber = parseInt(match[1], 10);
const columnNumber = parseInt(match[2], 10);
monacoInstance.editor.setModelMarkers(model, "customMarker", [
const line = parseInt(match[1], 10);
const col = parseInt(match[2], 10);

monacoInstance.editor.setModelMarkers(model, "customMarker", [{
startLineNumber: line,
startColumn: col,
endLineNumber: line,
endColumn: model.getLineMaxColumn(line),
message: error,
severity: monaco.MarkerSeverity.Error,
}]);

decorationsCollectionRef.current?.set([
{
startLineNumber: lineNumber,
startColumn: columnNumber - 1,
endLineNumber: lineNumber,
endColumn: columnNumber + 1,
message: ctoErr,
severity: monaco.MarkerSeverity.Error,
},
range: new monaco.Range(line, 1, line, 1),
options: {
isWholeLine: true,
className: 'errorLineHighlight',
}
}
]);
}
} else {
monacoInstance.editor.setModelMarkers(model, "customMarker", []);
decorationsCollectionRef.current?.clear();
}
}, [ctoErr, monacoInstance]);
}, [error, monacoInstance]);

return (
<div className="editorwrapper h-full w-full">
Expand All @@ -211,11 +186,11 @@ export default function ConcertoEditor({
value={value}
beforeMount={handleEditorWillMount}
onMount={handleEditorDidMount}
onChange={handleChange}
onChange={(val) => onChange?.(val)}
theme={themeName}
/>
</Suspense>
{MenuComponent}
</div>
);
}
}
5 changes: 5 additions & 0 deletions src/styles/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@
.app-spinner-container {
@apply flex-1 flex justify-center items-center;
}

.errorLineHighlight {
background: rgba(255, 0, 0, 0.1);
width: 100% !important;
}
Loading