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 { 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
@ -124,12 +88,12 @@ export default function NewProjectModal({
if (!loading) setOpen(open)
}}
>
<DialogContent>
<DialogContent className="max-h-[95vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create A Sandbox</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 w-full gap-2 mt-2">
{data.map((item) => (
{projectTemplates.map((item) => (
<button
disabled={item.disabled || loading}
key={item.id}

View File

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

View File

@ -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<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) => {
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,
}

View File

@ -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<HTMLInputElement>(null);
const inputRef = useRef<HTMLInputElement>(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`}
>
<ChevronRight
className={cn(
"min-w-3 min-h-3 mr-1 ml-auto transition-all duration-300",
isOpen ? "transform rotate-90" : ""
)}
/>
<Image
src={`/icons/${folder}`}
alt="Folder icon"
@ -149,48 +157,65 @@ export default function SidebarFolder({
<ContextMenuItem
disabled={isDeleting}
onClick={() => {
handleDeleteFolder(data);
handleDeleteFolder(data)
}}
>
<Trash2 className="w-4 h-4 mr-2" />
Delete
</ContextMenuItem>
</ContextMenuContent>
{isOpen ? (
<div
className={`flex w-full items-stretch ${
isDraggedOver ? "rounded-b-sm bg-secondary/50" : ""
}`}
>
<div className="w-[1px] bg-border mx-2 h-full"></div>
<div className="flex flex-col grow">
{data.children.map((child) =>
child.type === "file" ? (
<SidebarFile
key={child.id}
data={child}
selectFile={selectFile}
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
movingId={movingId}
deletingFolderId={deletingFolderId}
/>
) : (
<SidebarFolder
key={child.id}
data={child}
selectFile={selectFile}
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
movingId={movingId}
deletingFolderId={deletingFolderId}
/>
)
)}
</div>
</div>
) : null}
<AnimatePresence>
{isOpen ? (
<motion.div
className="overflow-y-hidden"
initial={{
height: 0,
opacity: 0,
}}
animate={{
height: "auto",
opacity: 1,
}}
exit={{
height: 0,
opacity: 0,
}}
>
<div
className={cn(
isDraggedOver ? "rounded-b-sm bg-secondary/50" : ""
)}
>
<div className="flex flex-col grow ml-2 pl-2 border-l border-border">
{data.children.map((child) =>
child.type === "file" ? (
<SidebarFile
key={child.id}
data={child}
selectFile={selectFile}
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
movingId={movingId}
deletingFolderId={deletingFolderId}
/>
) : (
<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>
);
)
}

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