From c2a23fcbcb7a4e1d41975a6b7605df39e6724e20 Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Tue, 24 Sep 2024 13:00:49 +0100 Subject: [PATCH 1/4] fix: remove editor red squiggly lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit by dynamically loading project'sĀ tsconfigĀ file and adding nice defaults --- frontend/components/editor/index.tsx | 94 ++++++++++++++++++++++++-- frontend/lib/tsconfig.ts | 99 ++++++++++++++++++++++++++++ frontend/lib/utils.ts | 23 +++++++ 3 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 frontend/lib/tsconfig.ts diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index f5750ee..ba10bc8 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -35,6 +35,8 @@ import { PreviewProvider, usePreview } from "@/context/PreviewContext" import { useSocket } from "@/context/SocketContext" import { Button } from "../ui/button" import React from "react" +import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig" +import { deepMerge } from "@/lib/utils" export default function CodeEditor({ userData, @@ -154,9 +156,78 @@ export default function CodeEditor({ } // Post-mount editor keybindings and actions - const handleEditorMount: OnMount = (editor, monaco) => { + const handleEditorMount: OnMount = async (editor, monaco) => { setEditorRef(editor) monacoRef.current = monaco + /** + * Sync all the models to the worker eagerly. + * This enables intelliSense for all files without needing an `addExtraLib` call. + */ + monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true) + monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true) + + monaco.languages.typescript.typescriptDefaults.setCompilerOptions( + defaultCompilerOptions + ) + monaco.languages.typescript.javascriptDefaults.setCompilerOptions( + defaultCompilerOptions + ) + const fetchFileContent = (fileId: string): Promise => { + return new Promise((resolve) => { + socket?.emit("getFile", fileId, (content: string) => { + resolve(content) + }) + }) + } + const loadTSConfig = async (files: (TFolder | TFile)[]) => { + const tsconfigFiles = files.filter((file) => + file.name.endsWith("tsconfig.json") + ) + let mergedConfig: any = { compilerOptions: {} } + + for (const file of tsconfigFiles) { + const containerId = file.id.split("/").slice(0, 2).join("/") + const content = await fetchFileContent(file.id) + + try { + let tsConfig = JSON.parse(content) + + // Handle references + if (tsConfig.references) { + for (const ref of tsConfig.references) { + const path = ref.path.replace("./", "") + const fileId = `${containerId}/${path}` + const refContent = await fetchFileContent(fileId) + const referenceTsConfig = JSON.parse(refContent) + + // Merge configurations + mergedConfig = deepMerge(mergedConfig, referenceTsConfig) + } + } + + // Merge current file's config + mergedConfig = deepMerge(mergedConfig, tsConfig) + } catch (error) { + console.error("Error parsing TSConfig:", error) + } + } + // Apply merged compiler options + if (mergedConfig.compilerOptions) { + const updatedOptions = parseTSConfigToMonacoOptions({ + ...defaultCompilerOptions, + ...mergedConfig.compilerOptions, + }) + monaco.languages.typescript.typescriptDefaults.setCompilerOptions( + updatedOptions + ) + monaco.languages.typescript.javascriptDefaults.setCompilerOptions( + updatedOptions + ) + } + } + + // Call the function with your file structure + await loadTSConfig(files) editor.onDidChangeCursorPosition((e) => { setIsSelected(false) @@ -784,11 +855,11 @@ export default function CodeEditor({ const afterLineNumber = isAbove ? line - 1 : line id = changeAccessor.addZone({ afterLineNumber, - heightInLines: isAbove?11: 12, + heightInLines: isAbove ? 11 : 12, domNode: generateRef.current, }) - const contentWidget= generate.widget - if (contentWidget){ + const contentWidget = generate.widget + if (contentWidget) { editorRef?.layoutContentWidget(contentWidget) } } else { @@ -984,3 +1055,18 @@ export default function CodeEditor({ ) } + +/** + * Configure the typescript compiler to detect JSX and load type definitions + */ +const defaultCompilerOptions: monaco.languages.typescript.CompilerOptions = { + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true, + resolveJsonModule: true, + + jsx: monaco.languages.typescript.JsxEmit.ReactJSX, + module: monaco.languages.typescript.ModuleKind.ESNext, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + target: monaco.languages.typescript.ScriptTarget.ESNext, +} diff --git a/frontend/lib/tsconfig.ts b/frontend/lib/tsconfig.ts new file mode 100644 index 0000000..12744d5 --- /dev/null +++ b/frontend/lib/tsconfig.ts @@ -0,0 +1,99 @@ +import * as monaco from "monaco-editor" + +export function parseTSConfigToMonacoOptions( + tsconfig: any +): monaco.languages.typescript.CompilerOptions { + const compilerOptions: monaco.languages.typescript.CompilerOptions = {} + + // Map tsconfig options to Monaco CompilerOptions + if (tsconfig.strict) compilerOptions.strict = tsconfig.strict + if (tsconfig.target) compilerOptions.target = mapScriptTarget(tsconfig.target) + if (tsconfig.module) compilerOptions.module = mapModule(tsconfig.module) + if (tsconfig.lib) compilerOptions.lib = tsconfig.lib + if (tsconfig.allowJs) compilerOptions.allowJs = tsconfig.allowJs + if (tsconfig.checkJs) compilerOptions.checkJs = tsconfig.checkJs + if (tsconfig.jsx) compilerOptions.jsx = mapJSX(tsconfig.jsx) + if (tsconfig.declaration) compilerOptions.declaration = tsconfig.declaration + if (tsconfig.declarationMap) + compilerOptions.declarationMap = tsconfig.declarationMap + if (tsconfig.sourceMap) compilerOptions.sourceMap = tsconfig.sourceMap + if (tsconfig.outFile) compilerOptions.outFile = tsconfig.outFile + if (tsconfig.outDir) compilerOptions.outDir = tsconfig.outDir + if (tsconfig.removeComments) + compilerOptions.removeComments = tsconfig.removeComments + if (tsconfig.noEmit) compilerOptions.noEmit = tsconfig.noEmit + if (tsconfig.noEmitOnError) + compilerOptions.noEmitOnError = tsconfig.noEmitOnError + + return compilerOptions +} + +function mapScriptTarget( + target: string +): monaco.languages.typescript.ScriptTarget { + const targetMap: { [key: string]: monaco.languages.typescript.ScriptTarget } = + { + es3: monaco.languages.typescript.ScriptTarget.ES3, + es5: monaco.languages.typescript.ScriptTarget.ES5, + es6: monaco.languages.typescript.ScriptTarget.ES2015, + es2015: monaco.languages.typescript.ScriptTarget.ES2015, + es2016: monaco.languages.typescript.ScriptTarget.ES2016, + es2017: monaco.languages.typescript.ScriptTarget.ES2017, + es2018: monaco.languages.typescript.ScriptTarget.ES2018, + es2019: monaco.languages.typescript.ScriptTarget.ES2019, + es2020: monaco.languages.typescript.ScriptTarget.ES2020, + esnext: monaco.languages.typescript.ScriptTarget.ESNext, + } + if (typeof target !== "string") { + return monaco.languages.typescript.ScriptTarget.Latest + } + return ( + targetMap[target?.toLowerCase()] || + monaco.languages.typescript.ScriptTarget.Latest + ) +} + +function mapModule(module: string): monaco.languages.typescript.ModuleKind { + const moduleMap: { [key: string]: monaco.languages.typescript.ModuleKind } = { + none: monaco.languages.typescript.ModuleKind.None, + commonjs: monaco.languages.typescript.ModuleKind.CommonJS, + amd: monaco.languages.typescript.ModuleKind.AMD, + umd: monaco.languages.typescript.ModuleKind.UMD, + system: monaco.languages.typescript.ModuleKind.System, + es6: monaco.languages.typescript.ModuleKind.ES2015, + es2015: monaco.languages.typescript.ModuleKind.ES2015, + esnext: monaco.languages.typescript.ModuleKind.ESNext, + } + if (typeof module !== "string") { + return monaco.languages.typescript.ModuleKind.ESNext + } + return ( + moduleMap[module.toLowerCase()] || + monaco.languages.typescript.ModuleKind.ESNext + ) +} + +function mapJSX(jsx: string): monaco.languages.typescript.JsxEmit { + const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = { + preserve: monaco.languages.typescript.JsxEmit.Preserve, + react: monaco.languages.typescript.JsxEmit.React, + "react-native": monaco.languages.typescript.JsxEmit.ReactNative, + } + return jsxMap[jsx.toLowerCase()] || monaco.languages.typescript.JsxEmit.React +} + +// Example usage: +const tsconfigJSON = { + compilerOptions: { + strict: true, + target: "ES2020", + module: "ESNext", + lib: ["DOM", "ES2020"], + jsx: "react", + sourceMap: true, + outDir: "./dist", + }, +} + +const monacoOptions = parseTSConfigToMonacoOptions(tsconfigJSON.compilerOptions) +console.log(monacoOptions) diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index 63b3764..8fd506f 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -75,3 +75,26 @@ export function debounce void>( timeout = setTimeout(() => func(...args), wait) } as T } + +// Deep merge utility function +export const deepMerge = (target: any, source: any) => { + const output = { ...target } + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach((key) => { + if (isObject(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }) + } else { + output[key] = deepMerge(target[key], source[key]) + } + } else { + Object.assign(output, { [key]: source[key] }) + } + }) + } + return output +} + +const isObject = (item: any) => { + return item && typeof item === "object" && !Array.isArray(item) +} From af45df28d5a557da65e4aee5401b9aa440c2be0f Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Tue, 24 Sep 2024 13:57:40 +0100 Subject: [PATCH 2/4] feat(ui): improve folder structure UI --- frontend/components/editor/sidebar/folder.tsx | 151 ++++++++++-------- 1 file changed, 88 insertions(+), 63 deletions(-) diff --git a/frontend/components/editor/sidebar/folder.tsx b/frontend/components/editor/sidebar/folder.tsx index 4715baf..94582d7 100644 --- a/frontend/components/editor/sidebar/folder.tsx +++ b/frontend/components/editor/sidebar/folder.tsx @@ -1,18 +1,20 @@ -"use client"; +"use client" -import Image from "next/image"; -import { useEffect, useRef, useState } from "react"; -import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"; -import { TFile, TFolder, TTab } from "@/lib/types"; -import SidebarFile from "./file"; +import Image from "next/image" +import { useEffect, useRef, useState } from "react" +import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js" +import { TFile, TFolder, TTab } from "@/lib/types" +import SidebarFile from "./file" import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, -} from "@/components/ui/context-menu"; -import { Loader2, Pencil, Trash2 } from "lucide-react"; -import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +} from "@/components/ui/context-menu" +import { ChevronRight, Loader2, Pencil, Trash2 } from "lucide-react" +import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter" +import { cn } from "@/lib/utils" +import { motion, AnimatePresence } from "framer-motion" // Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out @@ -25,27 +27,27 @@ export default function SidebarFolder({ movingId, deletingFolderId, }: { - data: TFolder; - selectFile: (file: TTab) => void; + data: TFolder + selectFile: (file: TTab) => void handleRename: ( id: string, newName: string, oldName: string, type: "file" | "folder" - ) => boolean; - handleDeleteFile: (file: TFile) => void; - handleDeleteFolder: (folder: TFolder) => void; - movingId: string; - deletingFolderId: string; + ) => boolean + handleDeleteFile: (file: TFile) => void + handleDeleteFolder: (folder: TFolder) => void + movingId: string + deletingFolderId: string }) { - const ref = useRef(null); // drop target - const [isDraggedOver, setIsDraggedOver] = useState(false); + const ref = useRef(null) // drop target + const [isDraggedOver, setIsDraggedOver] = useState(false) const isDeleting = - deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId); + deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId) useEffect(() => { - const el = ref.current; + const el = ref.current if (el) return dropTargetForElements({ @@ -67,17 +69,17 @@ export default function SidebarFolder({ // no dropping while awaiting move canDrop: () => { - return !movingId; + return !movingId }, - }); - }, []); + }) + }, []) - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false) const folder = isOpen ? getIconForOpenFolder(data.name) - : getIconForFolder(data.name); + : getIconForFolder(data.name) - const inputRef = useRef(null); + const inputRef = useRef(null) // const [editing, setEditing] = useState(false); // useEffect(() => { @@ -96,6 +98,12 @@ export default function SidebarFolder({ isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm" } w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary cursor-pointer`} > + Folder icon { - handleDeleteFolder(data); + handleDeleteFolder(data) }} > Delete - {isOpen ? ( -
-
-
- {data.children.map((child) => - child.type === "file" ? ( - - ) : ( - - ) - )} -
-
- ) : null} + + {isOpen ? ( + +
+
+ {data.children.map((child) => + child.type === "file" ? ( + + ) : ( + + ) + )} +
+
+
+ ) : null} +
- ); + ) } From b7230f1bc4d77df9eb4b15768a3fe42543d9b1ed Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Tue, 24 Sep 2024 14:01:51 +0100 Subject: [PATCH 3/4] fix: new project modal scrolls when it overflows(instead of clipping content) --- frontend/components/dashboard/newProject.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/dashboard/newProject.tsx b/frontend/components/dashboard/newProject.tsx index b91e738..2dc248d 100644 --- a/frontend/components/dashboard/newProject.tsx +++ b/frontend/components/dashboard/newProject.tsx @@ -71,7 +71,7 @@ const data: { icon: "/project-icons/python.svg", description: "A JavaScript runtime built on the V8 JavaScript engine", disabled: false, - } + }, ] const formSchema = z.object({ @@ -124,7 +124,7 @@ export default function NewProjectModal({ if (!loading) setOpen(open) }} > - + Create A Sandbox From 0f619ccb7d6c11fb60183d6f2c21b5afe2d0dcd5 Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Tue, 24 Sep 2024 14:10:56 +0100 Subject: [PATCH 4/4] feat: update project icon for each template type --- frontend/components/dashboard/newProject.tsx | 40 +------------------ .../dashboard/projectCard/index.tsx | 16 +++----- frontend/lib/data/index.ts | 36 +++++++++++++++++ 3 files changed, 43 insertions(+), 49 deletions(-) create mode 100644 frontend/lib/data/index.ts diff --git a/frontend/components/dashboard/newProject.tsx b/frontend/components/dashboard/newProject.tsx index 2dc248d..334d232 100644 --- a/frontend/components/dashboard/newProject.tsx +++ b/frontend/components/dashboard/newProject.tsx @@ -36,43 +36,7 @@ import { createSandbox } from "@/lib/actions" import { useRouter } from "next/navigation" import { Loader2 } from "lucide-react" import { Button } from "../ui/button" - -const data: { - id: string - name: string - icon: string - description: string - disabled: boolean -}[] = [ - { - id: "reactjs", - name: "React", - icon: "/project-icons/react.svg", - description: "A JavaScript library for building user interfaces", - disabled: false, - }, - { - id: "vanillajs", - name: "HTML/JS", - icon: "/project-icons/more.svg", - description: "More coming soon, feel free to contribute on GitHub", - disabled: false, - }, - { - id: "nextjs", - name: "NextJS", - icon: "/project-icons/node.svg", - description: "A JavaScript runtime built on the V8 JavaScript engine", - disabled: false, - }, - { - id: "streamlit", - name: "Streamlit", - icon: "/project-icons/python.svg", - description: "A JavaScript runtime built on the V8 JavaScript engine", - disabled: false, - }, -] +import { projectTemplates } from "@/lib/data" const formSchema = z.object({ name: z @@ -129,7 +93,7 @@ export default function NewProjectModal({ Create A Sandbox
- {data.map((item) => ( + {projectTemplates.map((item) => (