diff --git a/apps/desktop/docs/CODE_EDITOR_MIGRATION_PLAN.md b/apps/desktop/docs/CODE_EDITOR_MIGRATION_PLAN.md new file mode 100644 index 00000000000..bfe857dab0f --- /dev/null +++ b/apps/desktop/docs/CODE_EDITOR_MIGRATION_PLAN.md @@ -0,0 +1,334 @@ +# Code Editor Migration Plan + +## Overview + +This document outlines a staged migration away from Monaco in the desktop app. + +The target architecture is: + +- **Raw file editing:** CodeMirror 6 +- **Diff experience:** `diffs.com` +- **Rollout strategy:** direct migration, incremental, measurable + +This is intentionally not a big-bang rewrite. The main risk is not rendering text; it is preserving current editor behavior while reducing startup cost, memory usage, and typing latency. + +## Why Migrate + +Monaco is currently integrated deeply enough that it affects more than just file editing: + +- It is mounted at the desktop app root via `apps/desktop/src/renderer/routes/-layout.tsx` +- It is initialized through `apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx` +- It powers both raw editing and diff rendering + +Even with diagnostics disabled, Monaco still carries bundle, worker, and runtime overhead that is hard to justify if the product mostly needs a fast embedded editor rather than a full VS Code-style IDE surface. + +## Current Monaco Touchpoints + +The main replacement scope is concentrated in these files: + +- `apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx` +- `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx` +- `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/DiffViewer.tsx` +- `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx` +- `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/hooks/useFileSave/useFileSave.ts` +- `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/useEditorActions.ts` +- `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/editor-actions.ts` + +## Goals + +1. Stop paying Monaco initialization cost for users who are not actively editing files. +2. Replace the raw editor with a lighter, more modular editor. +3. Decouple editor-dependent behaviors from Monaco-specific APIs. +4. Move diff rendering to `diffs.com` instead of rebuilding Monaco diff behavior locally. +5. Compare performance as the migration lands and remove Monaco once parity is confirmed. + +## Non-Goals + +- Recreating every Monaco capability +- Adding IDE-grade language services +- Rewriting file loading or save mutations +- Migrating unrelated markdown or image rendering flows + +## Target Architecture + +### Raw Editor + +Use CodeMirror 6 for: + +- opening and editing text files +- read-only file viewing +- selection and cursor management +- save shortcuts +- basic find flow +- copy path with line numbers +- theme integration + +### Diff + +Use `diffs.com` for: + +- diff visualization +- diff navigation +- editable or review-oriented diff workflows, depending on the integration mode chosen + +This removes the need to port Monaco's diff-specific logic directly. + +### Adapter Layer + +Introduce an internal editor adapter so the rest of the file pane does not depend on Monaco or CodeMirror directly. + +Proposed adapter surface: + +```ts +interface CodeEditorAdapter { + focus(): void; + getValue(): string; + setValue(value: string): void; + revealPosition(line: number, column?: number): void; + getSelectionLines(): { startLine: number; endLine: number } | null; + selectAll(): void; + cut(): void; + copy(): void; + paste(): void; + openFind(): void; + dispose(): void; +} +``` + +The goal is not to abstract every editor feature. The goal is to cover the exact behaviors already used by the file pane and context menu. + +## Migration Phases + +### Phase 0: Measure Before Changing Behavior + +Establish a baseline for: + +- desktop app startup time +- time to first file open +- memory after app launch +- memory after opening a large file +- typing latency in the raw editor + +Capture each metric with a fixed harness so the rollout gate is objective: + +- run the same production desktop build on the same machine against the same representative repo snapshot +- take 5 cold runs for startup and first-file-open timings, and record both p50 and p95 +- measure memory as renderer RSS 30 seconds after launch and again 30 seconds after opening a representative large text file +- measure typing latency as input-to-paint p50/p95 while editing that same large text file +- record the results in the PR description or linked rollout issue before deleting Monaco + +Also run a quick experiment that lazy-loads Monaco instead of mounting it globally. This tells us how much of the pain is caused by Monaco itself versus eager initialization. + +Suggested rollout thresholds: + +- startup p95 must not regress against the lazy-loaded Monaco control +- first-file-open p95 must not regress against the current Monaco path +- memory after launch must improve by at least 20% +- memory after opening a large file must improve by at least 20% +- typing-latency p95 must not regress + +### Phase 1: Decouple File Pane Logic from Monaco + +Create an editor-agnostic layer and migrate current consumers to it. + +Work items: + +- Replace `Monaco.editor.IStandaloneCodeEditor` refs in `FileViewerPane.tsx` +- Update `useFileSave.ts` to depend on adapter methods like `getValue()` instead of Monaco types +- Update `useEditorActions.ts` to call adapter methods instead of `editor.trigger(...)` +- Move save shortcut registration out of Monaco-specific utilities +- Move copy-path-with-line behavior out of Monaco action registration + +Exit criteria: + +- file save flow is editor-agnostic +- context menu is editor-agnostic +- file pane state no longer imports Monaco types directly outside the Monaco wrapper + +### Phase 2: Migrate Raw File Editing to CodeMirror 6 + +Build a `CodeEditor` wrapper component backed by CodeMirror 6 and use it in the raw editor path. + +Required parity: + +- load file content +- update dirty state on edit +- save on `Cmd/Ctrl+S` +- read-only mode +- line/column jump from file viewer state +- copy path with current line or selection range +- basic find support +- theme application using existing Superset theme tokens + +Explicitly defer anything not used today. + +Suggested implementation shape: + +- `renderer/components/CodeEditor/CodeEditor.tsx` +- `renderer/components/CodeEditor/index.ts` +- `renderer/components/CodeEditor/lib/` for adapter and keymaps + +Exit criteria: + +- raw file editing no longer depends on `@monaco-editor/react` +- save and unsaved-change behavior matches current behavior + +### Phase 3: Replace Diff Viewer with `diffs.com` + +Do not port the current Monaco diff viewer one-to-one unless needed. Treat diff as a separate product surface. + +Work items: + +- define the integration contract for `diffs.com` +- map current inputs to the new diff viewer + - original content + - modified content + - file path + - editable state +- preserve save flow for editable diffs if supported by the chosen integration path +- preserve pane-level actions around closing, splitting, and navigation + +Behavior that may change: + +- first-diff auto-scroll +- hidden unchanged region behavior +- exact keyboard shortcuts +- exact selection semantics between original and modified panes + +These should be treated as explicit product decisions, not accidental regressions. + +Exit criteria: + +- diff view no longer renders Monaco +- editable diff save flow still works, or the product intentionally scopes editable diff differently +- users can review file changes without loading Monaco + +### Phase 4: Remove Monaco from the Root Layout + +Once both the raw editor and diff no longer depend on Monaco: + +- stop mounting `MonacoProvider` at the app root +- remove global worker setup from normal startup +This is where the startup win should become most visible. + +### Phase 5: Rollout, Compare, and Delete + +Roll out to internal users first. + +Compare: + +- startup time +- memory usage +- CPU during file open +- typing responsiveness +- crash rate or renderer instability + +Use the same capture method and thresholds from Phase 0 for the rollout decision. "Measurably faster or lighter" means those thresholds are met in the same test environment. + +If the CodeMirror + `diffs.com` path is stable and better, remove: + +- `@monaco-editor/react` +- `monaco-editor` +- `MonacoProvider` +- Monaco-only editor action utilities + +## Acceptance Criteria + +The migration is complete when: + +1. Opening the desktop app no longer initializes Monaco by default. +2. Raw text editing works through CodeMirror 6. +3. Dirty-state tracking and save flows still behave correctly. +4. Copy path, copy path with line, select all, cut, copy, paste, and find still work in the file pane. +5. Diff rendering is handled by `diffs.com`. +6. The new path meets the rollout thresholds defined in Phase 0. +7. Monaco can be removed without losing required user-facing capabilities. + +## Risks + +### Diff Product Fit + +`diffs.com` may not map exactly to Monaco's current editable diff behavior. This is the largest product and integration risk. + +Mitigation: + +- treat diff as a separate migration track +- define required behaviors early +- explicitly decide which Monaco diff behaviors matter and which do not + +### Context Menu and Shortcut Parity + +Current editor actions use Monaco-specific command IDs and selection models. + +Mitigation: + +- move all command logic behind the adapter +- write small parity tests around selection and save shortcuts + +### Theme Parity + +Monaco theming is currently specialized. + +Mitigation: + +- map Superset theme tokens to a shared editor theme contract +- avoid embedding Monaco-specific theme types into the broader renderer state + +### Rollout Complexity + +Landing the migration directly raises the risk of broad regressions if too many behaviors move at once. + +Mitigation: + +- keep the adapter small +- migrate raw editing and diff rendering in clearly separated commits +- delete Monaco quickly after validation + +## Recommended Sequence + +### Week 1 + +- baseline performance measurements +- lazy-load Monaco experiment +- define editor adapter +- remove Monaco types from save and context menu flows + +### Week 2 + +- implement CodeMirror 6 raw editor +- internal QA on raw editing flows + +### Week 3 + +- integrate `diffs.com` +- validate diff review workflows +- resolve save-path decisions for editable diffs + +### Week 4 + +- remove Monaco from root startup path +- compare metrics +- clean up fallback code if results hold + +## Implementation Checklist + +- [ ] Measure current Monaco startup and editor-open costs +- [ ] Test lazy-loaded Monaco as a control +- [ ] Add editor adapter interface +- [ ] Refactor `useFileSave.ts` to consume adapter methods +- [ ] Refactor `useEditorActions.ts` to consume adapter methods +- [ ] Ensure adapter owners call `dispose()` when tearing down editor instances +- [ ] Replace Monaco copy-path action registration +- [ ] Implement CodeMirror 6 raw editor wrapper +- [ ] Migrate raw editor path in `FileViewerContent.tsx` +- [ ] Integrate `diffs.com` for diff rendering +- [ ] Validate editable diff save flow +- [ ] Remove `MonacoProvider` from root layout +- [ ] Compare before/after metrics +- [ ] Remove Monaco dependencies and dead code + +## Open Questions + +1. Does `diffs.com` need to support in-place editable diffs, or is read/review-only acceptable initially? +2. Should find be implemented with native CodeMirror search UI, or should it continue to route through Superset-owned UI controls? +3. Do we want Monaco kept as a hidden fallback for one release, or removed immediately after internal validation? diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 41cbd04f619..49d7c7d864e 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -241,10 +241,6 @@ export default defineConfig({ format: "es", }, - optimizeDeps: { - include: ["monaco-editor"], - }, - publicDir: resolve(resources, "public"), build: { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 861c74903f6..8529a475251 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -39,6 +39,27 @@ "@ai-sdk/react": "^3.0.0", "@ast-grep/napi": "^0.41.0", "@better-auth/stripe": "1.4.18", + "@codemirror/commands": "^6.10.2", + "@codemirror/lang-cpp": "^6.0.3", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-java": "^6.0.2", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-php": "^6.0.2", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.2", + "@codemirror/legacy-modes": "^6.5.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.16", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -47,8 +68,8 @@ "@headless-tree/core": "^1.6.3", "@headless-tree/react": "^1.6.3", "@hookform/resolvers": "^5.2.2", + "@lezer/highlight": "^1.2.3", "@mastra/core": "^1.3.0", - "@monaco-editor/react": "^4.7.0", "@outlit/browser": "^1.4.3", "@outlit/node": "^1.4.3", "@pierre/diffs": "^1.0.10", @@ -149,7 +170,6 @@ "lowlight": "^3.3.0", "lucide-react": "^0.563.0", "mastracode": "^0.4.0", - "monaco-editor": "^0.55.1", "nanoid": "^5.1.6", "node-addon-api": "^7.1.0", "node-pty": "1.1.0", diff --git a/apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx b/apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx deleted file mode 100644 index 0876d05f6db..00000000000 --- a/apps/desktop/src/renderer/providers/MonacoProvider/MonacoProvider.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { loader } from "@monaco-editor/react"; -import * as monaco from "monaco-editor"; -import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; -import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; -import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; -import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; -import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; -import type React from "react"; -import { createContext, useContext, useEffect, useMemo, useState } from "react"; -import { electronTrpc } from "renderer/lib/electron-trpc"; -import { useMonacoTheme } from "renderer/stores/theme"; - -self.MonacoEnvironment = { - getWorker(_: unknown, label: string) { - if (label === "json") { - return new jsonWorker(); - } - if (label === "css" || label === "scss" || label === "less") { - return new cssWorker(); - } - if (label === "html" || label === "handlebars" || label === "razor") { - return new htmlWorker(); - } - if (label === "typescript" || label === "javascript") { - return new tsWorker(); - } - return new editorWorker(); - }, -}; - -loader.config({ monaco }); - -const SUPERSET_THEME = "superset-theme"; - -let monacoInitialized = false; - -async function initializeMonaco(): Promise { - if (monacoInitialized) { - return monaco; - } - - await loader.init(); - - // Note: Disable all diagnostics (lint errors, type errors, etc.) since it's a diff viewer - monaco.typescript.typescriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: true, - noSyntaxValidation: true, - }); - monaco.typescript.javascriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: true, - noSyntaxValidation: true, - }); - - monacoInitialized = true; - return monaco; -} - -const monacoPromise = initializeMonaco(); - -interface MonacoContextValue { - isReady: boolean; -} - -const MonacoContext = createContext({ isReady: false }); - -export function useMonacoReady(): boolean { - return useContext(MonacoContext).isReady; -} - -interface MonacoProviderProps { - children: React.ReactNode; -} - -export function MonacoProvider({ children }: MonacoProviderProps) { - const monacoTheme = useMonacoTheme(); - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - if (isReady) return; - if (!monacoTheme) return; - - let cancelled = false; - - monacoPromise.then((monacoInstance) => { - if (cancelled) return; - monacoInstance.editor.defineTheme(SUPERSET_THEME, monacoTheme); - setIsReady(true); - }); - - return () => { - cancelled = true; - }; - }, [isReady, monacoTheme]); - - useEffect(() => { - if (!isReady || !monacoTheme) return; - - monaco.editor.defineTheme(SUPERSET_THEME, monacoTheme); - }, [isReady, monacoTheme]); - - return ( - - {children} - - ); -} - -export const MONACO_EDITOR_OPTIONS = { - minimap: { enabled: false }, - scrollBeyondLastLine: false, - wordWrap: "on" as const, - fontSize: 13, - lineHeight: 20, - lineNumbersMinChars: 3, - fontFamily: - "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace", - padding: { top: 8, bottom: 8 }, - scrollbar: { - verticalScrollbarSize: 8, - horizontalScrollbarSize: 8, - }, - renderLineHighlight: "none" as const, - folding: false, - guides: { - indentation: false, - bracketPairs: false, - highlightActiveIndentation: false, - }, -}; - -export function useMonacoEditorOptions() { - const { data: fontSettings } = electronTrpc.settings.getFontSettings.useQuery( - undefined, - { - staleTime: 30_000, - }, - ); - - return useMemo(() => { - if (!fontSettings) return MONACO_EDITOR_OPTIONS; - const fontSize = - fontSettings.editorFontSize ?? MONACO_EDITOR_OPTIONS.fontSize; - return { - ...MONACO_EDITOR_OPTIONS, - ...(fontSettings.editorFontFamily && { - fontFamily: fontSettings.editorFontFamily, - }), - ...(fontSettings.editorFontSize != null && { - fontSize, - lineHeight: Math.round(fontSize * 1.5), - }), - }; - }, [fontSettings]); -} - -export function registerSaveAction( - editor: monaco.editor.IStandaloneCodeEditor, - onSave: () => void, -) { - editor.addAction({ - id: "save-file", - label: "Save File", - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], - run: onSave, - }); -} - -export { monaco, SUPERSET_THEME }; diff --git a/apps/desktop/src/renderer/providers/MonacoProvider/index.ts b/apps/desktop/src/renderer/providers/MonacoProvider/index.ts deleted file mode 100644 index 65656ed95f7..00000000000 --- a/apps/desktop/src/renderer/providers/MonacoProvider/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - MONACO_EDITOR_OPTIONS, - MonacoProvider, - monaco, - registerSaveAction, - SUPERSET_THEME, - useMonacoEditorOptions, - useMonacoReady, -} from "./MonacoProvider"; diff --git a/apps/desktop/src/renderer/routes/-layout.tsx b/apps/desktop/src/renderer/routes/-layout.tsx index fda9e4b844a..7172bc7793b 100644 --- a/apps/desktop/src/renderer/routes/-layout.tsx +++ b/apps/desktop/src/renderer/routes/-layout.tsx @@ -5,7 +5,6 @@ import { TelemetrySync } from "renderer/components/TelemetrySync"; import { ThemedToaster } from "renderer/components/ThemedToaster"; import { AuthProvider } from "renderer/providers/AuthProvider"; import { ElectronTRPCProvider } from "renderer/providers/ElectronTRPCProvider"; -import { MonacoProvider } from "renderer/providers/MonacoProvider"; import { OutlitProvider } from "renderer/providers/OutlitProvider"; import { PostHogProvider } from "renderer/providers/PostHogProvider"; @@ -17,11 +16,9 @@ export function RootLayout({ children }: { children: ReactNode }) { - - {children} - - - + {children} + + diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/FontSettingSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/FontSettingSection.tsx index faefac9f02e..b5fc2576c67 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/FontSettingSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/FontSettingSection.tsx @@ -2,15 +2,18 @@ import { Button } from "@superset/ui/button"; import { Input } from "@superset/ui/input"; import { useCallback, useEffect, useState } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { MONACO_EDITOR_OPTIONS } from "renderer/providers/MonacoProvider"; import { DEFAULT_TERMINAL_FONT_FAMILY, DEFAULT_TERMINAL_FONT_SIZE, } from "renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/config"; +import { + DEFAULT_CODE_EDITOR_FONT_FAMILY, + DEFAULT_CODE_EDITOR_FONT_SIZE, +} from "renderer/screens/main/components/WorkspaceView/components/CodeEditor/constants"; import { FontPreview } from "../FontPreview"; -const DEFAULT_EDITOR_FONT_FAMILY = MONACO_EDITOR_OPTIONS.fontFamily; -const DEFAULT_EDITOR_FONT_SIZE = MONACO_EDITOR_OPTIONS.fontSize; +const DEFAULT_EDITOR_FONT_FAMILY = DEFAULT_CODE_EDITOR_FONT_FAMILY; +const DEFAULT_EDITOR_FONT_SIZE = DEFAULT_CODE_EDITOR_FONT_SIZE; const VARIANT_CONFIG = { editor: { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/DiffViewer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/DiffViewer.tsx deleted file mode 100644 index 8bb7c919865..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/DiffViewer.tsx +++ /dev/null @@ -1,344 +0,0 @@ -import { DiffEditor, type DiffOnMount } from "@monaco-editor/react"; -import type * as Monaco from "monaco-editor"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { LuLoader } from "react-icons/lu"; -import { - registerSaveAction, - SUPERSET_THEME, - useMonacoEditorOptions, - useMonacoReady, -} from "renderer/providers/MonacoProvider"; -import type { Tab } from "renderer/stores/tabs/types"; -import type { DiffViewMode, FileContents } from "shared/changes-types"; -import { - EditorContextMenu, - type PaneActions, - registerCopyPathLineAction, - useEditorActions, -} from "../../../ContentView/components/EditorContextMenu"; -import { getLineNumbersMinChars } from "./utils"; - -function scrollToFirstDiff( - editor: Monaco.editor.IStandaloneDiffEditor, - modifiedEditor: Monaco.editor.IStandaloneCodeEditor, -) { - const lineChanges = editor.getLineChanges(); - if (!lineChanges || lineChanges.length === 0) return; - - const firstChange = lineChanges[0]; - const targetLine = - firstChange.modifiedStartLineNumber > 0 - ? firstChange.modifiedStartLineNumber - : firstChange.originalStartLineNumber; - - if (targetLine > 0) { - modifiedEditor.revealLineInCenter(targetLine); - } -} - -export interface DiffViewerContextMenuProps { - onSplitHorizontal: () => void; - onSplitVertical: () => void; - onSplitWithNewChat?: () => void; - onSplitWithNewBrowser?: () => void; - onClosePane: () => void; - currentTabId: string; - availableTabs: Tab[]; - onMoveToTab: (tabId: string) => void; - onMoveToNewTab: () => void; -} - -interface DiffViewerProps { - contents: FileContents; - viewMode: DiffViewMode; - hideUnchangedRegions?: boolean; - filePath: string; - editable?: boolean; - onSave?: (content: string) => void; - onChange?: (content: string) => void; - contextMenuProps?: DiffViewerContextMenuProps; - captureScroll?: boolean; - fitContent?: boolean; -} - -export function DiffViewer({ - contents, - viewMode, - hideUnchangedRegions = false, - filePath, - editable = false, - onSave, - onChange, - contextMenuProps, - captureScroll = true, - fitContent = false, -}: DiffViewerProps) { - const isMonacoReady = useMonacoReady(); - const monacoEditorOptions = useMonacoEditorOptions(); - const diffEditorRef = useRef( - null, - ); - const modifiedEditorRef = useRef( - null, - ); - const [isEditorMounted, setIsEditorMounted] = useState(false); - const [isFocused, setIsFocused] = useState(false); - const [contentHeight, setContentHeight] = useState(null); - const hasScrolledToFirstDiffRef = useRef(false); - const containerRef = useRef(null); - - const contentSizeListenersRef = useRef([]); - - useEffect(() => { - if (!isMonacoReady) return; - if (!isEditorMounted) return; - - requestAnimationFrame(() => { - const modifiedEditor = modifiedEditorRef.current; - if (modifiedEditor) { - modifiedEditor.layout(); - } - }); - }, [isMonacoReady, isEditorMounted]); - - // biome-ignore lint/correctness/useExhaustiveDependencies: Reset on file change only - useEffect(() => { - hasScrolledToFirstDiffRef.current = false; - }, [filePath]); - - const handleSave = useCallback(() => { - if (!editable || !onSave || !modifiedEditorRef.current) return; - onSave(modifiedEditorRef.current.getValue()); - }, [editable, onSave]); - - const changeListenerRef = useRef(null); - const diffUpdateListenerRef = useRef(null); - - const handleMount: DiffOnMount = useCallback( - (editor) => { - diffEditorRef.current = editor; - const originalEditor = editor.getOriginalEditor(); - const modifiedEditor = editor.getModifiedEditor(); - modifiedEditorRef.current = modifiedEditor; - - registerCopyPathLineAction(originalEditor, filePath); - registerCopyPathLineAction(modifiedEditor, filePath); - - diffUpdateListenerRef.current?.dispose(); - diffUpdateListenerRef.current = editor.onDidUpdateDiff(() => { - if (!hasScrolledToFirstDiffRef.current) { - scrollToFirstDiff(editor, modifiedEditor); - hasScrolledToFirstDiffRef.current = true; - } - }); - - if (fitContent) { - contentSizeListenersRef.current.forEach((d) => { - d.dispose(); - }); - contentSizeListenersRef.current = []; - - const updateHeight = () => { - const modHeight = modifiedEditor.getContentHeight(); - const origHeight = originalEditor.getContentHeight(); - setContentHeight(Math.max(modHeight, origHeight)); - }; - - contentSizeListenersRef.current.push( - modifiedEditor.onDidContentSizeChange(updateHeight), - originalEditor.onDidContentSizeChange(updateHeight), - ); - - requestAnimationFrame(updateHeight); - } - - setIsEditorMounted(true); - }, - [filePath, fitContent], - ); - - useEffect(() => { - return () => { - diffUpdateListenerRef.current?.dispose(); - diffUpdateListenerRef.current = null; - contentSizeListenersRef.current.forEach((d) => { - d.dispose(); - }); - contentSizeListenersRef.current = []; - }; - }, []); - - useEffect(() => { - if (captureScroll) return; - if (!isEditorMounted || !diffEditorRef.current) return; - - const originalEditor = diffEditorRef.current.getOriginalEditor(); - const modifiedEditor = diffEditorRef.current.getModifiedEditor(); - - const scrollOptions = { - scrollbar: { handleMouseWheel: isFocused }, - }; - - originalEditor.updateOptions(scrollOptions); - modifiedEditor.updateOptions(scrollOptions); - }, [captureScroll, isEditorMounted, isFocused]); - - const handleFocus = useCallback(() => { - if (!captureScroll) { - setIsFocused(true); - } - }, [captureScroll]); - - const handleBlur = useCallback( - (e: React.FocusEvent) => { - if (!captureScroll && containerRef.current) { - if (!containerRef.current.contains(e.relatedTarget as Node)) { - setIsFocused(false); - } - } - }, - [captureScroll], - ); - - useEffect(() => { - if (!isEditorMounted || !modifiedEditorRef.current) return; - - modifiedEditorRef.current.updateOptions({ readOnly: !editable }); - - if (editable) { - registerSaveAction(modifiedEditorRef.current, handleSave); - } - }, [isEditorMounted, editable, handleSave]); - - useEffect(() => { - if (!isEditorMounted || !modifiedEditorRef.current || !onChange) return; - - changeListenerRef.current?.dispose(); - - changeListenerRef.current = - modifiedEditorRef.current.onDidChangeModelContent(() => { - if (modifiedEditorRef.current) { - onChange(modifiedEditorRef.current.getValue()); - } - }); - - return () => { - changeListenerRef.current?.dispose(); - changeListenerRef.current = null; - }; - }, [isEditorMounted, onChange]); - - const getEditor = useCallback(() => { - return ( - modifiedEditorRef.current || diffEditorRef.current?.getOriginalEditor() - ); - }, []); - - const editorActions = useEditorActions({ - getEditor, - filePath, - editable: false, - }); - - useEffect(() => { - if (!diffEditorRef.current) return; - diffEditorRef.current.updateOptions({ - renderSideBySide: viewMode === "side-by-side", - hideUnchangedRegions: { enabled: hideUnchangedRegions }, - }); - }, [viewMode, hideUnchangedRegions]); - - if (!isMonacoReady) { - return ( -
- - Loading editor... -
- ); - } - - const editorHeight = fitContent && contentHeight ? contentHeight : "100%"; - - const diffEditor = ( - - - Loading editor... - - } - options={{ - ...monacoEditorOptions, - lineNumbersMinChars: getLineNumbersMinChars( - contents.original, - contents.modified, - ), - renderSideBySide: viewMode === "side-by-side", - useInlineViewWhenSpaceIsLimited: false, - readOnly: !editable, - originalEditable: false, - renderOverviewRuler: !fitContent, - glyphMargin: false, - diffWordWrap: "on", - contextmenu: !contextMenuProps, - renderIndicators: false, - hideUnchangedRegions: { - enabled: hideUnchangedRegions, - }, - scrollbar: { - handleMouseWheel: captureScroll, - vertical: fitContent ? "hidden" : "auto", - horizontal: fitContent ? "hidden" : "auto", - }, - scrollBeyondLastLine: !fitContent, - }} - /> - ); - - if (!contextMenuProps) { - return ( - // biome-ignore lint/a11y/noStaticElementInteractions: focus/blur tracking for scroll behavior -
- {diffEditor} -
- ); - } - - const paneActions: PaneActions = { - onSplitHorizontal: contextMenuProps.onSplitHorizontal, - onSplitVertical: contextMenuProps.onSplitVertical, - onSplitWithNewChat: contextMenuProps.onSplitWithNewChat, - onSplitWithNewBrowser: contextMenuProps.onSplitWithNewBrowser, - onClosePane: contextMenuProps.onClosePane, - currentTabId: contextMenuProps.currentTabId, - availableTabs: contextMenuProps.availableTabs, - onMoveToTab: contextMenuProps.onMoveToTab, - onMoveToNewTab: contextMenuProps.onMoveToNewTab, - }; - - return ( - - {/* biome-ignore lint/a11y/noStaticElementInteractions: focus/blur tracking for scroll behavior */} -
- {diffEditor} -
-
- ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/index.ts deleted file mode 100644 index e256ea2ce1b..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/DiffViewer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DiffViewer } from "./DiffViewer"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx index 897ae384735..4e0f053a563 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx @@ -3,14 +3,15 @@ import { Collapsible, CollapsibleContent } from "@superset/ui/collapsible"; import { useCallback, useEffect, useRef, useState } from "react"; import { LuFileCode, LuLoader } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { CodeEditor } from "renderer/screens/main/components/WorkspaceView/components/CodeEditor"; import { useChangesStore } from "renderer/stores/changes"; import type { ChangeCategory, ChangedFile } from "shared/changes-types"; +import { detectLanguage } from "shared/detect-language"; import { getStatusColor, getStatusIndicator, } from "../../../RightSidebar/ChangesView/utils"; import { createFileKey, useScrollContext } from "../../context"; -import { DiffViewer } from "../DiffViewer"; import { LightDiffViewer } from "../LightDiffViewer"; import { FileDiffHeader } from "./components/FileDiffHeader"; import { FILE_DIFF_SECTION_PLACEHOLDER_HEIGHT } from "./constants"; @@ -86,6 +87,7 @@ export function FileDiffSection({ const [hasBeenVisible, setHasBeenVisible] = useState(false); const [isInLoadRange, setIsInLoadRange] = useState(false); const [loadHiddenDiff, setLoadHiddenDiff] = useState(false); + const [editedContent, setEditedContent] = useState(null); const { isEditing, toggleEdit, handleSave } = useFileDiffEdit({ category, @@ -227,6 +229,17 @@ export function FileDiffSection({ }, ); const hasRenderedDiff = canShowDiffBody && !!diffData; + const modifiedDiffContent = diffData?.modified; + + useEffect(() => { + if (!isEditing) { + setEditedContent(null); + return; + } + + if (modifiedDiffContent == null) return; + setEditedContent((current) => current ?? modifiedDiffContent); + }, [isEditing, modifiedDiffContent]); const inactivePlaceholder = (
) : hasRenderedDiff ? ( isEditing ? ( - +
+ { + setEditedContent(value); + }} + onSave={() => handleSave(editedContent ?? diffData.modified)} + fillHeight={false} + /> +
) : ( = { - dark: { light: "github-light-default", dark: "github-dark-default" }, - light: { light: "github-light-default", dark: "github-dark-default" }, - "one-dark": { light: "one-light", dark: "one-dark-pro" }, - monokai: { light: "one-light", dark: "monokai" }, - ember: { light: "one-light", dark: "vitesse-dark" }, -}; - -const DEFAULT_THEMES = { - light: "github-light-default" as DiffsThemeNames, - dark: "github-dark-default" as DiffsThemeNames, -}; - interface LightDiffViewerProps { contents: FileContents; viewMode: DiffViewMode; hideUnchangedRegions?: boolean; filePath: string; + className?: string; + style?: CSSProperties; } export function LightDiffViewer({ @@ -32,24 +22,43 @@ export function LightDiffViewer({ viewMode, hideUnchangedRegions, filePath, + className, + style, }: LightDiffViewerProps) { - const activeTheme = useThemeStore((s) => s.activeTheme); - const themeId = activeTheme?.id ?? "dark"; - const themeType = useThemeStore((s) => - s.activeTheme?.type === "light" ? ("light" as const) : ("dark" as const), + const { data: fontSettings } = electronTrpc.settings.getFontSettings.useQuery( + undefined, + { + staleTime: 30_000, + }, ); - - const shikiTheme = SHIKI_THEME_MAP[themeId] ?? DEFAULT_THEMES; + const shikiTheme = getDiffsTheme(); + const parsedEditorFontSize = + typeof fontSettings?.editorFontSize === "number" + ? fontSettings.editorFontSize + : typeof fontSettings?.editorFontSize === "string" + ? Number.parseFloat(fontSettings.editorFontSize) + : Number.NaN; + const diffStyle = getDiffViewerStyle({ + fontFamily: fontSettings?.editorFontFamily ?? undefined, + fontSize: Number.isFinite(parsedEditorFontSize) + ? parsedEditorFontSize + : undefined, + }); return ( (null); + const editorRef = useRef(null); const markdownContainerRef = useRef(null); const [isDirty, setIsDirty] = useState(false); const originalContentRef = useRef(""); const draftContentRef = useRef(null); const originalDiffContentRef = useRef(""); - const currentDiffContentRef = useRef(""); const [showUnsavedDialog, setShowUnsavedDialog] = useState(false); const [isSavingAndSwitching, setIsSavingAndSwitching] = useState(false); const pendingModeRef = useRef(null); @@ -97,7 +95,7 @@ export function FileViewerPane({ filePath, }); - const { handleSaveRaw, handleSaveDiff } = useFileSave({ + const { handleSaveRaw } = useFileSave({ worktreePath, filePath, paneId, @@ -130,6 +128,7 @@ export function FileViewerPane({ const handleEditorChange = useCallback((value: string | undefined) => { if (value === undefined) return; + draftContentRef.current = value; if (originalContentRef.current === "") { originalContentRef.current = value; return; @@ -142,7 +141,6 @@ export function FileViewerPane({ setIsDirty(false); originalContentRef.current = ""; originalDiffContentRef.current = ""; - currentDiffContentRef.current = ""; draftContentRef.current = null; }, [filePath]); @@ -153,15 +151,6 @@ export function FileViewerPane({ } }, [isDirty, isPinned, paneId, pinPane]); - const handleDiffChange = useCallback((content: string) => { - currentDiffContentRef.current = content; - if (originalDiffContentRef.current === "") { - originalDiffContentRef.current = content; - return; - } - setIsDirty(content !== originalDiffContentRef.current); - }, []); - if (!fileViewer) { return ( @@ -314,7 +290,6 @@ export function FileViewerPane({ rawFileData={rawFileData} imageData={imageData} diffData={diffData} - isDiffEditable={canEditDiff} editorRef={editorRef} originalContentRef={originalContentRef} draftContentRef={draftContentRef} @@ -323,9 +298,7 @@ export function FileViewerPane({ diffViewMode={diffViewMode} hideUnchangedRegions={hideUnchangedRegions} onSaveRaw={handleSaveRaw} - onSaveDiff={canEditDiff ? handleSaveDiff : undefined} onEditorChange={handleEditorChange} - onDiffChange={canEditDiff ? handleDiffChange : undefined} setIsDirty={setIsDirty} // Context menu props onSplitHorizontal={() => splitPaneHorizontal(tabId, paneId, path)} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileEditorContextMenu/FileEditorContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileEditorContextMenu/FileEditorContextMenu.tsx index 6ef7b78d06f..015e85c2a60 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileEditorContextMenu/FileEditorContextMenu.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileEditorContextMenu/FileEditorContextMenu.tsx @@ -1,11 +1,14 @@ -import type * as Monaco from "monaco-editor"; import { type MutableRefObject, type ReactNode, useCallback } from "react"; import type { Tab } from "renderer/stores/tabs/types"; -import { EditorContextMenu, useEditorActions } from "../../../../../components"; +import { + type CodeEditorAdapter, + EditorContextMenu, + useEditorActions, +} from "../../../../../components"; interface FileEditorContextMenuProps { children: ReactNode; - editorRef: MutableRefObject; + editorRef: MutableRefObject; filePath: string; onSplitHorizontal: () => void; onSplitVertical: () => void; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx index 4b980bbbcea..51ab641849d 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx @@ -1,27 +1,19 @@ -import Editor, { type OnMount } from "@monaco-editor/react"; -import type * as Monaco from "monaco-editor"; import { type MutableRefObject, type RefObject, - useCallback, useEffect, useRef, } from "react"; import { LuLoader } from "react-icons/lu"; import { MarkdownRenderer } from "renderer/components/MarkdownRenderer"; -import { - registerSaveAction, - SUPERSET_THEME, - useMonacoEditorOptions, - useMonacoReady, -} from "renderer/providers/MonacoProvider"; +import { LightDiffViewer } from "renderer/screens/main/components/WorkspaceView/ChangesContent/components/LightDiffViewer"; +import type { CodeEditorAdapter } from "renderer/screens/main/components/WorkspaceView/ContentView/components"; +import { CodeEditor } from "renderer/screens/main/components/WorkspaceView/components/CodeEditor"; import type { Tab } from "renderer/stores/tabs/types"; import type { DiffViewMode } from "shared/changes-types"; import { detectLanguage } from "shared/detect-language"; import { isImageFile } from "shared/file-types"; import type { FileViewerMode } from "shared/tabs-types"; -import { DiffViewer } from "../../../../../../ChangesContent/components/DiffViewer"; -import { registerCopyPathLineAction } from "../../../../../components/EditorContextMenu"; import { FileEditorContextMenu } from "../FileEditorContextMenu"; import { MarkdownSearch } from "../MarkdownSearch"; @@ -75,8 +67,7 @@ interface FileViewerContentProps { rawFileData: RawFileResult; imageData?: ImageResult; diffData: DiffData | undefined; - isDiffEditable: boolean; - editorRef: MutableRefObject; + editorRef: MutableRefObject; originalContentRef: MutableRefObject; draftContentRef: MutableRefObject; initialLine?: number; @@ -84,11 +75,8 @@ interface FileViewerContentProps { diffViewMode: DiffViewMode; hideUnchangedRegions: boolean; onSaveRaw: () => Promise; - onSaveDiff?: (content: string) => Promise; onEditorChange: (value: string | undefined) => void; - onDiffChange?: (content: string) => void; setIsDirty: (dirty: boolean) => void; - // Context menu props onSplitHorizontal: () => void; onSplitVertical: () => void; onSplitWithNewChat?: () => void; @@ -98,7 +86,6 @@ interface FileViewerContentProps { availableTabs: Tab[]; onMoveToTab: (tabId: string) => void; onMoveToNewTab: () => void; - // Markdown search props markdownContainerRef: RefObject; markdownSearch: { isSearchOpen: boolean; @@ -123,7 +110,6 @@ export function FileViewerContent({ rawFileData, imageData, diffData, - isDiffEditable, editorRef, originalContentRef, draftContentRef, @@ -132,11 +118,8 @@ export function FileViewerContent({ diffViewMode, hideUnchangedRegions, onSaveRaw, - onSaveDiff, onEditorChange, - onDiffChange, setIsDirty, - // Context menu props onSplitHorizontal, onSplitVertical, onSplitWithNewChat, @@ -146,13 +129,10 @@ export function FileViewerContent({ availableTabs, onMoveToTab, onMoveToNewTab, - // Markdown search props markdownContainerRef, markdownSearch, }: FileViewerContentProps) { const isImage = isImageFile(filePath); - const isMonacoReady = useMonacoReady(); - const monacoEditorOptions = useMonacoEditorOptions(); const hasAppliedInitialLocationRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: Reset on file change only @@ -160,30 +140,27 @@ export function FileViewerContent({ hasAppliedInitialLocationRef.current = false; }, [filePath]); - // biome-ignore lint/correctness/useExhaustiveDependencies: Only reset when coordinates change + // biome-ignore lint/correctness/useExhaustiveDependencies: Reset when requested cursor target changes useEffect(() => { hasAppliedInitialLocationRef.current = false; }, [initialLine, initialColumn]); - const handleEditorMount: OnMount = useCallback( - (editor) => { - editorRef.current = editor; - if (!draftContentRef.current) { - originalContentRef.current = editor.getValue(); - } - setIsDirty(editor.getValue() !== originalContentRef.current); - registerSaveAction(editor, onSaveRaw); - registerCopyPathLineAction(editor, filePath); - }, - [ - onSaveRaw, - editorRef, - originalContentRef, - draftContentRef, - setIsDirty, - filePath, - ], - ); + useEffect(() => { + if (viewMode !== "raw") return; + if (isLoadingRaw) return; + if (!rawFileData?.ok) return; + if (draftContentRef.current !== null) return; + + originalContentRef.current = rawFileData.content; + setIsDirty(false); + }, [ + viewMode, + isLoadingRaw, + rawFileData, + draftContentRef, + originalContentRef, + setIsDirty, + ]); useEffect(() => { if ( @@ -197,80 +174,53 @@ export function FileViewerContent({ return; } - const editor = editorRef.current; - const model = editor.getModel(); - if (!model) return; - - const lineCount = model.getLineCount(); - const safeLine = Math.max(1, Math.min(initialLine, lineCount)); - const maxColumn = model.getLineMaxColumn(safeLine); - const safeColumn = Math.max(1, Math.min(initialColumn ?? 1, maxColumn)); - - const position = { lineNumber: safeLine, column: safeColumn }; - editor.setPosition(position); - editor.revealPositionInCenter(position); - editor.focus(); - + editorRef.current.revealPosition(initialLine, initialColumn ?? 1); hasAppliedInitialLocationRef.current = true; }, [ viewMode, + editorRef, initialLine, initialColumn, isLoadingRaw, rawFileData, - editorRef, ]); if (viewMode === "diff") { if (isLoadingDiff) { return ( -
+
Loading diff...
); } + if (!diffData) { return ( -
+
No diff available
); } + return ( - +
+ +
); } - // Handle image files in rendered mode if (viewMode === "rendered" && isImage) { if (isLoadingImage) { return ( -
- +
+ Loading image...
); @@ -287,19 +237,20 @@ export function FileViewerContent({ : imageData?.reason === "not-image" ? "Not a supported image format" : "Image not found"; + return ( -
+
{errorMessage}
); } return ( -
+
{filePath.split("/").pop()
@@ -308,7 +259,7 @@ export function FileViewerContent({ if (isLoadingRaw) { return ( -
+
Loading...
); @@ -325,8 +276,9 @@ export function FileViewerContent({ : rawFileData?.reason === "symlink-escape" ? "File is a symlink pointing outside worktree" : "File not found"; + return ( -
+
{errorMessage}
); @@ -347,22 +299,13 @@ export function FileViewerContent({ onFindPrevious={markdownSearch.findPrevious} onClose={markdownSearch.closeSearch} /> -
+
); } - if (!isMonacoReady) { - return ( -
- - Loading editor... -
- ); - } - return ( -
- + - - Loading editor... -
- } - options={{ - ...monacoEditorOptions, - contextmenu: false, // Disable Monaco's native context menu to use our custom one + onSave={() => { + void onSaveRaw(); }} + editorRef={editorRef} + fillHeight />
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/hooks/useFileSave/useFileSave.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/hooks/useFileSave/useFileSave.ts index 2473264172b..b9886a164f2 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/hooks/useFileSave/useFileSave.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/hooks/useFileSave/useFileSave.ts @@ -1,15 +1,15 @@ -import type * as Monaco from "monaco-editor"; import { type MutableRefObject, useCallback, useRef } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useTabsStore } from "renderer/stores/tabs/store"; import type { ChangeCategory } from "shared/changes-types"; +import type { CodeEditorAdapter } from "../../../../../components"; interface UseFileSaveParams { worktreePath: string; filePath: string; paneId: string; diffCategory?: ChangeCategory; - editorRef: MutableRefObject; + editorRef: MutableRefObject; originalContentRef: MutableRefObject; originalDiffContentRef: MutableRefObject; draftContentRef: MutableRefObject; @@ -28,7 +28,6 @@ export function useFileSave({ setIsDirty, }: UseFileSaveParams) { const savingFromRawRef = useRef(false); - const savingDiffContentRef = useRef(null); const utils = electronTrpc.useUtils(); const saveFileMutation = electronTrpc.changes.saveFile.useMutation({ @@ -37,14 +36,11 @@ export function useFileSave({ if (editorRef.current) { originalContentRef.current = editorRef.current.getValue(); } - if (savingDiffContentRef.current !== null) { - originalDiffContentRef.current = savingDiffContentRef.current; - savingDiffContentRef.current = null; - } if (savingFromRawRef.current) { draftContentRef.current = null; } savingFromRawRef.current = false; + originalDiffContentRef.current = ""; utils.changes.readWorkingFile.invalidate(); utils.changes.getFileContents.invalidate(); @@ -81,23 +77,8 @@ export function useFileSave({ }); }, [worktreePath, filePath, saveFileMutation, editorRef]); - const handleSaveDiff = useCallback( - async (content: string) => { - if (!filePath || !worktreePath) return; - savingFromRawRef.current = false; - savingDiffContentRef.current = content; - await saveFileMutation.mutateAsync({ - worktreePath, - filePath, - content, - }); - }, - [worktreePath, filePath, saveFileMutation], - ); - return { handleSaveRaw, - handleSaveDiff, isSaving: saveFileMutation.isPending, }; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/CodeEditorAdapter/CodeEditorAdapter.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/CodeEditorAdapter/CodeEditorAdapter.ts new file mode 100644 index 00000000000..17aea0f719c --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/CodeEditorAdapter/CodeEditorAdapter.ts @@ -0,0 +1,18 @@ +export interface EditorSelectionLines { + startLine: number; + endLine: number; +} + +export interface CodeEditorAdapter { + focus(): void; + getValue(): string; + setValue(value: string): void; + revealPosition(line: number, column?: number): void; + getSelectionLines(): EditorSelectionLines | null; + selectAll(): void; + cut(): void; + copy(): void; + paste(): void; + openFind(): void; + dispose(): void; +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/CodeEditorAdapter/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/CodeEditorAdapter/index.ts new file mode 100644 index 00000000000..8c65e00e552 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/CodeEditorAdapter/index.ts @@ -0,0 +1,4 @@ +export type { + CodeEditorAdapter, + EditorSelectionLines, +} from "./CodeEditorAdapter"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/EditorContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/EditorContextMenu.tsx index 7bdde4069ae..6a2429b0432 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/EditorContextMenu.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/EditorContextMenu.tsx @@ -13,7 +13,6 @@ import { LuFile, LuLink, LuMousePointerClick, - LuReplace, LuScissors, LuSearch, } from "react-icons/lu"; @@ -31,7 +30,6 @@ export interface EditorActions { onCopyPath?: () => void; onCopyPathWithLine?: () => void; onFind?: () => void; - onChangeAllOccurrences?: () => void; } export type PaneActions = PaneContextMenuActions; @@ -59,7 +57,6 @@ export function EditorContextMenu({ onCopyPath, onCopyPathWithLine, onFind, - onChangeAllOccurrences, } = editorActions; const showCutPaste = !!onCut && !!onPaste; @@ -103,15 +100,6 @@ export function EditorContextMenu({ - {/* Editor Actions */} - {onChangeAllOccurrences && ( - - - Change All Occurrences - {cmdKey}+Shift+L - - )} - Select All diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/editor-actions.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/editor-actions.ts deleted file mode 100644 index 7ffb3ae23bf..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/editor-actions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type * as Monaco from "monaco-editor"; -import { monaco } from "renderer/providers/MonacoProvider"; - -/** - * Registers a keyboard shortcut (Cmd+Shift+C / Ctrl+Shift+C) to copy - * the file path with the current line number(s) to the clipboard. - * - * Format: `path/to/file.ts:42` or `path/to/file.ts:42-50` for multi-line selections - */ -export function registerCopyPathLineAction( - editor: Monaco.editor.IStandaloneCodeEditor, - filePath: string, -) { - editor.addAction({ - id: "copy-path-line", - label: "Copy Path:Line", - keybindings: [ - monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyC, - ], - run: (ed) => { - const selection = ed.getSelection(); - if (!selection) return; - - const { startLineNumber, endLineNumber } = selection; - const pathWithLine = - startLineNumber === endLineNumber - ? `${filePath}:${startLineNumber}` - : `${filePath}:${startLineNumber}-${endLineNumber}`; - - navigator.clipboard.writeText(pathWithLine); - }, - }); -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/index.ts index 9d9baaaf8fe..de610c030b1 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/index.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/index.ts @@ -1,4 +1,3 @@ export type { EditorActions, PaneActions } from "./EditorContextMenu"; export { EditorContextMenu } from "./EditorContextMenu"; -export { registerCopyPathLineAction } from "./editor-actions"; export { useEditorActions } from "./useEditorActions"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/useEditorActions.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/useEditorActions.ts index e1419ad2e8e..2e05100e359 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/useEditorActions.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/useEditorActions.ts @@ -1,10 +1,10 @@ import { toast } from "@superset/ui/sonner"; -import type * as Monaco from "monaco-editor"; import { useCallback } from "react"; +import type { CodeEditorAdapter } from "../CodeEditorAdapter"; import type { EditorActions } from "./EditorContextMenu"; interface UseEditorActionsProps { - getEditor: () => Monaco.editor.IStandaloneCodeEditor | null | undefined; + getEditor: () => CodeEditorAdapter | null | undefined; filePath: string; /** If true, includes cut/paste actions (for editable editors) */ editable?: boolean; @@ -12,11 +12,7 @@ interface UseEditorActionsProps { /** * Hook that creates all editor action handlers for the context menu. - * Shared between FileEditorContextMenu and DiffViewer. - * - * Note: Standalone Monaco editor doesn't include language service features - * like Go to Definition, References, Rename, etc. Those require language - * providers to be registered. We only expose actions that are actually available. + * Shared by editor surfaces that operate through the adapter contract. */ export function useEditorActions({ getEditor, @@ -26,33 +22,25 @@ export function useEditorActions({ const handleCut = useCallback(() => { const editor = getEditor(); if (!editor) return; - editor.focus(); - editor.trigger("contextMenu", "editor.action.clipboardCutAction", null); + editor.cut(); }, [getEditor]); const handleCopy = useCallback(() => { const editor = getEditor(); if (!editor) return; - editor.focus(); - editor.trigger("contextMenu", "editor.action.clipboardCopyAction", null); + editor.copy(); }, [getEditor]); const handlePaste = useCallback(() => { const editor = getEditor(); if (!editor) return; - editor.focus(); - editor.trigger("contextMenu", "editor.action.clipboardPasteAction", null); + editor.paste(); }, [getEditor]); const handleSelectAll = useCallback(() => { const editor = getEditor(); if (!editor) return; - editor.focus(); - const model = editor.getModel(); - if (model) { - const fullRange = model.getFullModelRange(); - editor.setSelection(fullRange); - } + editor.selectAll(); }, [getEditor]); const handleCopyPath = useCallback(async () => { @@ -89,7 +77,7 @@ export function useEditorActions({ return; } - const selection = editor.getSelection(); + const selection = editor.getSelectionLines(); if (!selection) { console.error( "[handleCopyPathWithLine] Selection is missing, falling back to filePath only", @@ -108,11 +96,11 @@ export function useEditorActions({ return; } - const { startLineNumber, endLineNumber } = selection; + const { startLine, endLine } = selection; const pathWithLine = - startLineNumber === endLineNumber - ? `${filePath}:${startLineNumber}` - : `${filePath}:${startLineNumber}-${endLineNumber}`; + startLine === endLine + ? `${filePath}:${startLine}` + : `${filePath}:${startLine}-${endLine}`; try { await navigator.clipboard.writeText(pathWithLine); @@ -130,16 +118,7 @@ export function useEditorActions({ const handleFind = useCallback(() => { const editor = getEditor(); if (!editor) return; - editor.focus(); - editor.trigger("contextMenu", "actions.find", null); - }, [getEditor]); - - const handleChangeAllOccurrences = useCallback(() => { - const editor = getEditor(); - if (!editor) return; - editor.focus(); - // Use selectHighlights which is available in standalone Monaco - editor.trigger("contextMenu", "editor.action.selectHighlights", null); + editor.openFind(); }, [getEditor]); return { @@ -150,6 +129,5 @@ export function useEditorActions({ onCopyPath: handleCopyPath, onCopyPathWithLine: handleCopyPathWithLine, onFind: handleFind, - onChangeAllOccurrences: handleChangeAllOccurrences, }; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/index.ts index 54e93875ce5..55ed598d39b 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/index.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/index.ts @@ -1,3 +1,7 @@ +export type { + CodeEditorAdapter, + EditorSelectionLines, +} from "./CodeEditorAdapter"; export type { EditorActions, PaneActions } from "./EditorContextMenu"; export { EditorContextMenu, useEditorActions } from "./EditorContextMenu"; export type { PaneContextMenuActions } from "./PaneContextMenuItems"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/CodeEditor.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/CodeEditor.tsx new file mode 100644 index 00000000000..2be71afce24 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/CodeEditor.tsx @@ -0,0 +1,363 @@ +import { + defaultKeymap, + history, + historyKeymap, + indentWithTab, + selectAll, +} from "@codemirror/commands"; +import { bracketMatching, indentOnInput } from "@codemirror/language"; +import { + highlightSelectionMatches, + openSearchPanel, + searchKeymap, +} from "@codemirror/search"; +import { Compartment, EditorSelection, EditorState } from "@codemirror/state"; +import { + drawSelection, + dropCursor, + EditorView, + highlightActiveLine, + highlightActiveLineGutter, + highlightSpecialChars, + keymap, + lineNumbers, +} from "@codemirror/view"; +import { cn } from "@superset/ui/utils"; +import { type MutableRefObject, useEffect, useRef } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import type { CodeEditorAdapter } from "renderer/screens/main/components/WorkspaceView/ContentView/components"; +import { getCodeSyntaxHighlighting } from "renderer/screens/main/components/WorkspaceView/utils/code-theme"; +import { createCodeMirrorTheme } from "./createCodeMirrorTheme"; +import { loadLanguageSupport } from "./loadLanguageSupport"; + +interface CodeEditorProps { + value: string; + language: string; + readOnly?: boolean; + fillHeight?: boolean; + className?: string; + editorRef?: MutableRefObject; + onChange?: (value: string) => void; + onSave?: () => void; +} + +function createCodeMirrorAdapter(view: EditorView): CodeEditorAdapter { + let disposed = false; + + return { + focus() { + view.focus(); + }, + getValue() { + return view.state.doc.toString(); + }, + setValue(value) { + view.dispatch({ + changes: { + from: 0, + to: view.state.doc.length, + insert: value, + }, + }); + }, + revealPosition(line, column = 1) { + const safeLine = Math.max(1, Math.min(line, view.state.doc.lines)); + const lineInfo = view.state.doc.line(safeLine); + const offset = Math.min(column - 1, lineInfo.length); + const anchor = lineInfo.from + Math.max(0, offset); + + view.dispatch({ + selection: EditorSelection.cursor(anchor), + scrollIntoView: true, + }); + view.focus(); + }, + getSelectionLines() { + const selection = view.state.selection.main; + const startLine = view.state.doc.lineAt(selection.from).number; + const endLine = view.state.doc.lineAt(selection.to).number; + return { startLine, endLine }; + }, + selectAll() { + selectAll(view); + }, + cut() { + if (view.state.readOnly) return; + const clipboard = navigator.clipboard; + if (!clipboard) return; + + const selection = view.state.selection.main; + if (selection.empty) return; + + const text = view.state.sliceDoc(selection.from, selection.to); + void clipboard + .writeText(text) + .then(() => { + const currentSelection = view.state.selection.main; + if ( + currentSelection.from !== selection.from || + currentSelection.to !== selection.to + ) { + return; + } + + if (view.state.sliceDoc(selection.from, selection.to) !== text) { + return; + } + + view.dispatch({ + changes: { from: selection.from, to: selection.to, insert: "" }, + }); + }) + .catch((error) => { + console.error("[CodeEditor] Failed to cut selection:", error); + }); + }, + copy() { + const clipboard = navigator.clipboard; + if (!clipboard) return; + + const selection = view.state.selection.main; + if (selection.empty) return; + + void clipboard + .writeText(view.state.sliceDoc(selection.from, selection.to)) + .catch((error) => { + console.error("[CodeEditor] Failed to copy selection:", error); + }); + }, + paste() { + if (view.state.readOnly) return; + const clipboard = navigator.clipboard; + if (!clipboard) return; + + void clipboard + .readText() + .then((text) => { + const selection = view.state.selection.main; + view.dispatch({ + changes: { + from: selection.from, + to: selection.to, + insert: text, + }, + selection: EditorSelection.cursor(selection.from + text.length), + }); + }) + .catch((error) => { + console.error("[CodeEditor] Failed to paste from clipboard:", error); + }); + }, + openFind() { + openSearchPanel(view); + }, + dispose() { + if (disposed) return; + disposed = true; + view.destroy(); + }, + }; +} + +export function CodeEditor({ + value, + language, + readOnly = false, + fillHeight = true, + className, + editorRef, + onChange, + onSave, +}: CodeEditorProps) { + const containerRef = useRef(null); + const viewRef = useRef(null); + const languageCompartment = useRef(new Compartment()).current; + const themeCompartment = useRef(new Compartment()).current; + const editableCompartment = useRef(new Compartment()).current; + const onChangeRef = useRef(onChange); + const onSaveRef = useRef(onSave); + const { data: fontSettings } = electronTrpc.settings.getFontSettings.useQuery( + undefined, + { + staleTime: 30_000, + }, + ); + const editorFontFamily = fontSettings?.editorFontFamily ?? undefined; + const editorFontSize = fontSettings?.editorFontSize ?? undefined; + + onChangeRef.current = onChange; + onSaveRef.current = onSave; + + // biome-ignore lint/correctness/useExhaustiveDependencies: Editor instance is created once and reconfigured via dedicated effects below + useEffect(() => { + if (!containerRef.current) return; + + const updateListener = EditorView.updateListener.of((update) => { + if (!update.docChanged) return; + onChangeRef.current?.(update.state.doc.toString()); + }); + + const saveKeymap = keymap.of([ + { + key: "Mod-s", + run: () => { + onSaveRef.current?.(); + return true; + }, + }, + ]); + + const state = EditorState.create({ + doc: value, + extensions: [ + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + drawSelection(), + dropCursor(), + EditorState.allowMultipleSelections.of(true), + indentOnInput(), + bracketMatching(), + highlightActiveLine(), + highlightSelectionMatches(), + EditorView.lineWrapping, + editableCompartment.of([ + EditorState.readOnly.of(readOnly), + EditorView.editable.of(!readOnly), + ]), + EditorView.contentAttributes.of({ + "data-testid": "code-editor", + spellcheck: "false", + }), + keymap.of([ + indentWithTab, + ...defaultKeymap, + ...historyKeymap, + ...searchKeymap, + ]), + saveKeymap, + themeCompartment.of([ + getCodeSyntaxHighlighting(), + createCodeMirrorTheme( + { + fontFamily: editorFontFamily, + fontSize: editorFontSize, + }, + fillHeight, + ), + ]), + languageCompartment.of([]), + updateListener, + ], + }); + + const view = new EditorView({ + state, + parent: containerRef.current, + }); + const adapter = createCodeMirrorAdapter(view); + + viewRef.current = view; + if (editorRef) { + editorRef.current = adapter; + } + + return () => { + if (editorRef?.current === adapter) { + editorRef.current = null; + } + adapter.dispose(); + viewRef.current = null; + }; + }, []); + + useEffect(() => { + const view = viewRef.current; + if (!view) return; + + const currentValue = view.state.doc.toString(); + if (currentValue === value) return; + + view.dispatch({ + changes: { + from: 0, + to: view.state.doc.length, + insert: value, + }, + }); + }, [value]); + + useEffect(() => { + const view = viewRef.current; + if (!view) return; + + view.dispatch({ + effects: themeCompartment.reconfigure([ + getCodeSyntaxHighlighting(), + createCodeMirrorTheme( + { + fontFamily: editorFontFamily, + fontSize: editorFontSize, + }, + fillHeight, + ), + ]), + }); + }, [editorFontFamily, editorFontSize, fillHeight, themeCompartment]); + + useEffect(() => { + const view = viewRef.current; + if (!view) return; + + view.dispatch({ + effects: editableCompartment.reconfigure([ + EditorState.readOnly.of(readOnly), + EditorView.editable.of(!readOnly), + ]), + }); + }, [editableCompartment, readOnly]); + + useEffect(() => { + let cancelled = false; + + void loadLanguageSupport(language) + .then((extension) => { + if (cancelled) return; + const view = viewRef.current; + if (!view) return; + + view.dispatch({ + effects: languageCompartment.reconfigure(extension ?? []), + }); + }) + .catch((error) => { + if (cancelled) return; + const view = viewRef.current; + if (!view) return; + + console.error("[CodeEditor] Failed to load language support:", { + error, + language, + }); + view.dispatch({ + effects: languageCompartment.reconfigure([]), + }); + }); + + return () => { + cancelled = true; + }; + }, [language, languageCompartment]); + + return ( +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/constants.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/constants.ts new file mode 100644 index 00000000000..e8bdd960b80 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/constants.ts @@ -0,0 +1,18 @@ +export const DEFAULT_CODE_EDITOR_FONT_FAMILY = + "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace"; +export const DEFAULT_CODE_EDITOR_FONT_SIZE = 13; + +export const MIDNIGHT_CODE_COLORS = { + background: "#282c34", + border: "#21252b", + muted: "#636d83", + selection: "#4b5668", + search: "#e5c07b33", + searchActive: "#e5c07b55", + panel: "#21252b", + surface: "#2c313c", + foreground: "#abb2bf", + addition: "#98c379", + deletion: "#e06c75", + modified: "#61afef", +}; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts new file mode 100644 index 00000000000..3f626371214 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts @@ -0,0 +1,90 @@ +import { EditorView } from "@codemirror/view"; +import { + DEFAULT_CODE_EDITOR_FONT_FAMILY, + DEFAULT_CODE_EDITOR_FONT_SIZE, + MIDNIGHT_CODE_COLORS, +} from "./constants"; + +interface CodeEditorFontSettings { + fontFamily?: string; + fontSize?: number; +} + +export function createCodeMirrorTheme( + fontSettings: CodeEditorFontSettings, + fillHeight: boolean, +) { + const fontSize = fontSettings.fontSize ?? DEFAULT_CODE_EDITOR_FONT_SIZE; + const lineHeight = Math.round(fontSize * 1.5); + + return EditorView.theme( + { + "&": { + height: fillHeight ? "100%" : "auto", + backgroundColor: MIDNIGHT_CODE_COLORS.background, + color: MIDNIGHT_CODE_COLORS.foreground, + fontFamily: fontSettings.fontFamily ?? DEFAULT_CODE_EDITOR_FONT_FAMILY, + fontSize: `${fontSize}px`, + }, + ".cm-scroller": { + fontFamily: "inherit", + lineHeight: `${lineHeight}px`, + overflow: fillHeight ? "auto" : "visible", + }, + ".cm-content": { + padding: "8px 0", + caretColor: MIDNIGHT_CODE_COLORS.foreground, + }, + ".cm-line": { + padding: "0 12px", + }, + ".cm-gutters": { + backgroundColor: MIDNIGHT_CODE_COLORS.background, + color: MIDNIGHT_CODE_COLORS.muted, + borderRight: `1px solid ${MIDNIGHT_CODE_COLORS.border}`, + }, + ".cm-activeLine": { + backgroundColor: MIDNIGHT_CODE_COLORS.surface, + }, + ".cm-activeLineGutter": { + backgroundColor: MIDNIGHT_CODE_COLORS.surface, + }, + "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": + { + backgroundColor: MIDNIGHT_CODE_COLORS.selection, + }, + ".cm-selectionMatch": { + backgroundColor: MIDNIGHT_CODE_COLORS.search, + }, + ".cm-cursor, .cm-dropCursor": { + borderLeftColor: MIDNIGHT_CODE_COLORS.foreground, + }, + ".cm-searchMatch": { + backgroundColor: MIDNIGHT_CODE_COLORS.search, + outline: "none", + }, + ".cm-searchMatch.cm-searchMatch-selected": { + backgroundColor: MIDNIGHT_CODE_COLORS.searchActive, + }, + ".cm-panels": { + backgroundColor: MIDNIGHT_CODE_COLORS.panel, + color: MIDNIGHT_CODE_COLORS.foreground, + borderBottom: `1px solid ${MIDNIGHT_CODE_COLORS.border}`, + }, + ".cm-panels .cm-textfield": { + backgroundColor: MIDNIGHT_CODE_COLORS.background, + color: MIDNIGHT_CODE_COLORS.foreground, + border: `1px solid ${MIDNIGHT_CODE_COLORS.border}`, + }, + ".cm-button": { + backgroundImage: "none", + backgroundColor: MIDNIGHT_CODE_COLORS.surface, + color: MIDNIGHT_CODE_COLORS.foreground, + border: `1px solid ${MIDNIGHT_CODE_COLORS.border}`, + }, + }, + { + dark: true, + }, + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/index.ts new file mode 100644 index 00000000000..a2fc417c8b4 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/index.ts @@ -0,0 +1 @@ +export { CodeEditor } from "./CodeEditor"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/loadLanguageSupport.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/loadLanguageSupport.ts new file mode 100644 index 00000000000..876ca792f05 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/loadLanguageSupport.ts @@ -0,0 +1,127 @@ +import { StreamLanguage, type StreamParser } from "@codemirror/language"; +import type { Extension } from "@codemirror/state"; +import { + graphqlStreamLanguage, + makefileStreamLanguage, +} from "./streamLanguages"; + +async function loadLegacyLanguage( + loader: () => Promise>, + key: string, +): Promise { + const languageModule = await loader(); + return StreamLanguage.define(languageModule[key] as StreamParser); +} + +export async function loadLanguageSupport( + language: string, +): Promise { + switch (language) { + case "typescript": + case "javascript": { + const { javascript } = await import("@codemirror/lang-javascript"); + return javascript({ + typescript: language === "typescript", + jsx: true, + }); + } + case "json": { + const { json } = await import("@codemirror/lang-json"); + return json(); + } + case "html": { + const { html } = await import("@codemirror/lang-html"); + return html(); + } + case "css": + case "scss": + case "less": { + const { css } = await import("@codemirror/lang-css"); + return css(); + } + case "markdown": { + const { markdown } = await import("@codemirror/lang-markdown"); + return markdown(); + } + case "graphql": + return StreamLanguage.define(graphqlStreamLanguage); + case "plaintext": + return null; + case "yaml": { + const { yaml } = await import("@codemirror/lang-yaml"); + return yaml(); + } + case "xml": { + const { xml } = await import("@codemirror/lang-xml"); + return xml(); + } + case "python": { + const { python } = await import("@codemirror/lang-python"); + return python(); + } + case "rust": { + const { rust } = await import("@codemirror/lang-rust"); + return rust(); + } + case "sql": { + const { sql } = await import("@codemirror/lang-sql"); + return sql(); + } + case "php": { + const { php } = await import("@codemirror/lang-php"); + return php(); + } + case "java": { + const { java } = await import("@codemirror/lang-java"); + return java(); + } + case "c": + case "cpp": { + const { cpp } = await import("@codemirror/lang-cpp"); + return cpp(); + } + case "go": { + const { go } = await import("@codemirror/lang-go"); + return go(); + } + case "shell": + return loadLegacyLanguage( + () => import("@codemirror/legacy-modes/mode/shell"), + "shell", + ); + case "dockerfile": + return loadLegacyLanguage( + () => import("@codemirror/legacy-modes/mode/dockerfile"), + "dockerFile", + ); + case "makefile": + return StreamLanguage.define(makefileStreamLanguage); + case "toml": + return loadLegacyLanguage( + () => import("@codemirror/legacy-modes/mode/toml"), + "toml", + ); + case "ruby": + return loadLegacyLanguage( + () => import("@codemirror/legacy-modes/mode/ruby"), + "ruby", + ); + case "swift": + return loadLegacyLanguage( + () => import("@codemirror/legacy-modes/mode/swift"), + "swift", + ); + case "csharp": + return loadLegacyLanguage( + () => import("@codemirror/legacy-modes/mode/clike"), + "csharp", + ); + case "kotlin": + return loadLegacyLanguage( + () => import("@codemirror/legacy-modes/mode/clike"), + "kotlin", + ); + default: + return null; + } +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/streamLanguages.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/streamLanguages.ts new file mode 100644 index 00000000000..7096bb018c7 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/streamLanguages.ts @@ -0,0 +1,232 @@ +import type { StreamParser } from "@codemirror/language"; + +const GRAPHQL_KEYWORDS = new Set([ + "directive", + "enum", + "extend", + "fragment", + "implements", + "input", + "interface", + "mutation", + "on", + "query", + "repeatable", + "scalar", + "schema", + "subscription", + "type", + "union", +]); + +const GRAPHQL_ATOMS = new Set(["false", "null", "true"]); + +interface GraphqlState { + inBlockString: boolean; + inString: boolean; +} + +export const graphqlStreamLanguage: StreamParser = { + name: "graphql", + startState: () => ({ + inBlockString: false, + inString: false, + }), + token(stream, state) { + if (state.inBlockString) { + while (!stream.eol()) { + if (stream.match('"""')) { + state.inBlockString = false; + break; + } + stream.next(); + } + + return "string"; + } + + if (state.inString) { + let escaped = false; + + while (!stream.eol()) { + const next = stream.next(); + if (next === '"' && !escaped) { + state.inString = false; + break; + } + escaped = !escaped && next === "\\"; + } + + return "string"; + } + + if (stream.eatSpace()) return null; + + if (stream.match('"""')) { + while (!stream.eol()) { + if (stream.match('"""')) { + return "string"; + } + stream.next(); + } + + state.inBlockString = true; + return "string"; + } + + const next = stream.next(); + if (!next) return null; + + if (next === "#") { + stream.skipToEnd(); + return "comment"; + } + + if (next === '"') { + let escaped = false; + + while (!stream.eol()) { + const char = stream.next(); + if (char === '"' && !escaped) { + return "string"; + } + escaped = !escaped && char === "\\"; + } + + state.inString = true; + return "string"; + } + + if (next === "$") { + stream.eatWhile(/[_0-9A-Za-z]/); + return "variableName"; + } + + if (next === "@") { + stream.eatWhile(/[_0-9A-Za-z]/); + return "meta"; + } + + if (next === "-" && /\d/.test(stream.peek() ?? "")) { + stream.eatWhile(/\d/); + if (stream.peek() === ".") { + stream.next(); + stream.eatWhile(/\d/); + } + return "number"; + } + + if (/\d/.test(next)) { + stream.eatWhile(/\d/); + if (stream.peek() === ".") { + stream.next(); + stream.eatWhile(/\d/); + } + return "number"; + } + + if (/[A-Za-z_]/.test(next)) { + stream.eatWhile(/[_0-9A-Za-z]/); + const word = stream.current(); + + if (GRAPHQL_KEYWORDS.has(word)) return "keyword"; + if (GRAPHQL_ATOMS.has(word)) return "atom"; + return /^[A-Z]/.test(word) ? "typeName" : "variableName"; + } + + return null; + }, + languageData: { + commentTokens: { line: "#" }, + }, +}; + +const MAKEFILE_DIRECTIVES = new Set([ + "-include", + "define", + "else", + "endef", + "endif", + "export", + "ifdef", + "ifndef", + "ifeq", + "ifneq", + "include", + "override", + "private", + "sinclude", + "undefine", + "unexport", + "vpath", +]); + +function escapeRegex(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +const MAKEFILE_DIRECTIVE_PATTERN = new RegExp( + `^\\s*(?:${[...MAKEFILE_DIRECTIVES].map(escapeRegex).join("|")})\\b`, +); + +export const makefileStreamLanguage: StreamParser = { + name: "makefile", + token(stream) { + if (stream.sol()) { + if (stream.peek() === "\t") { + stream.skipToEnd(); + return "meta"; + } + + if (stream.match(MAKEFILE_DIRECTIVE_PATTERN)) { + return "keyword"; + } + + if (stream.match(/^\s*[A-Za-z_][A-Za-z0-9_.-]*(?=\s*(?::=|\+=|\?=|=))/)) { + return "variableName"; + } + + if (stream.match(/^\s*[^:=#\s][^:=#]*(?=\s*:)/)) { + return "def"; + } + } + + if (stream.eatSpace()) return null; + + if (stream.match(/^\$\(([^)]+)\)/) || stream.match(/^\$\{([^}]+)\}/)) { + return "variableName"; + } + + const next = stream.next(); + if (!next) return null; + + if (next === "#") { + stream.skipToEnd(); + return "comment"; + } + + if (next === ":" && stream.peek() === "=") { + stream.next(); + return "operator"; + } + + if ((next === "+" || next === "?") && stream.peek() === "=") { + stream.next(); + return "operator"; + } + + if (next === "=") { + return "operator"; + } + + if (/[A-Za-z_.-]/.test(next)) { + stream.eatWhile(/[A-Za-z0-9_.-]/); + return MAKEFILE_DIRECTIVES.has(stream.current()) ? "keyword" : null; + } + + return null; + }, + languageData: { + commentTokens: { line: "#" }, + }, +}; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/utils/code-theme.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/utils/code-theme.ts new file mode 100644 index 00000000000..72f9189feee --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/utils/code-theme.ts @@ -0,0 +1,65 @@ +import { syntaxHighlighting } from "@codemirror/language"; +import type { Extension } from "@codemirror/state"; +import { oneDarkHighlightStyle } from "@codemirror/theme-one-dark"; +import type { DiffsThemeNames } from "@pierre/diffs/react"; +import type { CSSProperties } from "react"; +import { + DEFAULT_CODE_EDITOR_FONT_FAMILY, + DEFAULT_CODE_EDITOR_FONT_SIZE, + MIDNIGHT_CODE_COLORS, +} from "../components/CodeEditor/constants"; + +interface CodeThemeFontSettings { + fontFamily?: string; + fontSize?: number; +} + +const MIDNIGHT_DIFF_THEME = { + light: "one-light" as DiffsThemeNames, + dark: "one-dark-pro" as DiffsThemeNames, +}; + +const MIDNIGHT_DIFF_COLORS = { + background: MIDNIGHT_CODE_COLORS.background, + buffer: MIDNIGHT_CODE_COLORS.border, + hover: "#2f343f", + separator: MIDNIGHT_CODE_COLORS.border, + lineNumber: MIDNIGHT_CODE_COLORS.muted, + addition: MIDNIGHT_CODE_COLORS.addition, + deletion: MIDNIGHT_CODE_COLORS.deletion, + modified: MIDNIGHT_CODE_COLORS.modified, + selection: MIDNIGHT_CODE_COLORS.selection, +}; + +export function getDiffsTheme() { + return MIDNIGHT_DIFF_THEME; +} + +export function getCodeSyntaxHighlighting(): Extension { + return syntaxHighlighting(oneDarkHighlightStyle); +} + +export function getDiffViewerStyle( + fontSettings: CodeThemeFontSettings, +): CSSProperties { + const fontFamily = fontSettings.fontFamily ?? DEFAULT_CODE_EDITOR_FONT_FAMILY; + const fontSize = fontSettings.fontSize ?? DEFAULT_CODE_EDITOR_FONT_SIZE; + const lineHeight = Math.round(fontSize * 1.5); + + return { + "--diffs-font-family": fontFamily, + "--diffs-font-size": `${fontSize}px`, + "--diffs-line-height": `${lineHeight}px`, + "--diffs-bg-buffer-override": MIDNIGHT_DIFF_COLORS.buffer, + "--diffs-bg-hover-override": MIDNIGHT_DIFF_COLORS.hover, + "--diffs-bg-context-override": MIDNIGHT_DIFF_COLORS.background, + "--diffs-bg-separator-override": MIDNIGHT_DIFF_COLORS.separator, + "--diffs-fg-number-override": MIDNIGHT_DIFF_COLORS.lineNumber, + "--diffs-addition-color-override": MIDNIGHT_DIFF_COLORS.addition, + "--diffs-deletion-color-override": MIDNIGHT_DIFF_COLORS.deletion, + "--diffs-modified-color-override": MIDNIGHT_DIFF_COLORS.modified, + "--diffs-selection-color-override": MIDNIGHT_DIFF_COLORS.selection, + backgroundColor: MIDNIGHT_DIFF_COLORS.background, + color: MIDNIGHT_CODE_COLORS.foreground, + } as CSSProperties; +} diff --git a/apps/desktop/src/renderer/stores/theme/index.ts b/apps/desktop/src/renderer/stores/theme/index.ts index 4f19eeac5c8..6b28af6a6bd 100644 --- a/apps/desktop/src/renderer/stores/theme/index.ts +++ b/apps/desktop/src/renderer/stores/theme/index.ts @@ -1,6 +1,5 @@ export { SYSTEM_THEME_ID, - useMonacoTheme, useSetTheme, useTerminalTheme, useTheme, diff --git a/apps/desktop/src/renderer/stores/theme/store.ts b/apps/desktop/src/renderer/stores/theme/store.ts index 99879c088a4..3e9983fdc4c 100644 --- a/apps/desktop/src/renderer/stores/theme/store.ts +++ b/apps/desktop/src/renderer/stores/theme/store.ts @@ -9,13 +9,7 @@ import { import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; import { trpcThemeStorage } from "../../lib/trpc-storage"; -import { - applyUIColors, - type MonacoTheme, - toMonacoTheme, - toXtermTheme, - updateThemeClass, -} from "./utils"; +import { applyUIColors, toXtermTheme, updateThemeClass } from "./utils"; /** Special theme ID for system preference (follows OS dark/light mode) */ export const SYSTEM_THEME_ID = "system"; @@ -33,9 +27,6 @@ interface ThemeState { /** Terminal theme in xterm.js format (derived from activeTheme) */ terminalTheme: ITheme | null; - /** Monaco editor theme (derived from activeTheme) */ - monacoTheme: MonacoTheme | null; - /** Set the active theme by ID (can be "system" or a specific theme ID) */ setTheme: (themeId: string) => void; @@ -114,7 +105,6 @@ function syncThemeToLocalStorage(theme: Theme): void { */ function applyTheme(theme: Theme): { terminalTheme: ITheme; - monacoTheme: MonacoTheme; } { // Apply UI colors to CSS variables applyUIColors(theme.ui); @@ -127,7 +117,6 @@ function applyTheme(theme: Theme): { // Convert to editor-specific formats return { terminalTheme: toXtermTheme(getTerminalColors(theme)), - monacoTheme: toMonacoTheme(theme), }; } @@ -139,7 +128,6 @@ export const useThemeStore = create()( customThemes: [], activeTheme: null, terminalTheme: null, - monacoTheme: null, setTheme: (themeId: string) => { const state = get(); @@ -152,13 +140,12 @@ export const useThemeStore = create()( return; } - const { terminalTheme, monacoTheme } = applyTheme(theme); + const { terminalTheme } = applyTheme(theme); set({ activeThemeId: themeId, // Store the original ID (could be "system") activeTheme: theme, // Store the resolved theme terminalTheme, - monacoTheme, }); }, @@ -204,12 +191,11 @@ export const useThemeStore = create()( return { added, updated, skipped }; } - const { terminalTheme, monacoTheme } = applyTheme(resolvedTheme); + const { terminalTheme } = applyTheme(resolvedTheme); set({ customThemes, activeTheme: resolvedTheme, terminalTheme, - monacoTheme, }); return { added, updated, skipped }; @@ -247,11 +233,10 @@ export const useThemeStore = create()( const theme = findTheme(resolvedId, state.customThemes); if (theme) { - const { terminalTheme, monacoTheme } = applyTheme(theme); + const { terminalTheme } = applyTheme(theme); set({ activeTheme: theme, terminalTheme, - monacoTheme, }); } else { state.setTheme(DEFAULT_THEME_ID); @@ -295,6 +280,5 @@ export const useThemeStore = create()( export const useTheme = () => useThemeStore((state) => state.activeTheme); export const useTerminalTheme = () => useThemeStore((state) => state.terminalTheme); -export const useMonacoTheme = () => useThemeStore((state) => state.monacoTheme); export const useSetTheme = () => useThemeStore((state) => state.setTheme); export const useThemeId = () => useThemeStore((state) => state.activeThemeId); diff --git a/apps/desktop/src/renderer/stores/theme/utils/index.ts b/apps/desktop/src/renderer/stores/theme/utils/index.ts index 3bbe3434ac0..8209e26c506 100644 --- a/apps/desktop/src/renderer/stores/theme/utils/index.ts +++ b/apps/desktop/src/renderer/stores/theme/utils/index.ts @@ -3,7 +3,5 @@ export { clearThemeVariables, updateThemeClass, } from "./css-variables"; -export type { MonacoTheme } from "./monaco-theme"; -export { toMonacoTheme } from "./monaco-theme"; export { toXtermTheme } from "./terminal-theme"; export { resolveTerminalThemeType } from "./terminal-theme-type"; diff --git a/apps/desktop/src/renderer/stores/theme/utils/monaco-theme.ts b/apps/desktop/src/renderer/stores/theme/utils/monaco-theme.ts deleted file mode 100644 index 4527a9087a6..00000000000 --- a/apps/desktop/src/renderer/stores/theme/utils/monaco-theme.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { editor } from "monaco-editor"; -import { getTerminalColors, type Theme } from "shared/themes"; -import { toHexAuto, withAlpha } from "shared/themes/utils"; - -export interface MonacoTheme { - base: "vs" | "vs-dark" | "hc-black"; - inherit: boolean; - rules: editor.ITokenThemeRule[]; - colors: editor.IColors; -} - -function createEditorColors(theme: Theme): editor.IColors { - const terminal = getTerminalColors(theme); - const { ui } = theme; - const hex = toHexAuto; - const alpha = withAlpha; - - const selectionBg = terminal.selectionBackground - ? hex(terminal.selectionBackground) - : alpha(terminal.foreground, 0.2); - - return { - "editor.background": hex(terminal.background), - "editor.foreground": hex(terminal.foreground), - "editor.lineHighlightBackground": hex(ui.accent), - "editor.lineHighlightBorder": "#00000000", - "editor.selectionBackground": selectionBg, - "editor.selectionHighlightBackground": alpha(terminal.blue, 0.2), - "editor.inactiveSelectionBackground": alpha(terminal.foreground, 0.1), - "editor.findMatchBackground": alpha(terminal.yellow, 0.27), - "editor.findMatchHighlightBackground": alpha(terminal.yellow, 0.13), - - "editorLineNumber.foreground": hex(terminal.brightBlack), - "editorLineNumber.activeForeground": hex(terminal.foreground), - "editorGutter.background": hex(terminal.background), - "editorCursor.foreground": hex(terminal.cursor), - - "diffEditor.insertedTextBackground": alpha(terminal.green, 0.2), - "diffEditor.removedTextBackground": alpha(terminal.red, 0.2), - "diffEditor.insertedLineBackground": alpha(terminal.green, 0.2), - "diffEditor.removedLineBackground": alpha(terminal.red, 0.2), - "diffEditorGutter.insertedLineBackground": alpha(terminal.green, 0.15), - "diffEditorGutter.removedLineBackground": alpha(terminal.red, 0.15), - "diffEditor.diagonalFill": hex(ui.border), - - "scrollbar.shadow": "#00000000", - "scrollbarSlider.background": alpha(terminal.foreground, 0.13), - "scrollbarSlider.hoverBackground": alpha(terminal.foreground, 0.2), - "scrollbarSlider.activeBackground": alpha(terminal.foreground, 0.27), - - "editorWidget.background": hex(ui.popover), - "editorWidget.foreground": hex(ui.popoverForeground), - "editorWidget.border": hex(ui.border), - - "editorBracketMatch.background": alpha(terminal.cyan, 0.2), - "editorBracketMatch.border": hex(terminal.cyan), - - "editorIndentGuide.background": alpha(terminal.foreground, 0.08), - "editorIndentGuide.activeBackground": alpha(terminal.foreground, 0.2), - "editorWhitespace.foreground": alpha(terminal.foreground, 0.13), - "editorOverviewRuler.border": "#00000000", - }; -} - -function createTokenRules(theme: Theme): editor.ITokenThemeRule[] { - const terminal = getTerminalColors(theme); - const hex = (color: string) => toHexAuto(color).slice(1); - - return [ - // Markdown - { token: "keyword.md", foreground: hex(terminal.blue) }, - { token: "string.link.md", foreground: hex(terminal.cyan) }, - { token: "variable.md", foreground: hex(terminal.blue) }, - { token: "string.md", foreground: hex(terminal.green) }, - { token: "variable.source.md", foreground: hex(terminal.foreground) }, - { token: "markup.bold.md", fontStyle: "bold" }, - { token: "markup.italic.md", fontStyle: "italic" }, - { token: "markup.strikethrough.md", fontStyle: "strikethrough" }, - ]; -} - -export function toMonacoTheme(theme: Theme): MonacoTheme { - const isDark = theme.type === "dark"; - return { - base: isDark ? "vs-dark" : "vs", - inherit: true, - rules: createTokenRules(theme), - colors: createEditorColors(theme), - }; -} diff --git a/apps/desktop/src/shared/changes-types.ts b/apps/desktop/src/shared/changes-types.ts index ad824b91e9a..8318d4cc7fc 100644 --- a/apps/desktop/src/shared/changes-types.ts +++ b/apps/desktop/src/shared/changes-types.ts @@ -75,7 +75,7 @@ export interface FileDiffInput { commitHash?: string; // For committed category: which commit to show } -/** File contents for Monaco diff editor */ +/** File contents for diff viewer */ export interface FileContents { original: string; // Original content (before changes) modified: string; // Modified content (after changes) diff --git a/bun.lock b/bun.lock index 1b004d4e358..5b713c41ab3 100644 --- a/bun.lock +++ b/bun.lock @@ -116,6 +116,27 @@ "@ai-sdk/react": "^3.0.0", "@ast-grep/napi": "^0.41.0", "@better-auth/stripe": "1.4.18", + "@codemirror/commands": "^6.10.2", + "@codemirror/lang-cpp": "^6.0.3", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-java": "^6.0.2", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-php": "^6.0.2", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.2", + "@codemirror/legacy-modes": "^6.5.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.16", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -124,8 +145,8 @@ "@headless-tree/core": "^1.6.3", "@headless-tree/react": "^1.6.3", "@hookform/resolvers": "^5.2.2", + "@lezer/highlight": "^1.2.3", "@mastra/core": "^1.3.0", - "@monaco-editor/react": "^4.7.0", "@outlit/browser": "^1.4.3", "@outlit/node": "^1.4.3", "@pierre/diffs": "^1.0.10", @@ -226,7 +247,6 @@ "lowlight": "^3.3.0", "lucide-react": "^0.563.0", "mastracode": "^0.4.0", - "monaco-editor": "^0.55.1", "nanoid": "^5.1.6", "node-addon-api": "^7.1.0", "node-pty": "1.1.0", @@ -1178,6 +1198,52 @@ "@code-inspector/webpack": ["@code-inspector/webpack@1.4.0", "", { "dependencies": { "@code-inspector/core": "1.4.0" } }, "sha512-ClUGCuowdLx36cTVPrz9E3TTP0WlTn3mSw4XPEQ9XWcEQpixFtImBxIRpC4xyztkYENTfEWZDy7mKxE4lthmDQ=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="], + + "@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="], + + "@codemirror/lang-cpp": ["@codemirror/lang-cpp@6.0.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/cpp": "^1.0.0" } }, "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA=="], + + "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="], + + "@codemirror/lang-go": ["@codemirror/lang-go@6.0.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/go": "^1.0.0" } }, "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg=="], + + "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="], + + "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="], + + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="], + + "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="], + + "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="], + + "@codemirror/lang-php": ["@codemirror/lang-php@6.0.2", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/php": "^1.0.0" } }, "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA=="], + + "@codemirror/lang-python": ["@codemirror/lang-python@6.2.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.3.2", "@codemirror/language": "^6.8.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/python": "^1.1.4" } }, "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw=="], + + "@codemirror/lang-rust": ["@codemirror/lang-rust@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/rust": "^1.0.0" } }, "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA=="], + + "@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="], + + "@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="], + + "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="], + + "@codemirror/language": ["@codemirror/language@6.12.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="], + + "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="], + + "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="], + + "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], + + "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="], + + "@codemirror/theme-one-dark": ["@codemirror/theme-one-dark@6.1.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA=="], + + "@codemirror/view": ["@codemirror/view@6.39.16", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q=="], + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], @@ -1462,6 +1528,38 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="], + + "@lezer/cpp": ["@lezer/cpp@1.1.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw=="], + + "@lezer/css": ["@lezer/css@1.3.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg=="], + + "@lezer/go": ["@lezer/go@1.0.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], + + "@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="], + + "@lezer/java": ["@lezer/java@1.1.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw=="], + + "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="], + + "@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="], + + "@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="], + + "@lezer/markdown": ["@lezer/markdown@1.6.3", "", { "dependencies": { "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw=="], + + "@lezer/php": ["@lezer/php@1.0.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.1.0" } }, "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA=="], + + "@lezer/python": ["@lezer/python@1.1.18", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg=="], + + "@lezer/rust": ["@lezer/rust@1.0.2", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg=="], + + "@lezer/xml": ["@lezer/xml@1.0.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="], + + "@lezer/yaml": ["@lezer/yaml@1.0.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw=="], + "@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="], "@libsql/core": ["@libsql/core@0.15.15", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA=="], @@ -1500,6 +1598,8 @@ "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@mariozechner/pi-tui": ["@mariozechner/pi-tui@0.56.2", "", { "dependencies": { "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", "marked": "^15.0.12", "mime-types": "^3.0.1" }, "optionalDependencies": { "koffi": "^2.9.0" } }, "sha512-UsbJNeyRnUqEp5AOCxNB/1EOCSN4ZpA/Irbbu1DLCzBPPGMrQnSp9mcFcuwqzw/t2P7VqGxY4n0hGkiAKbvgpQ=="], "@mastra/ai-sdk": ["@mastra/ai-sdk@1.0.5", "", { "peerDependencies": { "@mastra/core": ">=1.0.0-0 <2.0.0-0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ApezR259f5CtNNwIvbzE+GzwMKWEA1mUvXVLgbZ4cDz376oGxZYjrdvlOPgIXAezQYggg1US89LRFfOK8unnhg=="], @@ -1528,10 +1628,6 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], - "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], - - "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], - "@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.4.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg=="], "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], @@ -4040,7 +4136,7 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + "marked": ["marked@17.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA=="], "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], @@ -4252,8 +4348,6 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], - "motion": ["motion@12.29.0", "", { "dependencies": { "framer-motion": "^12.29.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rjB5CP2N9S2ESAyEFnAFMgTec6X8yvfxLNcz8n12gPq3M48R7ZbBeVYkDOTj8SPMwfvGIFI801SiPSr1+HCr9g=="], "motion-dom": ["motion-dom@12.29.0", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="], @@ -4988,8 +5082,6 @@ "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], - "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], - "stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="], "stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="], @@ -5042,6 +5134,8 @@ "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -6288,8 +6382,6 @@ "stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="], - "streamdown/marked": ["marked@17.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA=="], - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],