mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
feat(editor): integrate Monaco Editor for local file editing and enhance language inference
This commit is contained in:
parent
864f6f798a
commit
bbc1c76c0d
5 changed files with 166 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||
import { toast } from "sonner";
|
||||
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { VersionHistoryButton } from "@/components/documents/version-history";
|
||||
import { LocalFileMonaco } from "@/components/editor/local-file-monaco";
|
||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -14,6 +15,7 @@ import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/u
|
|||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { inferMonacoLanguageFromPath } from "@/lib/editor-language";
|
||||
|
||||
const PlateEditor = dynamic(
|
||||
() => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
|
||||
|
|
@ -77,6 +79,7 @@ export function EditorPanelContent({
|
|||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const [editedMarkdown, setEditedMarkdown] = useState<string | null>(null);
|
||||
const [localFileContent, setLocalFileContent] = useState("");
|
||||
const markdownRef = useRef<string>("");
|
||||
const initialLoadDone = useRef(false);
|
||||
const changeCountRef = useRef(0);
|
||||
|
|
@ -91,6 +94,7 @@ export function EditorPanelContent({
|
|||
setError(null);
|
||||
setEditorDoc(null);
|
||||
setEditedMarkdown(null);
|
||||
setLocalFileContent("");
|
||||
initialLoadDone.current = false;
|
||||
changeCountRef.current = 0;
|
||||
|
||||
|
|
@ -115,6 +119,7 @@ export function EditorPanelContent({
|
|||
source_markdown: readResult.content,
|
||||
};
|
||||
markdownRef.current = content.source_markdown;
|
||||
setLocalFileContent(content.source_markdown);
|
||||
setDisplayTitle(title || inferredTitle);
|
||||
setEditorDoc(content);
|
||||
initialLoadDone.current = true;
|
||||
|
|
@ -244,6 +249,7 @@ export function EditorPanelContent({
|
|||
? (isLocalFileMode || EDITABLE_DOCUMENT_TYPES.has(editorDoc.document_type ?? "")) &&
|
||||
!isLargeDocument
|
||||
: false;
|
||||
const localFileLanguage = inferMonacoLanguageFromPath(localFilePath);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -348,6 +354,20 @@ export function EditorPanelContent({
|
|||
</Alert>
|
||||
<MarkdownViewer content={editorDoc.source_markdown} />
|
||||
</div>
|
||||
) : isLocalFileMode ? (
|
||||
<div className="h-full overflow-hidden">
|
||||
<LocalFileMonaco
|
||||
filePath={localFilePath ?? "local-file.txt"}
|
||||
language={localFileLanguage}
|
||||
value={localFileContent}
|
||||
onChange={(next) => {
|
||||
markdownRef.current = next;
|
||||
setLocalFileContent(next);
|
||||
if (!initialLoadDone.current) return;
|
||||
setEditedMarkdown(next === (editorDoc?.source_markdown ?? "") ? null : next);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : isEditableType ? (
|
||||
<PlateEditor
|
||||
key={isLocalFileMode ? localFilePath ?? "local-file" : documentId}
|
||||
|
|
|
|||
56
surfsense_web/components/editor/local-file-monaco.tsx
Normal file
56
surfsense_web/components/editor/local-file-monaco.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
interface LocalFileMonacoProps {
|
||||
filePath: string;
|
||||
language: string;
|
||||
value: string;
|
||||
onChange: (next: string) => void;
|
||||
}
|
||||
|
||||
export function LocalFileMonaco({ filePath, language, value, onChange }: LocalFileMonacoProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden bg-sidebar">
|
||||
<MonacoEditor
|
||||
path={filePath}
|
||||
language={language}
|
||||
value={value}
|
||||
theme={resolvedTheme === "dark" ? "vs-dark" : "vs"}
|
||||
onChange={(next) => onChange(next ?? "")}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: "on",
|
||||
lineNumbersMinChars: 3,
|
||||
lineDecorationsWidth: 12,
|
||||
glyphMargin: false,
|
||||
folding: true,
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "off",
|
||||
scrollbar: {
|
||||
vertical: "hidden",
|
||||
horizontal: "hidden",
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
tabSize: 2,
|
||||
insertSpaces: true,
|
||||
fontSize: 12,
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace",
|
||||
renderWhitespace: "selection",
|
||||
smoothScrolling: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
surfsense_web/lib/editor-language.ts
Normal file
34
surfsense_web/lib/editor-language.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
const EXTENSION_TO_MONACO_LANGUAGE: Record<string, string> = {
|
||||
css: "css",
|
||||
csv: "plaintext",
|
||||
cjs: "javascript",
|
||||
html: "html",
|
||||
htm: "html",
|
||||
ini: "ini",
|
||||
js: "javascript",
|
||||
json: "json",
|
||||
markdown: "markdown",
|
||||
md: "markdown",
|
||||
mjs: "javascript",
|
||||
py: "python",
|
||||
sql: "sql",
|
||||
toml: "plaintext",
|
||||
ts: "typescript",
|
||||
tsx: "typescript",
|
||||
xml: "xml",
|
||||
yaml: "yaml",
|
||||
yml: "yaml",
|
||||
};
|
||||
|
||||
export function inferMonacoLanguageFromPath(filePath: string | null | undefined): string {
|
||||
if (!filePath) return "plaintext";
|
||||
|
||||
const fileName = filePath.split("/").pop() ?? filePath;
|
||||
const extensionIndex = fileName.lastIndexOf(".");
|
||||
if (extensionIndex <= 0 || extensionIndex === fileName.length - 1) {
|
||||
return "plaintext";
|
||||
}
|
||||
|
||||
const extension = fileName.slice(extensionIndex + 1).toLowerCase();
|
||||
return EXTENSION_TO_MONACO_LANGUAGE[extension] ?? "plaintext";
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
"@babel/standalone": "^7.29.2",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@marsidev/react-turnstile": "^1.5.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@platejs/autoformat": "^52.0.11",
|
||||
"@platejs/basic-nodes": "^52.0.11",
|
||||
|
|
@ -106,6 +107,7 @@
|
|||
"lenis": "^1.3.17",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"motion": "^12.23.22",
|
||||
"next": "^16.1.0",
|
||||
"next-intl": "^4.6.1",
|
||||
|
|
|
|||
54
surfsense_web/pnpm-lock.yaml
generated
54
surfsense_web/pnpm-lock.yaml
generated
|
|
@ -29,6 +29,9 @@ importers:
|
|||
'@marsidev/react-turnstile':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@monaco-editor/react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@number-flow/react':
|
||||
specifier: ^0.5.10
|
||||
version: 0.5.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -263,6 +266,9 @@ importers:
|
|||
lucide-react:
|
||||
specifier: ^0.577.0
|
||||
version: 0.577.0(react@19.2.4)
|
||||
monaco-editor:
|
||||
specifier: ^0.55.1
|
||||
version: 0.55.1
|
||||
motion:
|
||||
specifier: ^12.23.22
|
||||
version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -1980,6 +1986,16 @@ packages:
|
|||
peerDependencies:
|
||||
mediabunny: ^1.0.0
|
||||
|
||||
'@monaco-editor/loader@1.7.0':
|
||||
resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==}
|
||||
|
||||
'@monaco-editor/react@4.7.0':
|
||||
resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==}
|
||||
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
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.97':
|
||||
resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
|
@ -5368,6 +5384,9 @@ packages:
|
|||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.2.7:
|
||||
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
|
||||
|
||||
dompurify@3.3.1:
|
||||
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
||||
|
||||
|
|
@ -6745,6 +6764,11 @@ packages:
|
|||
markdown-table@3.0.4:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
marked@14.0.0:
|
||||
resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
marked@15.0.12:
|
||||
resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
|
||||
engines: {node: '>= 18'}
|
||||
|
|
@ -6965,6 +6989,9 @@ packages:
|
|||
module-details-from-path@1.0.4:
|
||||
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
||||
|
||||
motion-dom@12.34.3:
|
||||
resolution: {integrity: sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==}
|
||||
|
||||
|
|
@ -7943,6 +7970,9 @@ packages:
|
|||
stable-hash@0.0.5:
|
||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||
|
||||
state-local@1.0.7:
|
||||
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -10050,6 +10080,17 @@ snapshots:
|
|||
dependencies:
|
||||
mediabunny: 1.39.2
|
||||
|
||||
'@monaco-editor/loader@1.7.0':
|
||||
dependencies:
|
||||
state-local: 1.0.7
|
||||
|
||||
'@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@monaco-editor/loader': 1.7.0
|
||||
monaco-editor: 0.55.1
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.97':
|
||||
optional: true
|
||||
|
||||
|
|
@ -13748,6 +13789,10 @@ snapshots:
|
|||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.2.7:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dompurify@3.3.1:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
|
@ -15327,6 +15372,8 @@ snapshots:
|
|||
|
||||
markdown-table@3.0.4: {}
|
||||
|
||||
marked@14.0.0: {}
|
||||
|
||||
marked@15.0.12: {}
|
||||
|
||||
marked@17.0.3: {}
|
||||
|
|
@ -15822,6 +15869,11 @@ snapshots:
|
|||
|
||||
module-details-from-path@1.0.4: {}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
dependencies:
|
||||
dompurify: 3.2.7
|
||||
marked: 14.0.0
|
||||
|
||||
motion-dom@12.34.3:
|
||||
dependencies:
|
||||
motion-utils: 12.29.2
|
||||
|
|
@ -17073,6 +17125,8 @@ snapshots:
|
|||
|
||||
stable-hash@0.0.5: {}
|
||||
|
||||
state-local@1.0.7: {}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue