Merge pull request #7 from Code-Victor/feat/editor-fix-n-ui-updates

Feat/editor fix n UI updates
This commit is contained in:
James Murdza 2024-09-26 05:32:19 -07:00 committed by GitHub
commit 55fde2f648
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 344 additions and 117 deletions

View File

@ -36,43 +36,7 @@ import { createSandbox } from "@/lib/actions"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import { projectTemplates } from "@/lib/data"
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,
}
]
const formSchema = z.object({ const formSchema = z.object({
name: z name: z
@ -124,12 +88,12 @@ export default function NewProjectModal({
if (!loading) setOpen(open) if (!loading) setOpen(open)
}} }}
> >
<DialogContent> <DialogContent className="max-h-[95vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Create A Sandbox</DialogTitle> <DialogTitle>Create A Sandbox</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid grid-cols-2 w-full gap-2 mt-2"> <div className="grid grid-cols-2 w-full gap-2 mt-2">
{data.map((item) => ( {projectTemplates.map((item) => (
<button <button
disabled={item.disabled || loading} disabled={item.disabled || loading}
key={item.id} key={item.id}

View File

@ -8,6 +8,7 @@ import { Clock, Globe, Lock } from "lucide-react"
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { projectTemplates } from "@/lib/data"
export default function ProjectCard({ export default function ProjectCard({
children, children,
@ -43,7 +44,9 @@ export default function ProjectCard({
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`) setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
} }
}, [sandbox]) }, [sandbox])
const projectIcon =
projectTemplates.find((p) => p.id === sandbox.type)?.icon ??
"/project-icons/node.svg"
return ( return (
<Card <Card
tabIndex={0} tabIndex={0}
@ -65,16 +68,7 @@ export default function ProjectCard({
</AnimatePresence> </AnimatePresence>
<div className="space-x-2 flex items-center justify-start w-full z-10"> <div className="space-x-2 flex items-center justify-start w-full z-10">
<Image <Image alt="" src={projectIcon} width={20} height={20} />
alt=""
src={
sandbox.type === "react"
? "/project-icons/react.svg"
: "/project-icons/node.svg"
}
width={20}
height={20}
/>
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden"> <div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
{sandbox.name} {sandbox.name}
</div> </div>

View File

@ -35,6 +35,8 @@ import { PreviewProvider, usePreview } from "@/context/PreviewContext"
import { useSocket } from "@/context/SocketContext" import { useSocket } from "@/context/SocketContext"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import React from "react" import React from "react"
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
import { deepMerge } from "@/lib/utils"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -154,9 +156,78 @@ export default function CodeEditor({
} }
// Post-mount editor keybindings and actions // Post-mount editor keybindings and actions
const handleEditorMount: OnMount = (editor, monaco) => { const handleEditorMount: OnMount = async (editor, monaco) => {
setEditorRef(editor) setEditorRef(editor)
monacoRef.current = monaco 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<string> => {
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) => { editor.onDidChangeCursorPosition((e) => {
setIsSelected(false) setIsSelected(false)
@ -784,11 +855,11 @@ export default function CodeEditor({
const afterLineNumber = isAbove ? line - 1 : line const afterLineNumber = isAbove ? line - 1 : line
id = changeAccessor.addZone({ id = changeAccessor.addZone({
afterLineNumber, afterLineNumber,
heightInLines: isAbove?11: 12, heightInLines: isAbove ? 11 : 12,
domNode: generateRef.current, domNode: generateRef.current,
}) })
const contentWidget= generate.widget const contentWidget = generate.widget
if (contentWidget){ if (contentWidget) {
editorRef?.layoutContentWidget(contentWidget) editorRef?.layoutContentWidget(contentWidget)
} }
} else { } 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,
}

View File

@ -1,18 +1,20 @@
"use client"; "use client"
import Image from "next/image"; import Image from "next/image"
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react"
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"; import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
import { TFile, TFolder, TTab } from "@/lib/types"; import { TFile, TFolder, TTab } from "@/lib/types"
import SidebarFile from "./file"; import SidebarFile from "./file"
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu"; } from "@/components/ui/context-menu"
import { Loader2, Pencil, Trash2 } from "lucide-react"; import { ChevronRight, Loader2, Pencil, Trash2 } from "lucide-react"
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 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 // 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, movingId,
deletingFolderId, deletingFolderId,
}: { }: {
data: TFolder; data: TFolder
selectFile: (file: TTab) => void; selectFile: (file: TTab) => void
handleRename: ( handleRename: (
id: string, id: string,
newName: string, newName: string,
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => boolean; ) => boolean
handleDeleteFile: (file: TFile) => void; handleDeleteFile: (file: TFile) => void
handleDeleteFolder: (folder: TFolder) => void; handleDeleteFolder: (folder: TFolder) => void
movingId: string; movingId: string
deletingFolderId: string; deletingFolderId: string
}) { }) {
const ref = useRef(null); // drop target const ref = useRef(null) // drop target
const [isDraggedOver, setIsDraggedOver] = useState(false); const [isDraggedOver, setIsDraggedOver] = useState(false)
const isDeleting = const isDeleting =
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId); deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId)
useEffect(() => { useEffect(() => {
const el = ref.current; const el = ref.current
if (el) if (el)
return dropTargetForElements({ return dropTargetForElements({
@ -67,17 +69,17 @@ export default function SidebarFolder({
// no dropping while awaiting move // no dropping while awaiting move
canDrop: () => { canDrop: () => {
return !movingId; return !movingId
}, },
}); })
}, []); }, [])
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false)
const folder = isOpen const folder = isOpen
? getIconForOpenFolder(data.name) ? getIconForOpenFolder(data.name)
: getIconForFolder(data.name); : getIconForFolder(data.name)
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null)
// const [editing, setEditing] = useState(false); // const [editing, setEditing] = useState(false);
// useEffect(() => { // useEffect(() => {
@ -96,6 +98,12 @@ export default function SidebarFolder({
isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm" 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`} } w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary cursor-pointer`}
> >
<ChevronRight
className={cn(
"min-w-3 min-h-3 mr-1 ml-auto transition-all duration-300",
isOpen ? "transform rotate-90" : ""
)}
/>
<Image <Image
src={`/icons/${folder}`} src={`/icons/${folder}`}
alt="Folder icon" alt="Folder icon"
@ -149,48 +157,65 @@ export default function SidebarFolder({
<ContextMenuItem <ContextMenuItem
disabled={isDeleting} disabled={isDeleting}
onClick={() => { onClick={() => {
handleDeleteFolder(data); handleDeleteFolder(data)
}} }}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Delete Delete
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
{isOpen ? ( <AnimatePresence>
<div {isOpen ? (
className={`flex w-full items-stretch ${ <motion.div
isDraggedOver ? "rounded-b-sm bg-secondary/50" : "" className="overflow-y-hidden"
}`} initial={{
> height: 0,
<div className="w-[1px] bg-border mx-2 h-full"></div> opacity: 0,
<div className="flex flex-col grow"> }}
{data.children.map((child) => animate={{
child.type === "file" ? ( height: "auto",
<SidebarFile opacity: 1,
key={child.id} }}
data={child} exit={{
selectFile={selectFile} height: 0,
handleRename={handleRename} opacity: 0,
handleDeleteFile={handleDeleteFile} }}
movingId={movingId} >
deletingFolderId={deletingFolderId} <div
/> className={cn(
) : ( isDraggedOver ? "rounded-b-sm bg-secondary/50" : ""
<SidebarFolder )}
key={child.id} >
data={child} <div className="flex flex-col grow ml-2 pl-2 border-l border-border">
selectFile={selectFile} {data.children.map((child) =>
handleRename={handleRename} child.type === "file" ? (
handleDeleteFile={handleDeleteFile} <SidebarFile
handleDeleteFolder={handleDeleteFolder} key={child.id}
movingId={movingId} data={child}
deletingFolderId={deletingFolderId} selectFile={selectFile}
/> handleRename={handleRename}
) handleDeleteFile={handleDeleteFile}
)} movingId={movingId}
</div> deletingFolderId={deletingFolderId}
</div> />
) : null} ) : (
<SidebarFolder
key={child.id}
data={child}
selectFile={selectFile}
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
movingId={movingId}
deletingFolderId={deletingFolderId}
/>
)
)}
</div>
</div>
</motion.div>
) : null}
</AnimatePresence>
</ContextMenu> </ContextMenu>
); )
} }

View File

@ -0,0 +1,36 @@
export const projectTemplates: {
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,
},
]

99
frontend/lib/tsconfig.ts Normal file
View File

@ -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)

View File

@ -75,3 +75,26 @@ export function debounce<T extends (...args: any[]) => void>(
timeout = setTimeout(() => func(...args), wait) timeout = setTimeout(() => func(...args), wait)
} as T } 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)
}