Skip to content

Commit

Permalink
This commit replaces CodeMirror with Monaco editor to provide a more …
Browse files Browse the repository at this point in the history
…VSCode-like experience. Key changes:

Add Monaco editor with exact VSCode syntax highlighting colors
Configure client-side only Monaco setup to avoid SSR issues
Create dedicated theme configuration matching VSCode's token colors
Update EditorPanel to use Monaco instead of CodeMirror
Add proper theme switching between light/dark modes
Ensure correct syntax highlighting for all file types
The new editor provides a familiar VSCode experience with identical syntax highlighting colors, making code more readable and consistent with VSCode's default theme.
  • Loading branch information
vgcman16 committed Oct 25, 2024
1 parent 8596741 commit 4688714
Show file tree
Hide file tree
Showing 8 changed files with 715 additions and 19 deletions.
158 changes: 158 additions & 0 deletions app/components/editor/MonacoEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useEffect, useRef, useState } from 'react';
import type { Theme } from '~/types/theme';
import type { EditorDocument } from './codemirror/CodeMirrorEditor';
import type { editor } from 'monaco-editor';
import { setupMonaco } from '~/lib/monaco-setup';

interface MonacoEditorProps {
theme: Theme;
editable?: boolean;
settings?: {
fontSize?: string;
tabSize?: number;
};
doc?: EditorDocument;
autoFocusOnDocumentChange?: boolean;
onScroll?: (scrollTop: number) => void;
onChange?: (value: string) => void;
onSave?: () => void;
}

export function MonacoEditor({
theme,
editable = true,
settings,
doc,
autoFocusOnDocumentChange,
onScroll,
onChange,
onSave,
}: MonacoEditorProps) {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<editor.IStandaloneCodeEditor>();
const [monaco, setMonaco] = useState<typeof import('monaco-editor')>();

useEffect(() => {
// Only run in browser
if (typeof window === 'undefined') return;

// Load Monaco
import('monaco-editor').then(async (module) => {
await setupMonaco();
setMonaco(module);
});
}, []);

useEffect(() => {
if (!monaco || !containerRef.current) return;

// Initialize editor
const editor = monaco.editor.create(containerRef.current, {
value: doc?.value,
language: getLanguage(doc?.filePath),
theme: theme === 'dark' ? 'vs-dark' : 'vs',
fontSize: parseInt(settings?.fontSize || '14'),
tabSize: settings?.tabSize || 2,
minimap: {
enabled: false
},
scrollBeyondLastLine: false,
renderWhitespace: 'boundary',
readOnly: !editable,
lineNumbers: 'on',
wordWrap: 'on',
folding: true,
bracketPairColorization: {
enabled: true
},
automaticLayout: true,
scrollbar: {
verticalScrollbarSize: 14,
horizontalScrollbarSize: 14
}
});

editorRef.current = editor;

// Set up VSCode keybindings
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
onSave?.();
});

// Set up scroll listener
editor.onDidScrollChange((e) => {
onScroll?.(e.scrollTop);
});

// Set up change listener
editor.onDidChangeModelContent(() => {
onChange?.(editor.getValue());
});

return () => {
editor.dispose();
};
}, [monaco, containerRef.current]);

useEffect(() => {
if (!editorRef.current || !doc) return;

// Update content if needed
if (doc.value !== editorRef.current.getValue()) {
editorRef.current.setValue(doc.value);
}

// Update language if needed
const model = editorRef.current.getModel();
if (model) {
const currentLanguage = model.getLanguageId();
const newLanguage = getLanguage(doc.filePath);
if (currentLanguage !== newLanguage) {
monaco?.editor.setModelLanguage(model, newLanguage);
}
}
}, [doc?.value, doc?.filePath]);

useEffect(() => {
if (!editorRef.current || !monaco) return;
monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs');
}, [theme]);

useEffect(() => {
if (editorRef.current && autoFocusOnDocumentChange) {
editorRef.current.focus();
}
}, [doc, autoFocusOnDocumentChange]);

// Determine language from file extension
const getLanguage = (filePath?: string) => {
if (!filePath) return 'plaintext';
const ext = filePath.split('.').pop()?.toLowerCase();
switch (ext) {
case 'js':
return 'javascript';
case 'jsx':
return 'javascript';
case 'ts':
return 'typescript';
case 'tsx':
return 'typescript';
case 'json':
return 'json';
case 'md':
return 'markdown';
case 'css':
return 'css';
case 'scss':
return 'scss';
case 'html':
return 'html';
case 'py':
return 'python';
default:
return 'plaintext';
}
};

return <div ref={containerRef} className="h-full" />;
}
57 changes: 39 additions & 18 deletions app/components/workbench/EditorPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { useStore } from '@nanostores/react';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels';
import {
CodeMirrorEditor,
type EditorDocument,
type EditorSettings,
type OnChangeCallback as OnEditorChange,
type OnSaveCallback as OnEditorSave,
type OnScrollCallback as OnEditorScroll,
} from '~/components/editor/codemirror/CodeMirrorEditor';
import type { EditorDocument, EditorSettings } from '~/components/editor/codemirror/CodeMirrorEditor';
import type { OnChangeCallback as OnEditorChange } from '~/components/editor/codemirror/CodeMirrorEditor';
import type { OnSaveCallback as OnEditorSave } from '~/components/editor/codemirror/CodeMirrorEditor';
import type { OnScrollCallback as OnEditorScroll } from '~/components/editor/codemirror/CodeMirrorEditor';
import { IconButton } from '~/components/ui/IconButton';
import { PanelHeader } from '~/components/ui/PanelHeader';
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
Expand All @@ -23,6 +19,7 @@ import { isMobile } from '~/utils/mobile';
import { FileBreadcrumb } from './FileBreadcrumb';
import { FileTree } from './FileTree';
import { Terminal, type TerminalRef } from './terminal/Terminal';
import { EditorSelection } from '@codemirror/state';

interface EditorPanelProps {
files?: FileMap;
Expand Down Expand Up @@ -69,11 +66,19 @@ export const EditorPanel = memo(
const terminalRefs = useRef<Array<TerminalRef | null>>([]);
const terminalPanelRef = useRef<ImperativePanelHandle>(null);
const terminalToggledByShortcut = useRef(false);
const [Editor, setEditor] = useState<any>(null);

const [activeTerminal, setActiveTerminal] = useState(0);
const [terminalCount, setTerminalCount] = useState(1);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);

useEffect(() => {
// Dynamically import Monaco editor on client-side only
import('~/components/editor/MonacoEditor').then((module) => {
setEditor(() => module.MonacoEditor);
});
}, []);

const activeFileSegments = useMemo(() => {
if (!editorDocument) {
return undefined;
Expand Down Expand Up @@ -128,6 +133,18 @@ export const EditorPanel = memo(
}
};

// Adapter functions to convert between CodeMirror and Monaco callback types
const handleEditorScroll = (scrollTop: number) => {
onEditorScroll?.({ top: scrollTop, left: 0 });
};

const handleEditorChange = (value: string) => {
onEditorChange?.({
selection: EditorSelection.single(0),
content: value
});
};

return (
<PanelGroup direction="vertical">
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
Expand Down Expand Up @@ -180,16 +197,20 @@ export const EditorPanel = memo(
)}
</PanelHeader>
<div className="h-full flex-1 overflow-hidden">
<CodeMirrorEditor
theme={theme}
editable={!isStreaming && editorDocument !== undefined}
settings={editorSettings}
doc={editorDocument}
autoFocusOnDocumentChange={!isMobile()}
onScroll={onEditorScroll}
onChange={onEditorChange}
onSave={onFileSave}
/>
{Editor ? (
<Editor
theme={theme}
editable={!isStreaming && editorDocument !== undefined}
settings={editorSettings}
doc={editorDocument}
autoFocusOnDocumentChange={!isMobile()}
onScroll={handleEditorScroll}
onChange={handleEditorChange}
onSave={onFileSave}
/>
) : (
<div>Loading editor...</div>
)}
</div>
</Panel>
</PanelGroup>
Expand Down
Loading

0 comments on commit 4688714

Please sign in to comment.