diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index d364ed9..0000000 --- a/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[build] -rustflags = "--cfg=web_sys_unstable_apis" diff --git a/.gitignore b/.gitignore index 1e0417a..ff3bf11 100644 --- a/.gitignore +++ b/.gitignore @@ -135,7 +135,7 @@ flycheck_*.el *.webp # Portable Network Graphics -*.png +# *.png # Animated Portable Network Graphics *.apng diff --git a/Makefile.toml b/Makefile.toml index 4af9c6e..8db8519 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -36,13 +36,18 @@ script = ''' npm run build --workspace=packages/app ''' +[tasks.build-frontend] +script = ''' +npm run build --workspace=packages/frontend +''' + [tasks.build-backend] script = ''' cargo build -p backend --release ''' [tasks.build] -dependencies = ["build-server", "build-bindings", "build-app", "build-backend"] +dependencies = ["build-server", "build-bindings", "build-app","build-frontend", "build-backend"] [tasks.clean-server] script = ''' @@ -60,19 +65,32 @@ rm -rf packages/app/dist rm -rf packages/app/assets/wasm ''' +[tasks.clean-frontend] +script = ''' +rm -rf packages/frontend/.next +''' + [tasks.clean] -dependencies = ["clean-server", "clean-app"] +dependencies = ["clean-server", "clean-app", "clean-frontend"] [tasks.format] script = ''' cargo +nightly fmt --all ''' -[tasks.run] +[tasks.run-server] script = ''' -./target/release/backend --frontend_folder packages/app/dist --port 9000 +./target/release/backend --frontend_folder packages/app/dist --port 4444 ''' +[tasks.run-frontend] +script = ''' +npm run start --workspace=packages/frontend +''' + +[tasks.run] +run_task = { name = ["run-server", "run-frontend"], parallel = true } + [tasks.test-backend] script = ''' cargo test @@ -83,8 +101,13 @@ script = ''' npm run test --workspace=packages/app ''' +[tasks.test-frontend] +script = ''' +npm run test --workspace=packages/frontend +''' + [tasks.test] -dependencies = ["test-backend", "test-app"] +dependencies = ["test-backend", "test-app", "test-frontend"] [tasks.docker-build] script = ''' diff --git a/package.json b/package.json index 115ea97..a0515fd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "private": true, "workspaces": [ - "packages/app" + "packages/app", + "packages/frontend" ], "scripts": { "test": "npm run test --workspaces" diff --git a/.eslintrc.yaml b/packages/app/.eslintrc.yaml similarity index 100% rename from .eslintrc.yaml rename to packages/app/.eslintrc.yaml diff --git a/packages/app/src/app.ts b/packages/app/src/app.ts index 4ea74fe..ea22d4f 100644 --- a/packages/app/src/app.ts +++ b/packages/app/src/app.ts @@ -166,7 +166,7 @@ export default class App { (async () => { const result = await compileRequest( // FIXME: This should be configurable - { compileUrl: "http://localhost:9000/compile" }, + { compileUrl: "http://localhost:4444/compile" }, { source: code } ); diff --git a/packages/frontend/.eslintrc.json b/packages/frontend/.eslintrc.json new file mode 100644 index 0000000..d2dd4da --- /dev/null +++ b/packages/frontend/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "rules": { + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-extra-semi": "off", + "@typescript-eslint/no-empty-function": "off", + "prefer-const": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/ban-ts-comment": "off" + }, + + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore new file mode 100644 index 0000000..d32cc78 --- /dev/null +++ b/packages/frontend/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/packages/frontend/README.md b/packages/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/packages/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/packages/frontend/components.json b/packages/frontend/components.json new file mode 100644 index 0000000..d710b49 --- /dev/null +++ b/packages/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/packages/frontend/next.config.ts b/packages/frontend/next.config.ts new file mode 100644 index 0000000..da728e3 --- /dev/null +++ b/packages/frontend/next.config.ts @@ -0,0 +1,19 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ + async rewrites() { + return [ + { + source: "/compile", + destination: "http://localhost:4444/compile", + }, + { + source: "/health", + destination: "http://localhost:4444/health", + }, + ]; + }, +}; + +export default nextConfig; diff --git a/packages/frontend/package.json b/packages/frontend/package.json new file mode 100644 index 0000000..8e78246 --- /dev/null +++ b/packages/frontend/package.json @@ -0,0 +1,55 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@monaco-editor/react": "^4.6.0", + "@radix-ui/react-accordion": "^1.2.2", + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-slot": "^1.1.0", + "@statelyai/inspect": "^0.4.0", + "@xstate/store": "^2.6.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "debounce": "^2.2.0", + "immer": "^10.1.1", + "jotai": "^2.10.3", + "json-rpc-2.0": "^1.3.0", + "lodash": "^4.17.21", + "lucide-react": "^0.468.0", + "monaco-editor": "^0.52.2", + "monaco-editor-core": "^0.52.2", + "monaco-languageclient": "^1.0.1", + "nanoid": "^5.0.9", + "next": "15.0.3", + "next-auth": "^5.0.0-beta.25", + "next-themes": "^0.4.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.4.0", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "vscode-languageserver-protocol": "^3.17.5", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@types/lodash": "^4.17.13", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "15.0.3", + "postcss": "^8", + "sass": "^1.83.0", + "tailwindcss": "^3.4.1", + "typescript": "^5.7.2" + } +} diff --git a/packages/frontend/postcss.config.mjs b/packages/frontend/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/packages/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/packages/frontend/src/app/api/[...nextauth]/route.ts b/packages/frontend/src/app/api/[...nextauth]/route.ts new file mode 100644 index 0000000..2fcb3cd --- /dev/null +++ b/packages/frontend/src/app/api/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/lib/auth" // Referring to the auth.ts we just created +export const { GET, POST } = handlers \ No newline at end of file diff --git a/packages/frontend/src/app/fonts/GeistMonoVF.woff b/packages/frontend/src/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000..f2ae185 Binary files /dev/null and b/packages/frontend/src/app/fonts/GeistMonoVF.woff differ diff --git a/packages/frontend/src/app/fonts/GeistVF.woff b/packages/frontend/src/app/fonts/GeistVF.woff new file mode 100644 index 0000000..1b62daa Binary files /dev/null and b/packages/frontend/src/app/fonts/GeistVF.woff differ diff --git a/packages/frontend/src/app/globals.css b/packages/frontend/src/app/globals.css new file mode 100644 index 0000000..43d989a --- /dev/null +++ b/packages/frontend/src/app/globals.css @@ -0,0 +1,125 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* body { + font-family: Arial, Helvetica, sans-serif; +} */ + +@layer base { + :root, + .vs-light { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + + --blue: #033c73; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #c71c22; + --orange: #fd7e14; + --yellow: #dd5600; + --green: #73a839; + --teal: #20c997; + --cyan: #2fa4e7; + --white: #fff; + --gray: #868e96; + --gray-dark: #343a40; + --primary: #2fa4e7; + --secondary: #e9ecef; + --success: #73a839; + --info: #033c73; + --warning: #dd5600; + --danger: #c71c22; + --light: #f8f9fa; + --text: #343a40; + --dark: #343a40; + --body-bg: #fff; + --text-bg-mark: #fcf8e3; + --custom-select: #fff; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, + "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + } + .vs-dark, + .dark { + --background: 234 20% 21%; + --foreground: 238 17% 69%; + --card: 237 23% 17%; + --card-foreground: 238 17% 69%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 237 18% 30%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 226 15% 29%; + --input: 226 15% 29%; + --ring: 226 15% 29%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +@keyframes spinner-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(1turn); + } +} + +.animate-spinner-linear-spin { + animation: spinner-spin 0.8s linear infinite; +} + +.animate-spinner-ease-spin { + animation: spinner-spin 0.8s ease infinite; +} diff --git a/packages/frontend/src/app/globals.scss b/packages/frontend/src/app/globals.scss new file mode 100644 index 0000000..8dff636 --- /dev/null +++ b/packages/frontend/src/app/globals.scss @@ -0,0 +1,182 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* body { + font-family: Arial, Helvetica, sans-serif; +} */ + +@layer base { + :root, + .vs-light { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + + --blue: #033c73; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #c71c22; + --orange: #fd7e14; + --yellow: #dd5600; + --green: #73a839; + --teal: #20c997; + --cyan: #2fa4e7; + --white: #fff; + --gray: #868e96; + --gray-dark: #343a40; + --primary: #2fa4e7; + --secondary: #e9ecef; + --success: #73a839; + --info: #033c73; + --warning: #dd5600; + --danger: #c71c22; + --light: #f8f9fa; + --text: #343a40; + --dark: #343a40; + --body-bg: #fff; + --text-bg-mark: #fcf8e3; + --custom-select: #fff; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, + "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + } + .vs-dark, + .dark { + --background: 234 20% 21%; + --foreground: 238 17% 69%; + --card: 237 23% 17%; + --card-foreground: 238 17% 69%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 237 18% 30%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 226 15% 29%; + --input: 226 15% 29%; + --ring: 226 15% 29%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +@keyframes spinner-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(1turn); + } +} + +.animate-spinner-linear-spin { + animation: spinner-spin 0.8s linear infinite; +} + +.animate-spinner-ease-spin { + animation: spinner-spin 0.8s ease infinite; +} + + +.explorer-bg-hover { + position: relative; + + + &::after { + content: ""; + background-color: hsl(var(--foreground) / 0.05); + opacity: 0; + position: absolute; + width: 200%; + height: 24px; + transition-duration: 150ms; + z-index: -1; + left: 50%; + transform: translateX(-50%); + + } + + &:hover { + &::after { + opacity: 1; + } + } + + &.active { + &::after { + opacity: 0.3; + background-color: hsl(var(--primary) / 0.3); + } + } +} + + +.explorer-bg-drag { + position: relative; + + &::after { + content: ""; + background-color: hsl(var(--foreground) / 0.05); + opacity: 0; + position: absolute; + width: 200%; + inset: 0; + left: 50%; + transform: translateX(-50%); + transition-duration: 150ms; + z-index: -1; + } + + &.active:not(:has(&:hover)) { + &::after { + opacity: 1; + } + } +} diff --git a/packages/frontend/src/app/layout.tsx b/packages/frontend/src/app/layout.tsx new file mode 100644 index 0000000..f5b4b80 --- /dev/null +++ b/packages/frontend/src/app/layout.tsx @@ -0,0 +1,43 @@ +import type { Metadata } from "next"; +import localFont from "next/font/local"; +import "./globals.scss"; +import EditorProvider from "@/context/EditorProvider"; +import { Provider } from "jotai"; +import ThemeProvider from "@/components/ThemeProvider"; +import { SessionProvider } from "next-auth/react"; + +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); + +export const metadata: Metadata = { + title: "Solang", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + {children} + + + + + + ); +} diff --git a/packages/frontend/src/app/page.tsx b/packages/frontend/src/app/page.tsx new file mode 100644 index 0000000..2145928 --- /dev/null +++ b/packages/frontend/src/app/page.tsx @@ -0,0 +1,28 @@ +import Console from "@/components/Console"; +import Editor from "@/components/Editor"; +import Header from "@/components/Header"; +import SidePanel from "@/components/SidePanel"; +import Sidebar from "@/components/Sidebar"; +import HomeTab from "@/components/HomeTab"; + +export default function Home() { + return ( +
+
+
+ + +
+
+
+ + +
+ +
+
+ {/*
+
+ ); +} diff --git a/packages/frontend/src/app/state.ts b/packages/frontend/src/app/state.ts new file mode 100644 index 0000000..5571c2d --- /dev/null +++ b/packages/frontend/src/app/state.ts @@ -0,0 +1,46 @@ +import { isDarkTheme } from "@/lib/theme"; +import { generateRandomId } from "@/lib/utils"; +import { atom, useSetAtom } from "jotai"; +import { create } from "zustand"; +import { combine } from "zustand/middleware"; + +export const ConsoleState = atom<{ id: string; message: string }[]>([]); + +export function useAddConsole() { + const setState = useSetAtom(ConsoleState); + return (message: string) => { + const id = generateRandomId(); + setState((state) => [...state, { id, message }]); + }; +} + +export const useSettingsStore = create( + combine( + { + monacoTheme: isDarkTheme() ? "vs-dark" : "vs-light", + }, + (set) => ({ + setMonacoTheme(theme: "vs-dark" | "vs-light") { + set((state) => ({ ...state, monacoTheme: theme })); + }, + }), + ), +); + +export enum SidebarView { + FILE_EXPLORER = "FILE-EXPLORER", + SETTINGS = "SETTINGS", +} + +export const useAppStore = create( + combine( + { + sidebar: SidebarView.FILE_EXPLORER, + }, + (set) => ({ + setSidebar(sidebar: SidebarView) { + set((state) => ({ ...state, sidebar })); + }, + }), + ), +); diff --git a/packages/frontend/src/assets/image/solang-logo.png b/packages/frontend/src/assets/image/solang-logo.png new file mode 100644 index 0000000..ffdc7ba Binary files /dev/null and b/packages/frontend/src/assets/image/solang-logo.png differ diff --git a/packages/frontend/src/components/Accordion.tsx b/packages/frontend/src/components/Accordion.tsx new file mode 100644 index 0000000..52a8a05 --- /dev/null +++ b/packages/frontend/src/components/Accordion.tsx @@ -0,0 +1,46 @@ +import { cn } from "@/lib/utils"; +import { ComponentProps, createContext, useContext, useState } from "react"; +import Hide from "./Hide"; +import { ChevronDown, ChevronRight, ChevronUp } from "lucide-react"; + +const AccordionContext = createContext({ + open: false, + // setOpen: ((open: boolean) => {}) as React.Dispatch>, +}); + +export function AccordionTrigger({ className, children, ...props }: ComponentProps<"div">) { + // const { open } = useContext(AccordionContext); + return ( +
+ {children} +
+ ); +} + +export function AccordionContent({ className, children, ...props }: ComponentProps<"div">) { + const { open } = useContext(AccordionContext); + + if (!open) { + return null; + } + + return ( +
+ {children} +
+ ); +} + +export function Accordion({ className, open, children, ...props }: ComponentProps<"div"> & { open: boolean }) { + // const [open, setOpen] = useState(false); + return ( + +
+ {children} +
+
+ ); +} diff --git a/packages/frontend/src/components/Console.tsx b/packages/frontend/src/components/Console.tsx new file mode 100644 index 0000000..891c670 --- /dev/null +++ b/packages/frontend/src/components/Console.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { store } from "@/state"; +import { MessageTypeName } from "@/types/log"; +import { useSelector } from "@xstate/store/react"; +import { useEffect, useRef } from "react"; + +function Console() { + const logs = useSelector(store, (state) => state.context.logs); + const containerRef = useRef(null); + + useEffect(() => { + const element = containerRef.current; + if (element) { + element.scrollTo({ + top: element.scrollHeight, + behavior: "smooth", + }); + } + }, [logs]); + + return ( +
+ {logs.map((item) => ( + +
+            {MessageTypeName[item.type]}: {item.message}
+          
+
+ ))} +
+ ); +} + +export default Console; diff --git a/packages/frontend/src/components/Editor/Editor.tsx b/packages/frontend/src/components/Editor/Editor.tsx new file mode 100644 index 0000000..785e16b --- /dev/null +++ b/packages/frontend/src/components/Editor/Editor.tsx @@ -0,0 +1,34 @@ +"use client"; + +import MonacoEditor from "@monaco-editor/react"; +import Spinner from "../Spinner"; +import { useTheme } from "next-themes"; +import { init, mountService } from "@/lib/editor"; +import { useFileContent } from "@/state/hooks"; +import { store } from "@/state"; +import { useSelector } from "@xstate/store/react"; + +function Editor() { + const { resolvedTheme } = useTheme(); + const theme = { dark: "vs-dark", light: "vs-light" }[resolvedTheme!] || resolvedTheme; + const code = useFileContent(); + const { fontSize } = useSelector(store, (state) => state.context.preferences); + + return ( +
+ } + onChange={(value) => store.send({ type: "changeContent", content: value || "" })} + options={{ fontSize }} + /> +
+ ); +} + +export default Editor; diff --git a/packages/frontend/src/components/Editor/index.tsx b/packages/frontend/src/components/Editor/index.tsx new file mode 100644 index 0000000..f07fcb5 --- /dev/null +++ b/packages/frontend/src/components/Editor/index.tsx @@ -0,0 +1,7 @@ +"use client"; + +import dynamic from "next/dynamic"; + +const Editor = dynamic(() => import("./Editor"), { ssr: false }); + +export default Editor; diff --git a/packages/frontend/src/components/FileExplorer/components/File.tsx b/packages/frontend/src/components/FileExplorer/components/File.tsx new file mode 100644 index 0000000..e3b47c9 --- /dev/null +++ b/packages/frontend/src/components/FileExplorer/components/File.tsx @@ -0,0 +1,61 @@ +import { store } from "@/state"; +import { FileType } from "@/types/explorer"; +import { FileIcon } from "lucide-react"; +import Hide from "@/components/Hide"; +import { useSelector } from "@xstate/store/react"; +import { cn, onEnter } from "@/lib/utils"; +import FileActions from "./FileActions"; +import { useState } from "react"; +import { ParentContext } from "../provider/ParentContext"; +import { useExplorer, useExplorerItem } from "@/state/hooks"; + +function File({ path, basePath }: FileType & { basePath: string }) { + const selected = useSelector(store, (state) => state.context.currentFile); + const { name } = useExplorerItem(path); + const [editing, setEditing] = useState(name === ""); + const [newName, setNewName] = useState(name); + + function setCurrentPath(e: React.MouseEvent) { + store.send({ type: "setCurrentPath", path }); + } + + function handleFileEdit() { + store.send({ type: "renameFile", path, basePath, name: newName }); + setEditing(false); + } + + return ( + +
+
+
+ + {name}}> + setNewName(e.target.value)} + autoFocus + className="w-full rounded flex-1 outline-none ring-1 ring-primary/40 px-1" + value={newName} + onBlur={handleFileEdit} + placeholder="name..." + onKeyUp={onEnter(handleFileEdit)} + /> + +
+ + + + +
+
+
+ ); +} + +export default File; diff --git a/packages/frontend/src/components/FileExplorer/components/FileActions.tsx b/packages/frontend/src/components/FileExplorer/components/FileActions.tsx new file mode 100644 index 0000000..4ce9b81 --- /dev/null +++ b/packages/frontend/src/components/FileExplorer/components/FileActions.tsx @@ -0,0 +1,20 @@ +import { useParentContext } from "../provider/ParentContext"; +import IconButton from "@/components/IconButton"; +import { PencilIcon, TrashIcon } from "lucide-react"; +import { store } from "@/state"; + +function FileActions() { + const { setEditing, path, basePath } = useParentContext(); + return ( +
+ setEditing(true)} className="active:opacity-50 duration-150"> + + + store.send({ type: "deleteFile", path, basePath })}> + + +
+ ); +} + +export default FileActions; diff --git a/packages/frontend/src/components/FileExplorer/components/Folder.tsx b/packages/frontend/src/components/FileExplorer/components/Folder.tsx new file mode 100644 index 0000000..34759ac --- /dev/null +++ b/packages/frontend/src/components/FileExplorer/components/Folder.tsx @@ -0,0 +1,115 @@ +import { Accordion, AccordionContent, AccordionTrigger } from "@/components/Accordion"; +import { store } from "@/state"; +import { useExplorer } from "@/state/hooks"; +import { FolderIcon, FolderOpen } from "lucide-react"; +import Hide from "@/components/Hide"; +import { cn, onEnter } from "@/lib/utils"; +import FolderActions from "./FolderActions"; +import { useState } from "react"; +import { ParentContext } from "../provider/ParentContext"; +import RenderNode from "./RenderNode"; +import { logger } from "@/state/utils"; + +function Folder({ path, basePath }: { path: string; basePath: string }) { + const { items, open, name } = useExplorer(path); + const keys = Object.keys(items); + const [editing, setEditing] = useState(name === ""); + const [newName, setNewName] = useState(name); + const [dropping, setDropping] = useState(false); + + function handleToggle() { + store.send({ type: "toggleFolder", path }); + } + + function confirmFolderEdit() { + store.send({ type: "renameFolder", path, basePath, name: newName }); + setEditing(false); + } + + async function handleFileDrop(e: React.DragEvent) { + e.preventDefault(); + e.stopPropagation(); + + const fileArray = Array.from(e.dataTransfer.files); + const readFiles = fileArray.map(async (file) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (e) => { + resolve({ name: file.name, content: e.target?.result }); + }; + + reader.onerror = (e) => { + reject(`Error reading file ${file.name}`); + }; + + // Read as text (adjust if needed for images or binary files) + reader.readAsText(file); + }); + }); + + const files = (await Promise.all(readFiles).catch(() => { + logger.error("Error reading files"); + return []; + })) as any; + + if (files) { + store.send({ type: "addFiles", basePath: path, files }); + } + + setDropping(false); + } + + return ( + { + e.preventDefault(); + e.stopPropagation(); + setDropping(true); + }} + onDragLeave={() => setDropping(false)} + onDrop={handleFileDrop} + > + + }> + + +
+ {name}}> + setNewName(e.target.value)} + autoFocus + className="w-full rounded flex-1 outline-none ring-1 ring-primary/40 px-1" + value={newName} + onBlur={confirmFolderEdit} + placeholder="name..." + onKeyUp={onEnter(confirmFolderEdit)} + /> + +
+ + + {} }}> + + + +
+ +
+ {keys.map((key, index) => ( + + ))} +
+
+
+ ); +} + +export default Folder; diff --git a/packages/frontend/src/components/FileExplorer/components/FolderActions.tsx b/packages/frontend/src/components/FileExplorer/components/FolderActions.tsx new file mode 100644 index 0000000..f8e93e0 --- /dev/null +++ b/packages/frontend/src/components/FileExplorer/components/FolderActions.tsx @@ -0,0 +1,34 @@ +import IconButton from "@/components/IconButton"; +import { useParentContext } from "../provider/ParentContext"; +import { FileIcon, FolderIcon, PencilIcon, TrashIcon } from "lucide-react"; +import { store } from "@/state"; + +function FolderActions() { + const { path, editing, setEditing } = useParentContext(); + + function handleAddFile() { + store.send({ basePath: path, content: "", name: "", type: "addFile" }); + } + + return ( +
e.stopPropagation()} + > + store.send({ type: "addFolder", basePath: path, name: "" })}> + + + + + + setEditing(true)} className="active:opacity-50 duration-150"> + + + store.send({ type: "deleteFolder", path })}> + + +
+ ); +} + +export default FolderActions; diff --git a/packages/frontend/src/components/FileExplorer/components/RenderNode.tsx b/packages/frontend/src/components/FileExplorer/components/RenderNode.tsx new file mode 100644 index 0000000..9bcf0c9 --- /dev/null +++ b/packages/frontend/src/components/FileExplorer/components/RenderNode.tsx @@ -0,0 +1,16 @@ +import { ExplorerNode, ExpNodeType, FileType } from "@/types/explorer"; +import Folder from "./Folder"; +import File from "./File"; + +function isFile(node: ExplorerNode): node is FileType { + return node.type === ExpNodeType.FILE; +} + +function RenderNode({ node, basePath }: { node: ExplorerNode; basePath: string }) { + if (isFile(node)) { + return ; + } + return ; +} + +export default RenderNode; diff --git a/packages/frontend/src/components/FileExplorer/index.tsx b/packages/frontend/src/components/FileExplorer/index.tsx new file mode 100644 index 0000000..0aade6f --- /dev/null +++ b/packages/frontend/src/components/FileExplorer/index.tsx @@ -0,0 +1,15 @@ +import { FolderType } from "@/types/explorer"; +import RenderNode from "./components/RenderNode"; + +function FileExplorer({ root }: { root: FolderType }) { + return ( +
+

File Explorer

+
+ +
+
+ ); +} + +export default FileExplorer; diff --git a/packages/frontend/src/components/FileExplorer/provider/ParentContext.ts b/packages/frontend/src/components/FileExplorer/provider/ParentContext.ts new file mode 100644 index 0000000..fcd3709 --- /dev/null +++ b/packages/frontend/src/components/FileExplorer/provider/ParentContext.ts @@ -0,0 +1,14 @@ +import { createContext, useContext } from "react"; + +export const ParentContext = createContext({ + path: "", + basePath: "", + name: "", + editing: false, + setEditing: ((editing: boolean) => {}) as React.Dispatch>, + confirmEdit: () => {}, +}); + +export function useParentContext() { + return useContext(ParentContext); +} diff --git a/packages/frontend/src/components/Footer.tsx b/packages/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..b003b75 --- /dev/null +++ b/packages/frontend/src/components/Footer.tsx @@ -0,0 +1,20 @@ +import { AlertTriangle } from "lucide-react"; + +function Footer() { + return ( +
+
+ + Scam Alert +
+
+ + Did you know? To prototype on a uniswap v4 hooks, you can create a Multi Sig Swap Hook workspace. Template + created by the cookbook team. + +
+
+ ); +} + +export default Footer; \ No newline at end of file diff --git a/packages/frontend/src/components/Header.tsx b/packages/frontend/src/components/Header.tsx new file mode 100644 index 0000000..0ef69e2 --- /dev/null +++ b/packages/frontend/src/components/Header.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { cn, downloadBlob } from "@/lib/utils"; +import { FaPlay, FaTimes } from "react-icons/fa"; +import { useAddConsole } from "@/app/state"; +import { useExplorerItem, useFileContent } from "@/state/hooks"; +import { useSelector } from "@xstate/store/react"; +import { store } from "@/state"; +import IconButton from "./IconButton"; +import { useEffect, useRef } from "react"; +import { logger } from "@/state/utils"; +import Hide from "./Hide"; + +function TabItem({ path }: { path: string }) { + const file = useExplorerItem(path); + const active = useSelector(store, (state) => state.context.currentFile === path); + const itemRef = useRef(null); + + useEffect(() => { + if (active && itemRef.current) { + itemRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + }, [active]); + + return ( +
store.send({ type: "setCurrentPath", path })} + className={cn( + "bg-foreground/10 px-3 py-1 w-max h-full flex items-center gap-32 border-r duration-150 active:opacity-50", + active && "border-t border-t-primary bg-background/20", + )} + > +

{file?.name}

+ store.send({ type: "removeTab", path })} + > + + +
+ ); +} + +function TabHome({ path }: { path: string }) { + const active = useSelector(store, (state) => state.context.currentFile === path); + const itemRef = useRef(null); + + useEffect(() => { + if (active && itemRef.current) { + itemRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + }, [active]); + + return ( +
store.send({ type: "setCurrentPath", path })} + className={cn( + "bg-foreground/10 px-3 py-1 w-max h-full flex items-center gap-32 border-r duration-150 active:opacity-50 select-none", + active && "border-t border-t-primary bg-background/20", + )} + > +

Home

+ store.send({ type: "removeTab", path })} + > + + +
+ ); +} + +function Header() { + const code = useFileContent(); + const tabs = useSelector(store, (state) => state.context.tabs); + const containerRef = useRef(null); + + async function handleCompile() { + if (!code) { + return logger.error("Error: No Source Code Found"); + } + + logger.info("Compiling contract..."); + + const opts: RequestInit = { + method: "POST", + mode: "cors", + credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + source: code, + }), + }; + + const { result, success, message } = await fetch("/compile", opts).then(async (res) => { + console.log(res); + const result = await res.json().catch(() => null); + + if (!result) { + return { + success: false, + message: res.statusText, + result: null, + }; + } + + return { + success: res.ok, + message: res.statusText, + result: result, + }; + }); + + if (success) { + if (result.type === "SUCCESS") { + const wasm = result.payload.wasm; + downloadBlob(wasm); + logger.info("Contract compiled successfully!"); + } else { + const message = result.payload.compile_stderr; + logger.error(message); + } + } else { + logger.error(message); + } + } + + return ( +
+
+ +
+
+
+ {[...tabs].map((tab) => ( + }> + + + ))} +
+
+
+ ); +} + +export default Header; diff --git a/packages/frontend/src/components/Hide.tsx b/packages/frontend/src/components/Hide.tsx new file mode 100644 index 0000000..f339582 --- /dev/null +++ b/packages/frontend/src/components/Hide.tsx @@ -0,0 +1,19 @@ +import React, { ReactElement, ReactNode } from "react"; + +interface Props { + open: boolean | undefined | null | string; + children: ReactNode; + fallback?: ReactElement; +} + +function Hide({ open, children, fallback }: Props) { + if (open) { + return children; + } else if (fallback) { + return fallback; + } + + return null; +} + +export default Hide; \ No newline at end of file diff --git a/packages/frontend/src/components/HomeTab.tsx b/packages/frontend/src/components/HomeTab.tsx new file mode 100644 index 0000000..ace0f63 --- /dev/null +++ b/packages/frontend/src/components/HomeTab.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { store } from "@/state"; +import { useSelector } from "@xstate/store/react"; +import React from "react"; +import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa"; + +function Item({ title, href, className }: { title: string; href: string; className?: string }) { + return ( + + {title} + + ); +} + +function HomeTab() { + const current = useSelector(store, (state) => state.context.currentFile); + + if (current !== "home") { + return null; + } + + return ( +
+
+
+

Solang

+
IDE for Solang Development
+
+ +
+ + + +
+
+ +
+ +
+ + +
+
+
+ ); +} + +export default HomeTab; diff --git a/packages/frontend/src/components/IconButton.tsx b/packages/frontend/src/components/IconButton.tsx new file mode 100644 index 0000000..f818160 --- /dev/null +++ b/packages/frontend/src/components/IconButton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils"; + +function IconButton({ children, onClick, className, ...props }: React.HTMLAttributes) { + function handleClick(e: React.MouseEvent) { + e.stopPropagation(); + onClick?.(e); + } + return ( + + ); +} + +export default IconButton; diff --git a/packages/frontend/src/components/Router.tsx b/packages/frontend/src/components/Router.tsx new file mode 100644 index 0000000..4418dc5 --- /dev/null +++ b/packages/frontend/src/components/Router.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { store } from "@/state"; +import { useSelector } from "@xstate/store/react"; +import React from "react"; +import HomeTab from "./HomeTab"; +import Editor from "./Editor"; +import initState from "@/state/inistate"; + +initState(); + +function Router() { + const current = useSelector(store, (state) => state.context.currentFile); + + if (current === "home") { + return ; + } + + return ; +} + +export default Router; diff --git a/packages/frontend/src/components/Settings/components/GoogleBackup.tsx b/packages/frontend/src/components/Settings/components/GoogleBackup.tsx new file mode 100644 index 0000000..4cc1965 --- /dev/null +++ b/packages/frontend/src/components/Settings/components/GoogleBackup.tsx @@ -0,0 +1,58 @@ +import Hide from "@/components/Hide"; +import Spinner from "@/components/Spinner"; +import { Button } from "@/components/ui/button"; +import createGoogleBackup from "@/lib/googlebackup"; +import { store } from "@/state"; +import { logger } from "@/state/utils"; +import { LogOut } from "lucide-react"; +import { signIn, signOut, useSession } from "next-auth/react"; +import { useState } from "react"; + +function GoogleBackup() { + const { status, data: session } = useSession(); + const [loading, setLoading] = useState(false); + + async function handleCreateBackup() { + if (status === "unauthenticated") { + return signIn(); + } + + if (!(session as any).accessToken) { + return; + } + + setLoading(true); + logger.info("Creating Google Drive Backup"); + + const state = store.getSnapshot(); + + await createGoogleBackup(state.context.explorer, (session as any).accessToken, state.context.files).finally(() => { + setLoading(false); + }); + + logger.info("Google Drive Backup Complete"); + } + + return ( +
+

+ Google Backup + + + +

+
+ +
+
+ ); +} + +export default GoogleBackup; diff --git a/packages/frontend/src/components/Settings/components/Preferences.tsx b/packages/frontend/src/components/Settings/components/Preferences.tsx new file mode 100644 index 0000000..c09a1b2 --- /dev/null +++ b/packages/frontend/src/components/Settings/components/Preferences.tsx @@ -0,0 +1,37 @@ +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { store } from "@/state"; +import { useSelector } from "@xstate/store/react"; + +function Preferences() { + const {autoFormat,autoSave,fontSize} = useSelector(store, (state) => state.context.preferences); + + return ( +
+

Preferences

+
+
+

Font Size

+ store.send({ type: "changeFontSize", fontSize: parseInt(e.target.value) })} + /> +
+
+

Auto Save

+ +
+
+

Auto Format

+ +
+
+
+ ); +} + +export default Preferences; diff --git a/packages/frontend/src/components/Settings/components/ThemeSwitcher.tsx b/packages/frontend/src/components/Settings/components/ThemeSwitcher.tsx new file mode 100644 index 0000000..e5b4cb1 --- /dev/null +++ b/packages/frontend/src/components/Settings/components/ThemeSwitcher.tsx @@ -0,0 +1,38 @@ +import { Checkbox } from "@/components/ui/checkbox"; +import { useTheme } from "next-themes"; +import { useId } from "react"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; + +function ThemeItem({ value, title }: { value: string; title: string }) { + const { setTheme } = useTheme(); + const id = useId(); + + return ( +
setTheme(value)}> + + +
+ ); +} + +function ThemeSwitcher() { + const { resolvedTheme } = useTheme(); + return ( +
+

Theme

+ + {[ + { value: "light", title: "Light Mode" }, + { value: "dark", title: "Dark Mode" }, + ].map((theme) => ( + + ))} + +
+ ); +} + +export default ThemeSwitcher; diff --git a/packages/frontend/src/components/Settings/index.tsx b/packages/frontend/src/components/Settings/index.tsx new file mode 100644 index 0000000..69ce7b6 --- /dev/null +++ b/packages/frontend/src/components/Settings/index.tsx @@ -0,0 +1,18 @@ +import GoogleBackup from "./components/GoogleBackup"; +import Preferences from "./components/Preferences"; +import ThemeSwitcher from "./components/ThemeSwitcher"; + +function Settings() { + return ( +
+

Settings

+
+ + + +
+
+ ); +} + +export default Settings; diff --git a/packages/frontend/src/components/SidePanel.tsx b/packages/frontend/src/components/SidePanel.tsx new file mode 100644 index 0000000..b565f0a --- /dev/null +++ b/packages/frontend/src/components/SidePanel.tsx @@ -0,0 +1,33 @@ +"use client"; + +import Image from "next/image"; +import SolangLogo from "@/assets/image/solang-logo.png"; +import { Button } from "./ui/button"; +import { FaCog } from "react-icons/fa"; +import { SidebarView, useAppStore } from "@/app/state"; +import { Files, LucideFiles } from "lucide-react"; + +function SidePanel() { + const setSidebar = useAppStore((state) => state.setSidebar); + return ( +
+
+ Solang Logo +
+
+
+ +
+
+
+ +
+
+ ); +} + +export default SidePanel; diff --git a/packages/frontend/src/components/Sidebar.tsx b/packages/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..ad7d611 --- /dev/null +++ b/packages/frontend/src/components/Sidebar.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { SidebarView, useAppStore } from "@/app/state"; +import FileExplorer from "./FileExplorer"; +import Settings from "./Settings"; +import { ExpNodeType } from "@/types/explorer"; +import { store } from "@/state"; + +function Sidebar() { + const { sidebar } = useAppStore(); + const { explorer } = store.getSnapshot().context; + + if (sidebar === SidebarView.SETTINGS) { + return ; + } + + return ( +
+ +
+ ); +} + +function SidebarLayout() { + return ( +
+ +
+ ); +} + +export default SidebarLayout; diff --git a/packages/frontend/src/components/Spinner.tsx b/packages/frontend/src/components/Spinner.tsx new file mode 100644 index 0000000..c87ca63 --- /dev/null +++ b/packages/frontend/src/components/Spinner.tsx @@ -0,0 +1,37 @@ +import { cn } from "@/lib/utils"; +import { twMerge } from "tailwind-merge"; + +const shared = [ + "absolute", + "w-full", + "h-full", + "rounded-full", + "border-t-transparent", + "border-l-transparent", + "border-r-transparent", + "border-b-current", +].join(" "); + +interface Props { + className?: string; + thickness?: number; +} + +function Spinner({ className, thickness = 2 }: Props) { + return ( +
+ + +
+ ); +} + +export default Spinner; diff --git a/packages/frontend/src/components/ThemeProvider.tsx b/packages/frontend/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..5d16624 --- /dev/null +++ b/packages/frontend/src/components/ThemeProvider.tsx @@ -0,0 +1,13 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" + +function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children} +} + +export default ThemeProvider \ No newline at end of file diff --git a/packages/frontend/src/components/ui/accordion.tsx b/packages/frontend/src/components/ui/accordion.tsx new file mode 100644 index 0000000..24c788c --- /dev/null +++ b/packages/frontend/src/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/packages/frontend/src/components/ui/button.tsx b/packages/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..36496a2 --- /dev/null +++ b/packages/frontend/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/packages/frontend/src/components/ui/checkbox.tsx b/packages/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..b5e91f3 --- /dev/null +++ b/packages/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/packages/frontend/src/components/ui/input.tsx b/packages/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..68551b9 --- /dev/null +++ b/packages/frontend/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/packages/frontend/src/components/ui/label.tsx b/packages/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/packages/frontend/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/packages/frontend/src/components/ui/radio-group.tsx b/packages/frontend/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..e9bde17 --- /dev/null +++ b/packages/frontend/src/components/ui/radio-group.tsx @@ -0,0 +1,44 @@ +"use client" + +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/packages/frontend/src/context/EditorProvider.tsx b/packages/frontend/src/context/EditorProvider.tsx new file mode 100644 index 0000000..a12840a --- /dev/null +++ b/packages/frontend/src/context/EditorProvider.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { createContext, MutableRefObject, ReactNode, useRef } from "react"; +import { editor } from "monaco-editor-core"; + +export const EditorContext = createContext>(null as never); + +function EditorProvider({ children }: { children: ReactNode }) { + const editorRef = useRef(null); + return {children}; +} + +export default EditorProvider; diff --git a/packages/frontend/src/hooks/useEditor.tsx b/packages/frontend/src/hooks/useEditor.tsx new file mode 100644 index 0000000..1789ff8 --- /dev/null +++ b/packages/frontend/src/hooks/useEditor.tsx @@ -0,0 +1,8 @@ +import { EditorContext } from "@/context/EditorProvider"; +import { useContext } from "react"; + +function useEditor() { + return useContext(EditorContext); +} + +export default useEditor; diff --git a/packages/frontend/src/lib/auth.ts b/packages/frontend/src/lib/auth.ts new file mode 100644 index 0000000..876cb21 --- /dev/null +++ b/packages/frontend/src/lib/auth.ts @@ -0,0 +1,33 @@ +import NextAuth from "next-auth"; +import Google from "next-auth/providers/google"; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + callbacks: { + async jwt({ token, account }) { + if (account) { + token.accessToken = account?.access_token; + token.refreshToken = account?.refresh_token; + } + return token; + }, + + async session({ session, token }) { + (session as any).accessToken = (token as any).accessToken; + (session as any).refreshToken = (token as any).refreshToken; + return session; + }, + }, + + providers: [ + Google({ + authorization: { + params: { + prompt: "consent", + access_type: "offline", + response_type: "code", + scope: "openid email profile https://www.googleapis.com/auth/drive", + }, + }, + }), + ], +}); diff --git a/packages/frontend/src/lib/editor/client.ts b/packages/frontend/src/lib/editor/client.ts new file mode 100644 index 0000000..f2612c3 --- /dev/null +++ b/packages/frontend/src/lib/editor/client.ts @@ -0,0 +1,115 @@ +import * as jsrpc from "json-rpc-2.0"; +import * as proto from "vscode-languageserver-protocol"; + +import { Codec, FromServer, IntoServer } from "./codec"; +import { store } from "@/state"; + +// const consoleChannel = document.getElementById("channel-console") as HTMLTextAreaElement; + +export default class Client extends jsrpc.JSONRPCServerAndClient { + afterInitializedHooks: (() => Promise)[] = []; + #fromServer: FromServer; + diagnostic: proto.PublishDiagnosticsParams = { + uri: "", + diagnostics: [], + }; + + constructor(fromServer: FromServer, intoServer: IntoServer) { + super( + new jsrpc.JSONRPCServer(), + new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => { + const encoded = Codec.encode(json); + intoServer.enqueue(encoded); + console.log({ json }); + if (null != json.id) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const response = await fromServer.responses.get(json.id)!; + console.log({ response }); + this.client.receive(response as jsrpc.JSONRPCResponse); + } + }), + ); + this.#fromServer = fromServer; + } + + async start(): Promise { + // process "window/logMessage": client <- server + this.addMethod(proto.LogMessageNotification.type.method, (params) => { + const { type, message } = params as { type: proto.MessageType; message: string }; + store.send({ type: "addLog", message: message, logType: type }); + }); + + // request "initialize": client <-> server + await (this.request(proto.InitializeRequest.type.method, { + processId: null, + clientInfo: { + name: "demo-language-client", + }, + capabilities: {}, + rootUri: null, + } as proto.InitializeParams) as Promise); + + // notify "initialized": client --> server + this.notify(proto.InitializedNotification.type.method, {}); + + await Promise.all(this.afterInitializedHooks.map((f: () => Promise) => f())); + await Promise.all([this.processNotifications(), this.processRequests()]); + } + + async processNotifications(): Promise { + console.log("processNotifications called"); + for await (const notification of this.#fromServer.notifications) { + console.log("notification: ", notification); + if (notification.method == "textDocument/publishDiagnostics") { + // delete the old diagnostics + + this.diagnostic.diagnostics = []; + + this.diagnostic = notification.params as proto.PublishDiagnosticsParams; + + console.log("diagnostics: ", this.diagnostic); + } + + await this.receiveAndSend(notification); + } + } + + async processRequests(): Promise { + for await (const request of this.#fromServer.requests) { + console.log(request); + await this.receiveAndSend(request); + } + } + + printToConsole(type: proto.MessageType, message: string): void { + console.log({ type, message }); + // if (consoleChannel) { + // switch (type) { + // case proto.MessageType.Error: { + // consoleChannel.value += " ERROR: "; + // break; + // } + // case proto.MessageType.Warning: { + // consoleChannel.value += " WARNING: "; + // break; + // } + // case proto.MessageType.Info: { + // consoleChannel.value += " INFO: "; + // break; + // } + // case proto.MessageType.Log: { + // consoleChannel.value += " LOG: "; + // break; + // } + // } + // consoleChannel.value += message; + // consoleChannel.value += "\n"; + // } else { + // console.error("consoleChannel is not defined"); + // } + } + + pushAfterInitializeHook(...hooks: (() => Promise)[]): void { + this.afterInitializedHooks.push(...hooks); + } +} diff --git a/packages/frontend/src/lib/editor/codec/bytes.ts b/packages/frontend/src/lib/editor/codec/bytes.ts new file mode 100644 index 0000000..fb4df02 --- /dev/null +++ b/packages/frontend/src/lib/editor/codec/bytes.ts @@ -0,0 +1,28 @@ +import { encoder, decoder } from "."; + +export default class Bytes { + static encode(input: string): Uint8Array { + return encoder.encode(input); + } + + static decode(input: Uint8Array): string { + return decoder.decode(input); + } + + static append( + constructor: { new (length: number): T }, + ...arrays: T[] + ) { + let totalLength = 0; + for (const arr of arrays) { + totalLength += arr.length; + } + const result = new constructor(totalLength); + let offset = 0; + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + return result; + } +} diff --git a/packages/frontend/src/lib/editor/codec/demuxer.ts b/packages/frontend/src/lib/editor/codec/demuxer.ts new file mode 100644 index 0000000..a8f9267 --- /dev/null +++ b/packages/frontend/src/lib/editor/codec/demuxer.ts @@ -0,0 +1,79 @@ +// @ts-nocheck + +import * as vsrpc from "vscode-jsonrpc"; + +import Bytes from "./bytes"; +import PromiseMap from "./map"; +import Queue from "./queue"; +import Tracer from "../tracer"; + +export default class StreamDemuxer extends Queue { + readonly responses: PromiseMap = new PromiseMap(); + readonly notifications: Queue = new Queue(); + readonly requests: Queue = new Queue(); + + readonly #start: Promise; + + constructor() { + super(); + this.#start = this.start(); + } + + private async start(): Promise { + console.log("RESPONSES", this.responses); + let contentLength: null | number = null; + let buffer = new Uint8Array(); + + for await (const bytes of this) { + console.log("bytes", bytes); + + buffer = Bytes.append(Uint8Array, buffer, bytes); + + // check if the content length is known + if (null == contentLength) { + // if not, try to match the prefixed headers + const match = Bytes.decode(buffer).match(/^Content-Length:\s*(\d+)\s*/); + if (null == match) continue; + + // try to parse the content-length from the headers + const length = parseInt(match[1]); + if (isNaN(length)) throw new Error("invalid content length"); + + // slice the headers since we now have the content length + buffer = buffer.slice(match[0].length); + + // set the content length + contentLength = length; + } + + // if the buffer doesn't contain a full message; await another iteration + if (buffer.length < contentLength) continue; + + // decode buffer to a string + const delimited = Bytes.decode(buffer); + + // reset the buffer + buffer = buffer.slice(contentLength); + // reset the contentLength + contentLength = null; + + const message = JSON.parse(delimited) as vsrpc.Message; + console.log("message", message); + Tracer.server(message); + + // demux the message stream + if (vsrpc.Message.isResponse(message) && null != message.id) { + this.responses.set(message.id, message); + continue; + } + if (vsrpc.Message.isNotification(message)) { + this.notifications.enqueue(message); + continue; + } + if (vsrpc.Message.isRequest(message)) { + this.requests.enqueue(message); + continue; + } + } + } +} diff --git a/packages/frontend/src/lib/editor/codec/headers.ts b/packages/frontend/src/lib/editor/codec/headers.ts new file mode 100644 index 0000000..eb30fbc --- /dev/null +++ b/packages/frontend/src/lib/editor/codec/headers.ts @@ -0,0 +1,9 @@ +export default class Headers { + static add(message: string): string { + return `Content-Length: ${message.length}\r\n\r\n${message}`; + } + + static remove(delimited: string): string { + return delimited.replace(/^Content-Length:\s*\d+\s*/, ""); + } +} diff --git a/packages/frontend/src/lib/editor/codec/index.ts b/packages/frontend/src/lib/editor/codec/index.ts new file mode 100644 index 0000000..038acff --- /dev/null +++ b/packages/frontend/src/lib/editor/codec/index.ts @@ -0,0 +1,46 @@ +import * as jsrpc from "json-rpc-2.0"; +import * as vsrpc from "vscode-jsonrpc"; + +import Bytes from "./bytes"; +import StreamDemuxer from "./demuxer"; +import Headers from "./headers"; +import Queue from "./queue"; +import Tracer from "../tracer"; + +export const encoder = new TextEncoder(); +export const decoder = new TextDecoder(); + +export class Codec { + static encode(json: jsrpc.JSONRPCRequest | jsrpc.JSONRPCResponse): Uint8Array { + const message = JSON.stringify(json); + const delimited = Headers.add(message); + return Bytes.encode(delimited); + } + + static decode(data: Uint8Array): T { + const delimited = Bytes.decode(data); + const message = Headers.remove(delimited); + return JSON.parse(message) as T; + } +} + +// FIXME: tracing effiency +export class IntoServer extends Queue implements AsyncGenerator { + enqueue(item: Uint8Array): void { + Tracer.client(Headers.remove(decoder.decode(item))); + super.enqueue(item); + } +} + +export interface FromServer extends WritableStream { + readonly responses: { get(key: number | string): null | Promise }; + readonly notifications: AsyncGenerator; + readonly requests: AsyncGenerator; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace FromServer { + export function create(): FromServer { + return new StreamDemuxer(); + } +} diff --git a/packages/frontend/src/lib/editor/codec/map.ts b/packages/frontend/src/lib/editor/codec/map.ts new file mode 100644 index 0000000..2e36ea8 --- /dev/null +++ b/packages/frontend/src/lib/editor/codec/map.ts @@ -0,0 +1,70 @@ +export default class PromiseMap { + #map: Map> = new Map(); + + get(key: K & { toString(): string }): null | Promise { + let initialized: PromiseMap.Entry; + // if the entry doesn't exist, set it + if (!this.#map.has(key)) { + initialized = this.#set(key); + } else { + // otherwise return the entry + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + initialized = this.#map.get(key)!; + } + // if the entry is a pending promise, return it + if (initialized.status === "pending") { + return initialized.promise; + } else { + // otherwise return null + return null; + } + } + + #set(key: K, value?: V): PromiseMap.Entry { + if (this.#map.has(key)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.#map.get(key)!; + } + // placeholder resolver for entry + let resolve = (item: V) => { + void item; + }; + // promise for entry (which assigns the resolver + const promise = new Promise((resolver) => { + resolve = resolver; + }); + // the initialized entry + const initialized: PromiseMap.Entry = { status: "pending", resolve, promise }; + if (null != value) { + initialized.resolve(value); + } + // set the entry + this.#map.set(key, initialized); + return initialized; + } + + set(key: K & { toString(): string }, value: V): this { + const initialized = this.#set(key, value); + + console.log({ initialized }); + // if the promise is pending ... + if (initialized.status === "pending") { + // ... set the entry status to resolved to free the promise + this.#map.set(key, { status: "resolved" }); + // ... and resolve the promise with the given value + initialized.resolve(value); + } + return this; + } + + get size(): number { + return this.#map.size; + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PromiseMap { + export type Entry = + | { status: "pending"; resolve: (item: V) => void; promise: Promise } + | { status: "resolved" }; +} diff --git a/packages/frontend/src/lib/editor/codec/queue.ts b/packages/frontend/src/lib/editor/codec/queue.ts new file mode 100644 index 0000000..c052774 --- /dev/null +++ b/packages/frontend/src/lib/editor/codec/queue.ts @@ -0,0 +1,121 @@ +export default class Queue implements WritableStream, AsyncGenerator { + readonly #promises: Promise[] = []; + readonly #resolvers: ((item: T) => void)[] = []; + readonly #observers: ((item: T) => void)[] = []; + + #closed = false; + #locked = false; + readonly #stream: WritableStream; + + static #__add(promises: Promise[], resolvers: ((item: X) => void)[]): void { + const prom = new Promise((resolve) => { + (window as any).handle = resolve; + resolvers.push(resolve); + }); + (window as any).banana = prom; + promises.push(prom as any); + } + + static #__enqueue(closed: boolean, promises: Promise[], resolvers: ((item: X) => void)[], item: X): void { + if (!closed) { + if (!resolvers.length) Queue.#__add(promises, resolvers); + const resolve = resolvers.shift()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + (window as any).bull = item; + resolve(item); + } + } + + constructor() { + const closed = this.#closed; + const promises = this.#promises; + const resolvers = this.#resolvers; + this.#stream = new WritableStream({ + write(item: T): void { + Queue.#__enqueue(closed, promises, resolvers, item); + }, + }); + } + + #add(): void { + return Queue.#__add(this.#promises, this.#resolvers); + } + + enqueue(item: T): void { + const manog = Queue.#__enqueue(this.#closed, this.#promises, this.#resolvers, item); + + console.log({ item, resol: this.#resolvers, observers: this.#observers, promises: this.#promises }); + return manog; + } + + dequeue(): Promise { + if (!this.#promises.length) this.#add(); + const item = this.#promises.shift()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + console.log(item); + (window as any).mango = item; + return item; + } + + isEmpty(): boolean { + return !this.#promises.length; + } + + isBlocked(): boolean { + return !!this.#resolvers.length; + } + + get length(): number { + return this.#promises.length - this.#resolvers.length; + } + + async next(): Promise> { + const done = false; + console.log("This Next is called "); + const value = await this.dequeue(); + console.log({ value }); + for (const observer of this.#observers) { + observer(value); + } + return { done, value }; + } + + return(): Promise> { + this.close(); // Ensure the stream is closed on return + return Promise.resolve({ done: true, value: undefined! }); + } + + throw(err: Error): Promise> { + return new Promise((_resolve, reject) => { + reject(err); + }); + } + + [Symbol.asyncIterator](): AsyncGenerator { + return this; + } + + [Symbol.asyncDispose](): Promise { + // Cleanup resources + this.#promises.length = 0; // Clear pending promises + this.#resolvers.length = 0; // Clear resolvers + this.#observers.length = 0; // Clear observers + this.#closed = true; // Mark as closed + return Promise.resolve(); + } + + get locked(): boolean { + return this.#stream.locked; + } + + abort(reason?: Error): Promise { + return this.#stream.abort(reason); + } + + close(): Promise { + this.#closed = true; // Mark the queue as closed + return this.#stream.close(); + } + + getWriter(): WritableStreamDefaultWriter { + return this.#stream.getWriter(); + } +} diff --git a/packages/frontend/src/lib/editor/converter/index.ts b/packages/frontend/src/lib/editor/converter/index.ts new file mode 100644 index 0000000..df1a353 --- /dev/null +++ b/packages/frontend/src/lib/editor/converter/index.ts @@ -0,0 +1,1543 @@ +// @ts-nocheck +import type * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import * as ls from "vscode-languageserver-protocol"; +import * as Is from "vscode-languageserver-protocol/lib/common/utils/is"; +import { + CodeActionParams, + CodeLensParams, + DocumentFormattingParams, + DocumentOnTypeFormattingParams, + DocumentRangeFormattingParams, + ReferenceParams, + RenameParams, + TextDocumentPositionParams, + Position, + TextDocumentIdentifier, + CompletionItem, + CompletionList, + CompletionParams, + CompletionContext, + CompletionTriggerKind, + InsertTextFormat, + Range, + Diagnostic, + CompletionItemKind, + Hover, + SignatureHelp, + SignatureInformation, + ParameterInformation, + Definition, + DefinitionLink, + Location, + LocationLink, + DocumentHighlight, + DocumentHighlightKind, + SymbolInformation, + DocumentSymbolParams, + CodeActionContext, + DiagnosticSeverity, + Command, + CodeLens, + FormattingOptions, + TextEdit, + WorkspaceEdit, + DocumentLinkParams, + DocumentLink, + MarkedString, + MarkupContent, + ColorInformation, + ColorPresentation, + FoldingRange, + FoldingRangeKind, + DiagnosticRelatedInformation, + MarkupKind, + SymbolKind, + DocumentSymbol, + CodeAction, + SignatureHelpContext, + SignatureHelpTriggerKind, + SemanticTokens, + InsertTextMode, + AnnotatedTextEdit, + ChangeAnnotation, + InlayHint, + InlayHintLabelPart, +} from "./services"; + +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + +export interface ProtocolDocumentLink extends monaco.languages.ILink { + data?: unknown; +} +export namespace ProtocolDocumentLink { + export function is(item: any): item is ProtocolDocumentLink { + return !!item && "data" in item; + } +} + +export interface ProtocolCodeLens extends monaco.languages.CodeLens { + data?: unknown; +} +export namespace ProtocolCodeLens { + export function is(item: any): item is ProtocolCodeLens { + return !!item && "data" in item; + } +} + +export interface ProtocolCompletionItem extends monaco.languages.CompletionItem { + data?: unknown; + fromEdit?: boolean; + documentationFormat?: string; + originalItemKind?: CompletionItemKind; + deprecated?: boolean; + insertTextMode?: InsertTextMode; +} +export namespace ProtocolCompletionItem { + export function is(item: any): item is ProtocolCompletionItem { + return !!item && "data" in item; + } +} + +export interface ProtocolCodeAction extends monaco.languages.CodeAction { + data?: unknown; +} +export namespace ProtocolCodeAction { + export function is(item: any): item is ProtocolCodeAction { + return !!item && "data" in item; + } +} + +export interface ProtocolInlayHint extends monaco.languages.InlayHint { + data?: unknown; +} +export namespace ProtocolInlayHint { + export function is(item: any): item is ProtocolInlayHint { + return !!item && "data" in item; + } +} + +type RangeReplace = { insert: monaco.IRange; replace: monaco.IRange }; + +function isRangeReplace(v: Partial | RangeReplace): v is RangeReplace { + return (v as RangeReplace).insert !== undefined; +} + +export class MonacoToProtocolConverter { + public constructor(protected readonly _monaco: typeof monaco) {} + + asPosition(lineNumber: undefined | null, column: undefined | null): {}; + asPosition(lineNumber: number, column: undefined | null): Pick; + asPosition(lineNumber: undefined | null, column: number): Pick; + asPosition(lineNumber: number, column: number): Position; + asPosition(lineNumber: number | undefined | null, column: number | undefined | null): Partial; + asPosition(lineNumber: number | undefined | null, column: number | undefined | null): Partial { + const line = lineNumber === undefined || lineNumber === null ? undefined : lineNumber - 1; + const character = column === undefined || column === null ? undefined : column - 1; + return { + line, + character, + }; + } + + asRange(range: null): null; + asRange(range: undefined): undefined; + asRange(range: monaco.IRange): Range; + asRange(range: monaco.IRange | undefined): Range | undefined; + asRange(range: monaco.IRange | null): Range | null; + asRange(range: monaco.IRange | { insert: monaco.IRange; replace: monaco.IRange }): Range; + asRange(range: Partial): RecursivePartial; + asRange(range: Partial | undefined): RecursivePartial | undefined; + asRange(range: Partial | null): RecursivePartial | null; + asRange(range: Partial | undefined | null | RangeReplace): RecursivePartial | undefined | null { + if (range === undefined) { + return undefined; + } + if (range === null) { + return null; + } + + if (isRangeReplace(range)) { + return this.asRange(range.insert); + } else { + const start = this.asPosition(range.startLineNumber, range.startColumn); + const end = this.asPosition(range.endLineNumber, range.endColumn); + return { + start, + end, + }; + } + } + + asLocation(item: monaco.languages.Location): Location; + asLocation(item: undefined | null): undefined; + asLocation(item: monaco.languages.Location | undefined | null): Location | undefined; + asLocation(item: monaco.languages.Location | undefined | null): Location | undefined { + if (!item) { + return undefined; + } + const uri = item.uri.toString(); + const range = this.asRange(item.range); + return { + uri, + range, + }; + } + + asTextDocumentIdentifier(model: monaco.editor.IReadOnlyModel): TextDocumentIdentifier { + return { + uri: model.uri.toString(), + }; + } + + asTextDocumentPositionParams( + model: monaco.editor.IReadOnlyModel, + position: monaco.Position, + ): TextDocumentPositionParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + position: this.asPosition(position.lineNumber, position.column), + }; + } + + asCompletionParams( + model: monaco.editor.IReadOnlyModel, + position: monaco.Position, + context: monaco.languages.CompletionContext, + ): CompletionParams { + return Object.assign(this.asTextDocumentPositionParams(model, position), { + context: this.asCompletionContext(context), + }); + } + + asCompletionContext(context: monaco.languages.CompletionContext): CompletionContext { + return { + triggerKind: this.asCompletionTriggerKind(context.triggerKind), + triggerCharacter: context.triggerCharacter, + }; + } + + asSignatureHelpContext(context: monaco.languages.SignatureHelpContext): SignatureHelpContext { + return { + triggerKind: this.asSignatureHelpTriggerKind(context.triggerKind), + triggerCharacter: context.triggerCharacter, + isRetrigger: context.isRetrigger, + activeSignatureHelp: this.asSignatureHelp(context.activeSignatureHelp), + }; + } + + asSignatureHelp(signatureHelp: monaco.languages.SignatureHelp | undefined): SignatureHelp | undefined { + if (signatureHelp === undefined) { + return undefined; + } + return { + signatures: signatureHelp.signatures.map((signatureInfo) => this.asSignatureInformation(signatureInfo)), + activeParameter: signatureHelp.activeParameter, + activeSignature: signatureHelp.activeSignature, + }; + } + + asSignatureInformation(signatureInformation: monaco.languages.SignatureInformation): SignatureInformation { + return { + documentation: this.asMarkupContent(signatureInformation.documentation), + label: signatureInformation.label, + parameters: signatureInformation.parameters.map((paramInfo) => this.asParameterInformation(paramInfo)), + activeParameter: signatureInformation.activeParameter, + }; + } + + asParameterInformation(parameterInformation: monaco.languages.ParameterInformation): ParameterInformation { + return { + documentation: this.asMarkupContent(parameterInformation.documentation), + label: parameterInformation.label, + }; + } + + asMarkupContent(markupContent: string | monaco.IMarkdownString | undefined): string | MarkupContent | undefined { + if (markupContent === undefined) { + return undefined; + } + if (typeof markupContent === "string") { + return markupContent; + } + return { + kind: MarkupKind.Markdown, + value: markupContent.value, + }; + } + + asSignatureHelpTriggerKind(triggerKind: monaco.languages.SignatureHelpTriggerKind): SignatureHelpTriggerKind { + switch (triggerKind) { + case this._monaco.languages.SignatureHelpTriggerKind.ContentChange: + return SignatureHelpTriggerKind.ContentChange; + case this._monaco.languages.SignatureHelpTriggerKind.TriggerCharacter: + return SignatureHelpTriggerKind.TriggerCharacter; + default: + return SignatureHelpTriggerKind.Invoke; + } + } + + asCompletionTriggerKind(triggerKind: monaco.languages.CompletionTriggerKind): CompletionTriggerKind { + switch (triggerKind) { + case this._monaco.languages.CompletionTriggerKind.TriggerCharacter: + return CompletionTriggerKind.TriggerCharacter; + case this._monaco.languages.CompletionTriggerKind.TriggerForIncompleteCompletions: + return CompletionTriggerKind.TriggerForIncompleteCompletions; + default: + return CompletionTriggerKind.Invoked; + } + } + + asCompletionItem(item: monaco.languages.CompletionItem): CompletionItem { + const result: CompletionItem = { label: item.label as string }; + const protocolItem = ProtocolCompletionItem.is(item) ? item : undefined; + if (item.detail) { + result.detail = item.detail; + } + if (item.documentation) { + if (typeof item.documentation === "string") { + result.documentation = item.documentation; + } else { + result.documentation = this.asDocumentation( + protocolItem?.documentationFormat ?? MarkupKind.Markdown, + item.documentation, + ); + } + } + if (item.filterText) { + result.filterText = item.filterText; + } + this.fillPrimaryInsertText(result, item as ProtocolCompletionItem); + if (Is.number(item.kind)) { + result.kind = this.asCompletionItemKind(item.kind, protocolItem && protocolItem.originalItemKind); + } + if (item.sortText) { + result.sortText = item.sortText; + } + if (item.additionalTextEdits) { + result.additionalTextEdits = this.asTextEdits(item.additionalTextEdits); + } + if (item.command) { + result.command = this.asCommand(item.command); + } + if (item.commitCharacters) { + result.commitCharacters = item.commitCharacters.slice(); + } + if (item.command) { + result.command = this.asCommand(item.command); + } + if (item.preselect === true || item.preselect === false) { + result.preselect = item.preselect; + } + if (protocolItem) { + if (protocolItem.data !== undefined) { + result.data = protocolItem.data; + } + if (protocolItem.deprecated === true || protocolItem.deprecated === false) { + result.deprecated = protocolItem.deprecated; + } + } + if (item.tags) { + result.tags = item.tags?.slice(); + } + return result; + } + + protected asCompletionItemKind( + value: monaco.languages.CompletionItemKind, + original: CompletionItemKind | undefined, + ): CompletionItemKind { + if (original !== undefined) { + return original; + } + switch (value) { + case this._monaco.languages.CompletionItemKind.Method: + return CompletionItemKind.Method; + case this._monaco.languages.CompletionItemKind.Function: + return CompletionItemKind.Function; + case this._monaco.languages.CompletionItemKind.Constructor: + return CompletionItemKind.Constructor; + case this._monaco.languages.CompletionItemKind.Field: + return CompletionItemKind.Field; + case this._monaco.languages.CompletionItemKind.Variable: + return CompletionItemKind.Variable; + case this._monaco.languages.CompletionItemKind.Class: + return CompletionItemKind.Class; + case this._monaco.languages.CompletionItemKind.Struct: + return CompletionItemKind.Struct; + case this._monaco.languages.CompletionItemKind.Interface: + return CompletionItemKind.Interface; + case this._monaco.languages.CompletionItemKind.Module: + return CompletionItemKind.Module; + case this._monaco.languages.CompletionItemKind.Property: + return CompletionItemKind.Property; + case this._monaco.languages.CompletionItemKind.Event: + return CompletionItemKind.Event; + case this._monaco.languages.CompletionItemKind.Operator: + return CompletionItemKind.Operator; + case this._monaco.languages.CompletionItemKind.Unit: + return CompletionItemKind.Unit; + case this._monaco.languages.CompletionItemKind.Value: + return CompletionItemKind.Value; + case this._monaco.languages.CompletionItemKind.Constant: + return CompletionItemKind.Constant; + case this._monaco.languages.CompletionItemKind.Enum: + return CompletionItemKind.Enum; + case this._monaco.languages.CompletionItemKind.EnumMember: + return CompletionItemKind.EnumMember; + case this._monaco.languages.CompletionItemKind.Keyword: + return CompletionItemKind.Keyword; + case this._monaco.languages.CompletionItemKind.Text: + return CompletionItemKind.Text; + case this._monaco.languages.CompletionItemKind.Color: + return CompletionItemKind.Color; + case this._monaco.languages.CompletionItemKind.File: + return CompletionItemKind.File; + case this._monaco.languages.CompletionItemKind.Reference: + return CompletionItemKind.Reference; + case this._monaco.languages.CompletionItemKind.Customcolor: + return CompletionItemKind.Color; + case this._monaco.languages.CompletionItemKind.Folder: + return CompletionItemKind.Folder; + case this._monaco.languages.CompletionItemKind.TypeParameter: + return CompletionItemKind.TypeParameter; + case this._monaco.languages.CompletionItemKind.Snippet: + return CompletionItemKind.Snippet; + default: + return (value + 1) as CompletionItemKind; + } + } + + protected asDocumentation(format: string, documentation: string | monaco.IMarkdownString): string | MarkupContent { + switch (format) { + case MarkupKind.PlainText: + return { kind: format, value: documentation as string }; + case MarkupKind.Markdown: + return { kind: format, value: (documentation as monaco.IMarkdownString).value }; + default: + return `Unsupported Markup content received. Kind is: ${format}`; + } + } + + protected fillPrimaryInsertText(target: CompletionItem, source: ProtocolCompletionItem): void { + let format: InsertTextFormat = InsertTextFormat.PlainText; + let text: string | undefined; + let range: Range | undefined; + if ( + source.insertTextRules !== undefined && + (source.insertTextRules & this._monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet) === 0 + ) { + format = InsertTextFormat.Snippet; + text = source.insertText; + } + target.insertTextFormat = format; + + text = source.insertText; + if (source.range) { + range = this.asRange(source.range); + } + + target.insertTextFormat = format; + if (source.fromEdit && text && range) { + target.textEdit = { newText: text, range: range }; + } else { + target.insertText = text; + } + target.insertTextMode = source.insertTextMode; + } + + asTextEdit(edit: monaco.editor.ISingleEditOperation): TextEdit { + const range = this.asRange(edit.range)!; + return { + range, + newText: edit.text || "", + }; + } + + asTextEdits(items: monaco.editor.ISingleEditOperation[]): TextEdit[]; + asTextEdits(items: undefined | null): undefined; + asTextEdits(items: monaco.editor.ISingleEditOperation[] | undefined | null): TextEdit[] | undefined; + asTextEdits(items: monaco.editor.ISingleEditOperation[] | undefined | null): TextEdit[] | undefined { + if (!items) { + return undefined; + } + return items.map((item) => this.asTextEdit(item)); + } + + asReferenceParams( + model: monaco.editor.IReadOnlyModel, + position: monaco.Position, + options: { includeDeclaration: boolean }, + ): ReferenceParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + position: this.asPosition(position.lineNumber, position.column), + context: { includeDeclaration: options.includeDeclaration }, + }; + } + + asDocumentSymbolParams(model: monaco.editor.IReadOnlyModel): DocumentSymbolParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + }; + } + + asCodeLensParams(model: monaco.editor.IReadOnlyModel): CodeLensParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + }; + } + + asDiagnosticSeverity(value: monaco.MarkerSeverity): DiagnosticSeverity | undefined { + switch (value) { + case this._monaco.MarkerSeverity.Error: + return DiagnosticSeverity.Error; + case this._monaco.MarkerSeverity.Warning: + return DiagnosticSeverity.Warning; + case this._monaco.MarkerSeverity.Info: + return DiagnosticSeverity.Information; + case this._monaco.MarkerSeverity.Hint: + return DiagnosticSeverity.Hint; + } + return undefined; + } + + asDiagnostic(marker: monaco.editor.IMarkerData): Diagnostic { + const range = this.asRange( + new this._monaco.Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), + ); + const severity = this.asDiagnosticSeverity(marker.severity); + const diag = Diagnostic.create(range, marker.message, severity, marker.code as string, marker.source); + return diag; + } + + asDiagnostics(markers: monaco.editor.IMarkerData[]): Diagnostic[] { + if (markers === void 0 || markers === null) { + return markers; + } + return markers.map((marker) => this.asDiagnostic(marker)); + } + + asCodeActionContext(context: monaco.languages.CodeActionContext, diagnostics: Diagnostic[]): CodeActionContext { + if (context === void 0 || context === null) { + return context; + } + // FIXME: CodeActionTriggerKind is missing + return CodeActionContext.create(diagnostics, Is.string(context.only) ? [context.only] : undefined, undefined); + } + + asCodeActionParams( + model: monaco.editor.IReadOnlyModel, + range: monaco.Range, + context: monaco.languages.CodeActionContext, + diagnostics: Diagnostic[], + ): CodeActionParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + range: this.asRange(range), + context: this.asCodeActionContext(context, diagnostics), + }; + } + + asCommand(item: monaco.languages.Command | undefined | null): Command | undefined { + if (item) { + let args = item.arguments || []; + return Command.create(item.title, item.id, ...args); + } + return undefined; + } + + asCodeLens(item: monaco.languages.CodeLens): CodeLens { + let result = CodeLens.create(this.asRange(item.range)); + if (item.command) { + result.command = this.asCommand(item.command); + } + if (ProtocolCodeLens.is(item)) { + if (item.data) { + result.data = item.data; + } + } + return result; + } + + asFormattingOptions(options: monaco.languages.FormattingOptions): FormattingOptions { + return { tabSize: options.tabSize, insertSpaces: options.insertSpaces }; + } + + asDocumentFormattingParams( + model: monaco.editor.IReadOnlyModel, + options: monaco.languages.FormattingOptions, + ): DocumentFormattingParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + options: this.asFormattingOptions(options), + }; + } + + asDocumentRangeFormattingParams( + model: monaco.editor.IReadOnlyModel, + range: monaco.Range, + options: monaco.languages.FormattingOptions, + ): DocumentRangeFormattingParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + range: this.asRange(range), + options: this.asFormattingOptions(options), + }; + } + + asDocumentOnTypeFormattingParams( + model: monaco.editor.IReadOnlyModel, + position: monaco.IPosition, + ch: string, + options: monaco.languages.FormattingOptions, + ): DocumentOnTypeFormattingParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + position: this.asPosition(position.lineNumber, position.column), + ch, + options: this.asFormattingOptions(options), + }; + } + + asRenameParams(model: monaco.editor.IReadOnlyModel, position: monaco.IPosition, newName: string): RenameParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + position: this.asPosition(position.lineNumber, position.column), + newName, + }; + } + + asDocumentLinkParams(model: monaco.editor.IReadOnlyModel): DocumentLinkParams { + return { + textDocument: this.asTextDocumentIdentifier(model), + }; + } + + asDocumentLink(item: monaco.languages.ILink): DocumentLink { + let result = DocumentLink.create(this.asRange(item.range)); + if (item.url) { + result.target = typeof item.url === "string" ? item.url : item.url.toString(); + } + if (ProtocolDocumentLink.is(item) && item.data) { + result.data = item.data; + } + if (item.tooltip) { + result.tooltip = item.tooltip; + } + return result; + } + + asCodeAction(item: monaco.languages.CodeAction): CodeAction { + const result: CodeAction = { title: item.title }; + const protocolCodeAction = ProtocolCodeAction.is(item) ? item : undefined; + if (Is.number(item.kind)) { + result.kind = item.kind; + } + if (item.diagnostics) { + result.diagnostics = this.asDiagnostics(item.diagnostics); + } + if (item.edit) { + throw new Error(`VS Code code actions can only be converted to a protocol code action without an edit.`); + } + if (item.command) { + result.command = this.asCommand(item.command); + } + if (item.isPreferred !== undefined) { + result.isPreferred = item.isPreferred; + } + if (item.disabled) { + result.disabled = { reason: item.disabled }; + } + if (protocolCodeAction) { + if (protocolCodeAction.data !== undefined) { + result.data = protocolCodeAction.data; + } + } + return result; + } + + asInlayHintLabelPart(part: monaco.languages.InlayHintLabelPart): InlayHintLabelPart { + return { + value: part.label, + command: this.asCommand(part.command), + location: this.asLocation(part.location), + tooltip: this.asMarkupContent(part.tooltip), + }; + } + + asInlayHintLabel(label: string | monaco.languages.InlayHintLabelPart[]): string | InlayHintLabelPart[] { + if (Array.isArray(label)) { + return label.map((part) => this.asInlayHintLabelPart(part)); + } + return label; + } + + asInlayHint(item: monaco.languages.InlayHint): InlayHint { + let result = InlayHint.create( + this.asPosition(item.position.lineNumber, item.position.column), + this.asInlayHintLabel(item.label), + item.kind, + ); + if (ProtocolInlayHint.is(item)) { + if (item.data) { + result.data = item.data; + } + } + return result; + } +} + +export class ProtocolToMonacoConverter { + public constructor(protected readonly _monaco: typeof monaco) {} + + asResourceEdits( + resource: monaco.Uri, + edits: (TextEdit | AnnotatedTextEdit)[], + asMetadata: ( + annotation: ls.ChangeAnnotationIdentifier | undefined, + ) => monaco.languages.WorkspaceEditMetadata | undefined, + modelVersionId?: number, + ): monaco.languages.IWorkspaceTextEdit[] { + return edits.map( + (edit) => + ({ + resource: resource, + edit: this.asTextEdit(edit), + modelVersionId, + metadata: AnnotatedTextEdit.is(edit) ? asMetadata(edit.annotationId) : undefined, + } as any), + ); + } + + asWorkspaceEditMetadata(changeAnnotation: ChangeAnnotation): monaco.languages.WorkspaceEditMetadata { + return { + needsConfirmation: changeAnnotation.needsConfirmation === true, + label: changeAnnotation.label, + description: changeAnnotation.description, + }; + } + + asWorkspaceEdit(item: WorkspaceEdit): monaco.languages.WorkspaceEdit; + asWorkspaceEdit(item: undefined | null): undefined; + asWorkspaceEdit(item: WorkspaceEdit | undefined | null): monaco.languages.WorkspaceEdit | undefined; + asWorkspaceEdit(item: WorkspaceEdit | undefined | null): monaco.languages.WorkspaceEdit | undefined { + if (!item) { + return undefined; + } + const sharedMetadata: Map = new Map(); + if (item.changeAnnotations !== undefined) { + for (const key of Object.keys(item.changeAnnotations)) { + const metaData = this.asWorkspaceEditMetadata(item.changeAnnotations[key]); + sharedMetadata.set(key, metaData); + } + } + const asMetadata = ( + annotation: ls.ChangeAnnotationIdentifier | undefined, + ): monaco.languages.WorkspaceEditMetadata | undefined => { + if (annotation === undefined) { + return undefined; + } else { + return sharedMetadata.get(annotation); + } + }; + const edits: (monaco.languages.IWorkspaceTextEdit | monaco.languages.IWorkspaceFileEdit)[] = []; + if (item.documentChanges) { + item.documentChanges.forEach((change) => { + if (ls.CreateFile.is(change)) { + edits.push({ + newUri: this._monaco.Uri.parse(change.uri), + options: change.options, + metadata: asMetadata(change.annotationId), + }); + } else if (ls.RenameFile.is(change)) { + edits.push({ + oldUri: this._monaco.Uri.parse(change.oldUri), + newUri: this._monaco.Uri.parse(change.newUri), + options: change.options, + metadata: asMetadata(change.annotationId), + }); + } else if (ls.DeleteFile.is(change)) { + edits.push({ + oldUri: this._monaco.Uri.parse(change.uri), + options: change.options, + metadata: asMetadata(change.annotationId), + }); + } else if (ls.TextDocumentEdit.is(change)) { + const resource = this._monaco.Uri.parse(change.textDocument.uri); + const version = typeof change.textDocument.version === "number" ? change.textDocument.version : undefined; + edits.push(...this.asResourceEdits(resource, change.edits, asMetadata, version)); + } else { + console.error(`Unknown workspace edit change received:\n${JSON.stringify(change, undefined, 4)}`); + } + }); + } else if (item.changes) { + for (const key of Object.keys(item.changes)) { + const resource = this._monaco.Uri.parse(key); + edits.push(...this.asResourceEdits(resource, item.changes[key], asMetadata)); + } + } + return { + edits, + }; + } + + asTextEdit(edit: TextEdit): monaco.languages.TextEdit; + asTextEdit(edit: undefined | null): undefined; + asTextEdit(edit: TextEdit | undefined | null): undefined; + asTextEdit(edit: TextEdit | undefined | null): monaco.languages.TextEdit | undefined { + if (!edit) { + return undefined; + } + const range = this.asRange(edit.range)!; + return { + range, + text: edit.newText, + }; + } + + asTextEdits(items: TextEdit[]): monaco.languages.TextEdit[]; + asTextEdits(items: undefined | null): undefined; + asTextEdits(items: TextEdit[] | undefined | null): monaco.languages.TextEdit[] | undefined; + asTextEdits(items: TextEdit[] | undefined | null): monaco.languages.TextEdit[] | undefined { + if (!items) { + return undefined; + } + return items.map((item) => this.asTextEdit(item)); + } + + asCodeLens(item: CodeLens): monaco.languages.CodeLens; + asCodeLens(item: undefined | null): undefined; + asCodeLens(item: CodeLens | undefined | null): monaco.languages.CodeLens | undefined; + asCodeLens(item: CodeLens | undefined | null): monaco.languages.CodeLens | undefined { + if (!item) { + return undefined; + } + const range = this.asRange(item.range); + let result = { range }; + if (item.command) { + result.command = this.asCommand(item.command); + } + if (item.data !== void 0 && item.data !== null) { + result.data = item.data; + } + return result; + } + + asCodeLensList(items: CodeLens[]): monaco.languages.CodeLensList; + asCodeLensList(items: undefined | null): undefined; + asCodeLensList(items: CodeLens[] | undefined | null): monaco.languages.CodeLensList | undefined; + asCodeLensList(items: CodeLens[] | undefined | null): monaco.languages.CodeLensList | undefined { + if (!items) { + return undefined; + } + return { + lenses: items.map((codeLens) => this.asCodeLens(codeLens)), + dispose: () => {}, + }; + } + + asCodeActionList(actions: (Command | CodeAction)[]): monaco.languages.CodeActionList { + return { + actions: actions.map((action) => this.asCodeAction(action)), + dispose: () => {}, + }; + } + + asCodeAction(item: Command | CodeAction): ProtocolCodeAction { + if (Command.is(item)) { + return { + command: { + id: item.command, + title: item.title, + arguments: item.arguments, + }, + title: item.title, + }; + } + return { + title: item.title, + command: this.asCommand(item.command), + edit: this.asWorkspaceEdit(item.edit), + diagnostics: this.asDiagnostics(item.diagnostics), + kind: item.kind, + disabled: item.disabled ? item.disabled.reason : undefined, + isPreferred: item.isPreferred, + data: item.data, + }; + } + + asCommand(command: Command): monaco.languages.Command; + asCommand(command: undefined): undefined; + asCommand(command: Command | undefined): monaco.languages.Command | undefined; + asCommand(command: Command | undefined): monaco.languages.Command | undefined { + if (!command) { + return undefined; + } + return { + id: command.command, + title: command.title, + arguments: command.arguments, + }; + } + + asDocumentSymbol(value: DocumentSymbol): monaco.languages.DocumentSymbol { + const children = value.children && value.children.map((c) => this.asDocumentSymbol(c)); + return { + name: value.name, + detail: value.detail || "", + kind: this.asSymbolKind(value.kind), + tags: value.tags || [], + range: this.asRange(value.range), + selectionRange: this.asRange(value.selectionRange), + children, + }; + } + + asDocumentSymbols(values: SymbolInformation[] | DocumentSymbol[]): monaco.languages.DocumentSymbol[] { + if (DocumentSymbol.is(values[0])) { + return (values as DocumentSymbol[]).map((s) => this.asDocumentSymbol(s)); + } + return this.asSymbolInformations(values as SymbolInformation[]); + } + + asSymbolInformations(values: SymbolInformation[], uri?: monaco.Uri): monaco.languages.DocumentSymbol[]; + asSymbolInformations(values: undefined | null, uri?: monaco.Uri): undefined; + asSymbolInformations( + values: SymbolInformation[] | undefined | null, + uri?: monaco.Uri, + ): monaco.languages.DocumentSymbol[] | undefined; + asSymbolInformations( + values: SymbolInformation[] | undefined | null, + uri?: monaco.Uri, + ): monaco.languages.DocumentSymbol[] | undefined { + if (!values) { + return undefined; + } + return values.map((information) => this.asSymbolInformation(information, uri)); + } + + asSymbolInformation(item: SymbolInformation, uri?: monaco.Uri): monaco.languages.DocumentSymbol { + const location = this.asLocation(uri ? { ...item.location, uri: uri.toString() } : item.location); + return { + name: item.name, + detail: "", + containerName: item.containerName, + kind: this.asSymbolKind(item.kind), + tags: item.tags || [], + range: location.range, + selectionRange: location.range, + }; + } + + asSymbolKind(item: SymbolKind): monaco.languages.SymbolKind { + if (item <= SymbolKind.TypeParameter) { + // Symbol kind is one based in the protocol and zero based in code. + return item - 1; + } + return this._monaco.languages.SymbolKind.Property; + } + + asDocumentHighlights(values: DocumentHighlight[]): monaco.languages.DocumentHighlight[]; + asDocumentHighlights(values: undefined | null): undefined; + asDocumentHighlights( + values: DocumentHighlight[] | undefined | null, + ): monaco.languages.DocumentHighlight[] | undefined; + asDocumentHighlights( + values: DocumentHighlight[] | undefined | null, + ): monaco.languages.DocumentHighlight[] | undefined { + if (!values) { + return undefined; + } + return values.map((item) => this.asDocumentHighlight(item)); + } + + asDocumentHighlight(item: DocumentHighlight): monaco.languages.DocumentHighlight { + const range = this.asRange(item.range)!; + const kind = Is.number(item.kind) ? this.asDocumentHighlightKind(item.kind) : undefined!; + return { range, kind }; + } + + asDocumentHighlightKind(item: number): monaco.languages.DocumentHighlightKind { + switch (item) { + case DocumentHighlightKind.Text: + return this._monaco.languages.DocumentHighlightKind.Text; + case DocumentHighlightKind.Read: + return this._monaco.languages.DocumentHighlightKind.Read; + case DocumentHighlightKind.Write: + return this._monaco.languages.DocumentHighlightKind.Write; + } + return this._monaco.languages.DocumentHighlightKind.Text; + } + + asReferences(values: Location[]): monaco.languages.Location[]; + asReferences(values: undefined | null): monaco.languages.Location[] | undefined; + asReferences(values: Location[] | undefined | null): monaco.languages.Location[] | undefined; + asReferences(values: Location[] | undefined | null): monaco.languages.Location[] | undefined { + if (!values) { + return undefined; + } + return values.map((location) => this.asLocation(location)); + } + + asDefinitionResult(item: Definition): monaco.languages.Definition; + asDefinitionResult(item: DefinitionLink[]): monaco.languages.Definition; + asDefinitionResult(item: undefined | null): undefined; + asDefinitionResult(item: Definition | DefinitionLink[] | undefined | null): monaco.languages.Definition | undefined; + asDefinitionResult(item: Definition | DefinitionLink[] | undefined | null): monaco.languages.Definition | undefined { + if (!item) { + return undefined; + } + if (Is.array(item)) { + if (item.length == 0) { + return undefined; + } else if (LocationLink.is(item[0])) { + let links: LocationLink[] = item as LocationLink[]; + return links.map((location) => this.asLocationLink(location)); + } else { + let locations: Location[] = item as Location[]; + return locations.map((location) => this.asLocation(location)); + } + } else { + return this.asLocation(item); + } + } + + asLocation(item: Location): monaco.languages.Location; + asLocation(item: undefined | null): undefined; + asLocation(item: Location | undefined | null): monaco.languages.Location | undefined; + asLocation(item: Location | undefined | null): monaco.languages.Location | undefined { + if (!item) { + return undefined; + } + const uri = this._monaco.Uri.parse(item.uri); + const range = this.asRange(item.range)!; + return { + uri, + range, + }; + } + + asLocationLink(item: undefined | null): undefined; + asLocationLink(item: ls.LocationLink): monaco.languages.LocationLink; + asLocationLink(item: ls.LocationLink | undefined | null): monaco.languages.LocationLink | undefined { + if (!item) { + return undefined; + } + let result: monaco.languages.LocationLink = { + uri: this._monaco.Uri.parse(item.targetUri), + range: this.asRange(item.targetSelectionRange)!, // See issue: https://github.com/Microsoft/vscode/issues/58649 + originSelectionRange: this.asRange(item.originSelectionRange), + targetSelectionRange: this.asRange(item.targetSelectionRange), + }; + if (!result.targetSelectionRange) { + throw new Error(`targetSelectionRange must not be undefined or null`); + } + return result; + } + + asSignatureHelpResult(item: undefined | null): undefined; + asSignatureHelpResult(item: SignatureHelp): monaco.languages.SignatureHelpResult; + asSignatureHelpResult(item: SignatureHelp | undefined | null): monaco.languages.SignatureHelpResult | undefined; + asSignatureHelpResult(item: SignatureHelp | undefined | null): monaco.languages.SignatureHelpResult | undefined { + if (!item) { + return undefined; + } + let result = {}; + if (Is.number(item.activeSignature)) { + result.activeSignature = item.activeSignature; + } else { + // activeSignature was optional in the past + result.activeSignature = 0; + } + if (Is.number(item.activeParameter)) { + result.activeParameter = item.activeParameter; + } else { + // activeParameter was optional in the past + result.activeParameter = 0; + } + if (item.signatures) { + result.signatures = this.asSignatureInformations(item.signatures); + } else { + result.signatures = []; + } + return { + value: result, + dispose: () => {}, + }; + } + + asSignatureInformations(items: SignatureInformation[]): monaco.languages.SignatureInformation[] { + return items.map((item) => this.asSignatureInformation(item)); + } + + asSignatureInformation(item: SignatureInformation): monaco.languages.SignatureInformation { + let result = { label: item.label }; + if (item.documentation) { + result.documentation = this.asDocumentation(item.documentation); + } + if (item.parameters) { + result.parameters = this.asParameterInformations(item.parameters); + } else { + result.parameters = []; + } + if (item.activeParameter) { + result.activeParameter = item.activeParameter; + } + return result; + } + + asParameterInformations(item: ParameterInformation[]): monaco.languages.ParameterInformation[] { + return item.map((item) => this.asParameterInformation(item)); + } + + asParameterInformation(item: ParameterInformation): monaco.languages.ParameterInformation { + let result = { label: item.label }; + if (item.documentation) { + result.documentation = this.asDocumentation(item.documentation); + } + return result; + } + + asHover(hover: Hover): monaco.languages.Hover; + asHover(hover: undefined | null): undefined; + asHover(hover: Hover | undefined | null): monaco.languages.Hover | undefined; + asHover(hover: Hover | undefined | null): monaco.languages.Hover | undefined { + if (!hover) { + return undefined; + } + return { + contents: this.asHoverContent(hover.contents), + range: this.asRange(hover.range), + }; + } + + asHoverContent(contents: MarkedString | MarkedString[] | MarkupContent): monaco.IMarkdownString[] { + if (Array.isArray(contents)) { + return contents.map((content) => this.asMarkdownString(content)); + } + return [this.asMarkdownString(contents)]; + } + + asDocumentation(value: string | MarkupContent): string | monaco.IMarkdownString { + if (Is.string(value)) { + return value; + } + if (value.kind === MarkupKind.PlainText) { + return value.value; + } + return this.asMarkdownString(value); + } + + asMarkdownString(content: MarkedString | MarkupContent): monaco.IMarkdownString { + if (MarkupContent.is(content)) { + return { + value: content.value, + }; + } + if (Is.string(content)) { + return { value: content }; + } + const { language, value } = content; + return { + value: "```" + language + "\n" + value + "\n```", + }; + } + + asSeverity(severity?: ls.DiagnosticSeverity): monaco.MarkerSeverity { + if (severity === 1) { + return this._monaco.MarkerSeverity.Error; + } + if (severity === 2) { + return this._monaco.MarkerSeverity.Warning; + } + if (severity === 3) { + return this._monaco.MarkerSeverity.Info; + } + return this._monaco.MarkerSeverity.Hint; + } + + asDiagnostics(diagnostics: undefined): undefined; + asDiagnostics(diagnostics: Diagnostic[]): monaco.editor.IMarkerData[]; + asDiagnostics(diagnostics: Diagnostic[] | undefined): monaco.editor.IMarkerData[] | undefined; + asDiagnostics(diagnostics: Diagnostic[] | undefined): monaco.editor.IMarkerData[] | undefined { + if (!diagnostics) { + return undefined; + } + return diagnostics.map((diagnostic) => this.asDiagnostic(diagnostic)); + } + + asDiagnostic(diagnostic: Diagnostic): monaco.editor.IMarkerData { + return { + code: typeof diagnostic.code === "number" ? diagnostic.code.toString() : diagnostic.code, + severity: this.asSeverity(diagnostic.severity), + message: diagnostic.message, + source: diagnostic.source, + startLineNumber: diagnostic.range.start.line + 1, + startColumn: diagnostic.range.start.character + 1, + endLineNumber: diagnostic.range.end.line + 1, + endColumn: diagnostic.range.end.character + 1, + relatedInformation: this.asRelatedInformations(diagnostic.relatedInformation), + tags: diagnostic.tags, + }; + } + + asRelatedInformations( + relatedInformation?: DiagnosticRelatedInformation[], + ): monaco.editor.IRelatedInformation[] | undefined { + if (!relatedInformation) { + return undefined; + } + return relatedInformation.map((item) => this.asRelatedInformation(item)); + } + + asRelatedInformation(relatedInformation: DiagnosticRelatedInformation): monaco.editor.IRelatedInformation { + return { + resource: this._monaco.Uri.parse(relatedInformation.location.uri), + startLineNumber: relatedInformation.location.range.start.line + 1, + startColumn: relatedInformation.location.range.start.character + 1, + endLineNumber: relatedInformation.location.range.end.line + 1, + endColumn: relatedInformation.location.range.end.character + 1, + message: relatedInformation.message, + }; + } + + asCompletionResult( + result: CompletionItem[] | CompletionList | null | undefined, + defaultRange: monaco.IRange, + ): monaco.languages.CompletionList { + if (!result) { + return { + incomplete: false, + suggestions: [], + }; + } + if (Array.isArray(result)) { + const suggestions = result.map((item) => this.asCompletionItem(item, defaultRange)); + return { + incomplete: false, + suggestions, + }; + } + return { + incomplete: result.isIncomplete, + suggestions: result.items.map((item) => this.asCompletionItem(item, defaultRange)), + }; + } + + asCompletionItem(item: CompletionItem, defaultRange: monaco.IRange | RangeReplace): ProtocolCompletionItem { + const result = { label: item.label }; + if (item.detail) { + result.detail = item.detail; + } + if (item.documentation) { + result.documentation = this.asDocumentation(item.documentation); + result.documentationFormat = Is.string(item.documentation) ? undefined : item.documentation.kind; + } + if (item.filterText) { + result.filterText = item.filterText; + } + const insertText = this.asCompletionInsertText(item, defaultRange); + result.insertText = insertText.insertText; + result.range = insertText.range; + result.fromEdit = insertText.fromEdit; + if (insertText.isSnippet) { + result.insertTextRules = this._monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; + } + if (Is.number(item.kind)) { + let [itemKind, original] = this.asCompletionItemKind(item.kind); + result.kind = itemKind; + if (original) { + result.originalItemKind = original; + } + } + if (item.sortText) { + result.sortText = item.sortText; + } + if (item.additionalTextEdits) { + result.additionalTextEdits = this.asTextEdits(item.additionalTextEdits); + } + if (Is.stringArray(item.commitCharacters)) { + result.commitCharacters = item.commitCharacters.slice(); + } + if (item.command) { + result.command = this.asCommand(item.command); + } + if (item.deprecated === true || item.deprecated === false) { + result.deprecated = item.deprecated; + } + if (item.preselect === true || item.preselect === false) { + result.preselect = item.preselect; + } + if (item.data !== undefined) { + result.data = item.data; + } + if (item.deprecated === true || item.deprecated === false) { + result.deprecated = item.deprecated; + } + if (item.insertTextMode) { + result.insertTextMode = item.insertTextMode; + } + if (item.tags) { + result.tags = item.tags; + } + return result; + } + + asCompletionItemKind( + value: CompletionItemKind, + ): [monaco.languages.CompletionItemKind, CompletionItemKind | undefined] { + if (CompletionItemKind.Text <= value && value <= CompletionItemKind.TypeParameter) { + switch (value) { + case CompletionItemKind.Text: + return [this._monaco.languages.CompletionItemKind.Text, undefined]; + case CompletionItemKind.Method: + return [this._monaco.languages.CompletionItemKind.Method, undefined]; + case CompletionItemKind.Function: + return [this._monaco.languages.CompletionItemKind.Function, undefined]; + case CompletionItemKind.Constructor: + return [this._monaco.languages.CompletionItemKind.Constructor, undefined]; + case CompletionItemKind.Field: + return [this._monaco.languages.CompletionItemKind.Field, undefined]; + case CompletionItemKind.Variable: + return [this._monaco.languages.CompletionItemKind.Variable, undefined]; + case CompletionItemKind.Class: + return [this._monaco.languages.CompletionItemKind.Class, undefined]; + case CompletionItemKind.Interface: + return [this._monaco.languages.CompletionItemKind.Interface, undefined]; + case CompletionItemKind.Module: + return [this._monaco.languages.CompletionItemKind.Module, undefined]; + case CompletionItemKind.Property: + return [this._monaco.languages.CompletionItemKind.Property, undefined]; + case CompletionItemKind.Unit: + return [this._monaco.languages.CompletionItemKind.Unit, undefined]; + case CompletionItemKind.Value: + return [this._monaco.languages.CompletionItemKind.Value, undefined]; + case CompletionItemKind.Enum: + return [this._monaco.languages.CompletionItemKind.Enum, undefined]; + case CompletionItemKind.Keyword: + return [this._monaco.languages.CompletionItemKind.Keyword, undefined]; + case CompletionItemKind.Snippet: + return [this._monaco.languages.CompletionItemKind.Snippet, undefined]; + case CompletionItemKind.Color: + return [this._monaco.languages.CompletionItemKind.Color, undefined]; + case CompletionItemKind.File: + return [this._monaco.languages.CompletionItemKind.File, undefined]; + case CompletionItemKind.Reference: + return [this._monaco.languages.CompletionItemKind.Reference, undefined]; + case CompletionItemKind.Folder: + return [this._monaco.languages.CompletionItemKind.Folder, undefined]; + case CompletionItemKind.EnumMember: + return [this._monaco.languages.CompletionItemKind.EnumMember, undefined]; + case CompletionItemKind.Constant: + return [this._monaco.languages.CompletionItemKind.Constant, undefined]; + case CompletionItemKind.Struct: + return [this._monaco.languages.CompletionItemKind.Struct, undefined]; + case CompletionItemKind.Event: + return [this._monaco.languages.CompletionItemKind.Event, undefined]; + case CompletionItemKind.Operator: + return [this._monaco.languages.CompletionItemKind.Operator, undefined]; + case CompletionItemKind.TypeParameter: + return [this._monaco.languages.CompletionItemKind.TypeParameter, undefined]; + default: + return [value - 1, undefined]; + } + } + return [CompletionItemKind.Text, value]; + } + + asCompletionInsertText( + item: CompletionItem, + defaultRange: monaco.IRange | RangeReplace, + ): { insertText: string; range: monaco.IRange | RangeReplace; fromEdit: boolean; isSnippet: boolean } { + const isSnippet = item.insertTextFormat === InsertTextFormat.Snippet; + if (item.textEdit) { + if (TextEdit.is(item.textEdit)) { + const range = this.asRange(item.textEdit.range); + const value = item.textEdit.newText; + return { isSnippet, insertText: value, range, fromEdit: true }; + } else { + const range = { + insert: this.asRange(item.textEdit.insert), + replace: this.asRange(item.textEdit.replace), + }; + const value = item.textEdit.newText; + return { isSnippet, insertText: value, range, fromEdit: true }; + } + } + if (item.insertText) { + return { isSnippet, insertText: item.insertText, fromEdit: false, range: defaultRange }; + } + return { insertText: item.label, range: defaultRange, fromEdit: false, isSnippet: false }; + } + + asDocumentLinks(documentLinks: DocumentLink[]): monaco.languages.ILinksList { + const links = documentLinks.map((link) => this.asDocumentLink(link)); + return { links }; + } + + asDocumentLink(documentLink: DocumentLink): ProtocolDocumentLink { + return { + range: this.asRange(documentLink.range), + url: documentLink.target, + data: documentLink.data, + tooltip: documentLink.tooltip, + }; + } + + asRange(range: null): null; + asRange(range: undefined): undefined; + asRange(range: Range): monaco.Range; + asRange(range: Range | undefined): monaco.Range | undefined; + asRange(range: Range | null): monaco.Range | null; + asRange(range: RecursivePartial): Partial; + asRange(range: RecursivePartial | undefined): monaco.Range | Partial | undefined; + asRange(range: RecursivePartial | null): monaco.Range | Partial | null; + asRange(range: RecursivePartial | undefined | null): monaco.Range | Partial | undefined | null { + if (range === undefined) { + return undefined; + } + if (range === null) { + return null; + } + const start = this.asPosition(range.start); + const end = this.asPosition(range.end); + if (start instanceof this._monaco.Position && end instanceof this._monaco.Position) { + return new this._monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column); + } + const startLineNumber = !start || start.lineNumber === undefined ? undefined : start.lineNumber; + const startColumn = !start || start.column === undefined ? undefined : start.column; + const endLineNumber = !end || end.lineNumber === undefined ? undefined : end.lineNumber; + const endColumn = !end || end.column === undefined ? undefined : end.column; + return { startLineNumber, startColumn, endLineNumber, endColumn }; + } + + asPosition(position: null): null; + asPosition(position: undefined): undefined; + asPosition(position: Position): monaco.Position; + asPosition(position: Position | undefined): monaco.Position | undefined; + asPosition(position: Position | null): monaco.Position | null; + asPosition(position: Partial): Partial; + asPosition(position: Partial | undefined): monaco.Position | Partial | undefined; + asPosition(position: Partial | null): monaco.Position | Partial | null; + asPosition( + position: Partial | undefined | null, + ): monaco.Position | Partial | undefined | null { + if (position === undefined) { + return undefined; + } + if (position === null) { + return null; + } + const { line, character } = position; + const lineNumber = line === undefined ? undefined : line + 1; + const column = character === undefined ? undefined : character + 1; + if (lineNumber !== undefined && column !== undefined) { + return new this._monaco.Position(lineNumber, column); + } + return { lineNumber, column }; + } + + asColorInformations(items: ColorInformation[]): monaco.languages.IColorInformation[] { + return items.map((item) => this.asColorInformation(item)); + } + + asColorInformation(item: ColorInformation): monaco.languages.IColorInformation { + return { + range: this.asRange(item.range), + color: item.color, + }; + } + + asColorPresentations(items: ColorPresentation[]): monaco.languages.IColorPresentation[] { + return items.map((item) => this.asColorPresentation(item)); + } + + asColorPresentation(item: ColorPresentation): monaco.languages.IColorPresentation { + return { + label: item.label, + textEdit: this.asTextEdit(item.textEdit), + additionalTextEdits: this.asTextEdits(item.additionalTextEdits), + }; + } + + asFoldingRanges(items: undefined | null): undefined | null; + asFoldingRanges(items: FoldingRange[]): monaco.languages.FoldingRange[]; + asFoldingRanges(items: FoldingRange[] | undefined | null): monaco.languages.FoldingRange[] | undefined | null { + if (!items) { + return items; + } + return items.map((item) => this.asFoldingRange(item)); + } + + asFoldingRange(item: FoldingRange): monaco.languages.FoldingRange { + return { + start: item.startLine + 1, + end: item.endLine + 1, + kind: this.asFoldingRangeKind(item.kind), + }; + } + + asFoldingRangeKind(kind?: string): monaco.languages.FoldingRangeKind | undefined { + if (kind) { + switch (kind) { + case FoldingRangeKind.Comment: + return this._monaco.languages.FoldingRangeKind.Comment; + case FoldingRangeKind.Imports: + return this._monaco.languages.FoldingRangeKind.Imports; + case FoldingRangeKind.Region: + return this._monaco.languages.FoldingRangeKind.Region; + } + } + return undefined; + } + + asSemanticTokens(semanticTokens: SemanticTokens): monaco.languages.SemanticTokens { + return { + resultId: semanticTokens.resultId, + data: Uint32Array.from(semanticTokens.data), + }; + } + + asInlayHintLabelPart(part: InlayHintLabelPart): monaco.languages.InlayHintLabelPart { + return { + label: part.value, + command: this.asCommand(part.command), + location: this.asLocation(part.location), + tooltip: part.tooltip && this.asMarkdownString(part.tooltip), + }; + } + + asInlayHintLabel(label: string | InlayHintLabelPart[]): string | monaco.languages.InlayHintLabelPart[] { + if (Array.isArray(label)) { + return label.map((part) => this.asInlayHintLabelPart(part)); + } + return label; + } + + asInlayHint(inlayHint: InlayHint): ProtocolInlayHint { + return { + data: inlayHint.data, + label: this.asInlayHintLabel(inlayHint.label), + position: this.asPosition(inlayHint.position), + kind: inlayHint.kind, + paddingLeft: inlayHint.paddingLeft, + paddingRight: inlayHint.paddingRight, + tooltip: inlayHint.tooltip && this.asMarkdownString(inlayHint.tooltip), + }; + } + + asInlayHintList(items: InlayHint[]): monaco.languages.InlayHintList; + asInlayHintList(items: undefined | null): undefined; + asInlayHintList(items: InlayHint[] | undefined | null): monaco.languages.InlayHintList | undefined; + asInlayHintList(items: InlayHint[] | undefined | null): monaco.languages.InlayHintList | undefined { + if (!items) { + return undefined; + } + return { + hints: items.map((hint) => this.asInlayHint(hint)), + dispose: () => {}, + }; + } +} diff --git a/packages/frontend/src/lib/editor/converter/services.ts b/packages/frontend/src/lib/editor/converter/services.ts new file mode 100644 index 0000000..363a8a0 --- /dev/null +++ b/packages/frontend/src/lib/editor/converter/services.ts @@ -0,0 +1,417 @@ +//@ts-nocheck + +import { + DocumentSelector, + MessageActionItem, + MessageType, + TextDocumentPositionParams, + ReferenceParams, + CodeActionParams, + CodeLensParams, + DocumentFormattingParams, + DocumentRangeFormattingParams, + DocumentOnTypeFormattingParams, + RenameParams, + DocumentLinkParams, + WorkspaceClientCapabilities, + Diagnostic, + CompletionItem, + CompletionList, + Hover, + SignatureHelp, + Definition, + Location, + DocumentHighlight, + SymbolInformation, + Command, + CodeLens, + TextEdit, + WorkspaceEdit, + DocumentLink, + TextDocumentSaveReason, + DocumentSymbolParams, + WorkspaceSymbolParams, + TextDocumentContentChangeEvent, + CompletionParams, + ColorInformation, + ColorPresentation, + DocumentColorParams, + ColorPresentationParams, + FoldingRange, + FoldingRangeParams, + DocumentSymbol, + CodeAction, + Declaration, + SelectionRangeParams, + SelectionRange, + SemanticTokensParams, + SemanticTokens, + SemanticTokensEdit, + SemanticTokensLegend, + SemanticTokensRangeParams, + SemanticTokensDeltaParams, + InlayHint, + InlayHintParams, +} from "vscode-languageserver-protocol"; + +import { TextDocument } from "vscode-languageserver-textdocument"; + +import { Disposable, CancellationToken, Event, Emitter } from "vscode-jsonrpc"; + +import { URI as Uri } from "vscode-uri"; +// import { TextDocumentShowOptions } from 'vscode'; + +export { Disposable, CancellationToken, Event, Emitter }; +export * from "vscode-languageserver-protocol/lib/common/api"; + +export { TextDocument }; + +export interface Services { + languages: Languages; + workspace: Workspace; + commands?: Commands; + window?: Window; + env?: Env; +} +export namespace Services { + const global = window as any; + const symbol = Symbol("Services"); + export type Provider = () => Services; + export const get: Provider = () => { + const services = global[symbol]; + if (!services) { + throw new Error("Language Client services has not been installed"); + } + return services; + }; + export function install(services: Services): Disposable { + if (global[symbol]) { + console.warn("Language Client services have been overridden"); + } + global[symbol] = services; + + return Disposable.create(() => (global[symbol] = undefined)); + } +} + +export interface DiagnosticCollection extends Disposable { + set(uri: string, diagnostics: Diagnostic[]): void; + get(uri: string): Diagnostic[]; +} + +export type ProviderResult = T | undefined | null | PromiseLike; + +export interface CompletionItemProvider { + provideCompletionItems( + params: CompletionParams, + token: CancellationToken, + ): ProviderResult; + resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult; +} + +export interface HoverProvider { + provideHover(params: TextDocumentPositionParams, token: CancellationToken): ProviderResult; +} + +export enum SignatureHelpTriggerKind { + Invoke = 1, + TriggerCharacter = 2, + ContentChange = 3, +} + +// runtime support +export enum VsCodeDiagnosticSeverity { + Error = 0, + Warning = 1, + Information = 2, + Hint = 3, +} + +export enum ProgressLocation { + SourceControl = 1, + Window = 10, + Notification = 15, +} + +export interface SignatureHelpContext { + readonly triggerKind: SignatureHelpTriggerKind; + readonly triggerCharacter?: string; + readonly isRetrigger: boolean; + readonly activeSignatureHelp?: SignatureHelp; +} + +export interface SignatureHelpProvider { + readonly triggerCharacters?: ReadonlyArray; + readonly retriggerCharacters?: ReadonlyArray; + provideSignatureHelp( + params: TextDocumentPositionParams, + token: CancellationToken, + context: SignatureHelpContext, + ): ProviderResult; +} + +export interface DefinitionProvider { + provideDefinition(params: TextDocumentPositionParams, token: CancellationToken): ProviderResult; +} + +export interface ReferenceProvider { + provideReferences(params: ReferenceParams, token: CancellationToken): ProviderResult; +} + +export interface DocumentHighlightProvider { + provideDocumentHighlights( + params: TextDocumentPositionParams, + token: CancellationToken, + ): ProviderResult; +} + +export interface DocumentSymbolProvider { + provideDocumentSymbols( + params: DocumentSymbolParams, + token: CancellationToken, + ): ProviderResult; +} + +export interface WorkspaceSymbolProvider { + provideWorkspaceSymbols(params: WorkspaceSymbolParams, token: CancellationToken): ProviderResult; +} + +export interface CodeActionProvider { + provideCodeActions(params: CodeActionParams, token: CancellationToken): ProviderResult<(Command | CodeAction)[]>; + resolveCodeAction?(codeAction: T, token: CancellationToken): ProviderResult; +} + +export interface CodeLensProvider { + provideCodeLenses(params: CodeLensParams, token: CancellationToken): ProviderResult; + resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult; +} + +export interface DocumentFormattingEditProvider { + provideDocumentFormattingEdits( + params: DocumentFormattingParams, + token: CancellationToken, + ): ProviderResult; +} + +export interface DocumentRangeFormattingEditProvider { + provideDocumentRangeFormattingEdits( + params: DocumentRangeFormattingParams, + token: CancellationToken, + ): ProviderResult; +} + +export interface OnTypeFormattingEditProvider { + provideOnTypeFormattingEdits( + params: DocumentOnTypeFormattingParams, + token: CancellationToken, + ): ProviderResult; +} + +export interface RenameProvider { + provideRenameEdits(params: RenameParams, token: CancellationToken): ProviderResult; +} + +export interface DocumentLinkProvider { + provideDocumentLinks(params: DocumentLinkParams, token: CancellationToken): ProviderResult; + resolveDocumentLink?(link: DocumentLink, token: CancellationToken): ProviderResult; +} + +export interface DocumentIdentifier { + uri: string; + languageId: string; +} +export namespace DocumentIdentifier { + export function is(arg: any): arg is DocumentIdentifier { + return !!arg && "uri" in arg && "languageId" in arg; + } +} + +export interface ImplementationProvider { + provideImplementation(params: TextDocumentPositionParams, token: CancellationToken): ProviderResult; +} + +export interface TypeDefinitionProvider { + provideTypeDefinition(params: TextDocumentPositionParams, token: CancellationToken): ProviderResult; +} + +export interface DeclarationProvider { + provideDeclaration(params: TextDocumentPositionParams, token: CancellationToken): ProviderResult; +} + +export interface DocumentColorProvider { + provideDocumentColors(params: DocumentColorParams, token: CancellationToken): ProviderResult; + provideColorPresentations( + params: ColorPresentationParams, + token: CancellationToken, + ): ProviderResult; +} + +export interface FoldingRangeProvider { + provideFoldingRanges(params: FoldingRangeParams, token: CancellationToken): ProviderResult; +} + +export interface SelectionRangeProvider { + provideSelectionRanges(params: SelectionRangeParams, token: CancellationToken): ProviderResult; +} + +export interface DocumentSemanticTokensProvider { + onDidChange?: Event; + provideDocumentSemanticTokens(params: SemanticTokensParams, token: CancellationToken): ProviderResult; + provideDocumentSemanticTokensEdits?( + params: SemanticTokensDeltaParams, + token: CancellationToken, + ): ProviderResult; +} + +export interface DocumentRangeSemanticTokensProvider { + provideDocumentRangeSemanticTokens( + params: SemanticTokensRangeParams, + token: CancellationToken, + ): ProviderResult; +} + +export interface InlayHintsProvider { + onDidChangeInlayHints?: Event; + provideInlayHints(params: InlayHintParams, token: CancellationToken): ProviderResult; + resolveInlayHint?(hint: T, token: CancellationToken): ProviderResult; +} + +export interface Languages { + match(selector: DocumentSelector, document: DocumentIdentifier): boolean; + createDiagnosticCollection?(name?: string): DiagnosticCollection; + registerCompletionItemProvider?( + selector: DocumentSelector, + provider: CompletionItemProvider, + ...triggerCharacters: string[] + ): Disposable; + registerHoverProvider?(selector: DocumentSelector, provider: HoverProvider): Disposable; + registerSignatureHelpProvider?(selector: DocumentSelector, provider: SignatureHelpProvider): Disposable; + registerDefinitionProvider?(selector: DocumentSelector, provider: DefinitionProvider): Disposable; + registerReferenceProvider?(selector: DocumentSelector, provider: ReferenceProvider): Disposable; + registerDocumentHighlightProvider?(selector: DocumentSelector, provider: DocumentHighlightProvider): Disposable; + registerDocumentSymbolProvider?(selector: DocumentSelector, provider: DocumentSymbolProvider): Disposable; + registerWorkspaceSymbolProvider?(provider: WorkspaceSymbolProvider): Disposable; + registerCodeActionsProvider?(selector: DocumentSelector, provider: CodeActionProvider): Disposable; + registerCodeLensProvider?(selector: DocumentSelector, provider: CodeLensProvider): Disposable; + registerDocumentFormattingEditProvider?( + selector: DocumentSelector, + provider: DocumentFormattingEditProvider, + ): Disposable; + registerDocumentRangeFormattingEditProvider?( + selector: DocumentSelector, + provider: DocumentRangeFormattingEditProvider, + ): Disposable; + registerOnTypeFormattingEditProvider?( + selector: DocumentSelector, + provider: OnTypeFormattingEditProvider, + firstTriggerCharacter: string, + ...moreTriggerCharacter: string[] + ): Disposable; + registerRenameProvider?(selector: DocumentSelector, provider: RenameProvider): Disposable; + registerDocumentLinkProvider?(selector: DocumentSelector, provider: DocumentLinkProvider): Disposable; + registerImplementationProvider?(selector: DocumentSelector, provider: ImplementationProvider): Disposable; + registerTypeDefinitionProvider?(selector: DocumentSelector, provider: TypeDefinitionProvider): Disposable; + registerDeclarationProvider?(selector: DocumentSelector, provider: DeclarationProvider): Disposable; + registerColorProvider?(selector: DocumentSelector, provider: DocumentColorProvider): Disposable; + registerFoldingRangeProvider?(selector: DocumentSelector, provider: FoldingRangeProvider): Disposable; + registerSelectionRangeProvider?(selector: DocumentSelector, provider: SelectionRangeProvider): Disposable; + registerDocumentSemanticTokensProvider?( + selector: DocumentSelector, + provider: DocumentSemanticTokensProvider, + legend: SemanticTokensLegend, + ): Disposable; + registerDocumentRangeSemanticTokensProvider?( + selector: DocumentSelector, + provider: DocumentRangeSemanticTokensProvider, + legend: SemanticTokensLegend, + ): Disposable; + registerInlayHintsProvider(selector: DocumentSelector, provider: InlayHintsProvider): Disposable; +} + +export interface TextDocumentDidChangeEvent { + readonly textDocument: TextDocument; + readonly contentChanges: TextDocumentContentChangeEvent[]; + readonly isUndoing: boolean; + readonly isRedoing: boolean; +} + +export interface TextDocumentWillSaveEvent { + readonly textDocument: TextDocument; + readonly reason: TextDocumentSaveReason; + waitUntil?(PromiseLike: PromiseLike): void; +} + +export enum ConfigurationTarget { + Global = 1, + Workspace = 2, + WorkspaceFolder = 3, +} + +export interface WorkspaceConfiguration { + toJSON(): any; + get(section: string): T | undefined; + get(section: string, defaultValue: T): T; + has(section: string): boolean; + readonly [key: string]: any; +} + +export interface FileSystemWatcher extends Disposable { + readonly onDidCreate: Event; + readonly onDidChange: Event; + readonly onDidDelete: Event; +} + +export interface ConfigurationChangeEvent { + affectsConfiguration(section: string): boolean; +} +export interface Configurations { + getConfiguration(section?: string, resource?: string): WorkspaceConfiguration; + readonly onDidChangeConfiguration: Event; +} + +export interface Workspace { + readonly capabilities?: WorkspaceClientCapabilities; + readonly rootPath?: string | null; + readonly rootUri: string | null; + readonly workspaceFolders?: any; + readonly onDidChangeWorkspaceFolders?: any; + readonly textDocuments: TextDocument[]; + readonly onDidOpenTextDocument: Event; + readonly onDidCloseTextDocument: Event; + readonly onDidChangeTextDocument: Event; + readonly configurations?: Configurations; + readonly onWillSaveTextDocument?: Event; + readonly onDidSaveTextDocument?: Event; + applyEdit(changes: WorkspaceEdit): PromiseLike; + createFileSystemWatcher?( + globPattern: string, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, + ): FileSystemWatcher; +} + +export interface Commands { + registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable; +} + +export interface OutputChannel extends Disposable { + append(value: string): void; + appendLine(line: string): void; + show(preserveFocus?: boolean): void; +} + +export interface Window { + showMessage( + type: MessageType, + message: string, + ...actions: T[] + ): PromiseLike; + createOutputChannel?(name: string): OutputChannel; + withProgress?: any; + showTextDocument?(document: Uri, options?: any): PromiseLike; +} + +export interface Env { + openExternal?(document: Uri): PromiseLike; +} diff --git a/packages/frontend/src/lib/editor/index.ts b/packages/frontend/src/lib/editor/index.ts new file mode 100644 index 0000000..917c7d7 --- /dev/null +++ b/packages/frontend/src/lib/editor/index.ts @@ -0,0 +1,48 @@ +import monaco from "monaco-editor"; +import { loader, Monaco } from "@monaco-editor/react"; +import Server from "./server"; +import { FromServer, IntoServer } from "./codec"; +import Client from "./client"; +import Language from "./language"; +import { EditorService } from "./services"; +import debounce from "debounce"; +import { protocolToMonaco } from "./utils"; +import { store } from "@/state"; +import { defaultCode } from "@/state/initstate"; +import initState from "@/state/inistate"; + +const intoServer = new IntoServer(); +const fromServer = FromServer.create(); +const client = new Client(fromServer, intoServer); +const editorService = new EditorService(client); + +let language: Language; + +export async function init(monaco: Monaco) { + store.send({ type: "setMonaco", monaco }); + initState(); + const server = await Server.initialize(intoServer, fromServer); + language = Language.initialize(client, monaco); + + return await Promise.all([server.start(), client.start()]); +} + +export async function mountService(editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) { + const model = editor.getModel()!; + + client.pushAfterInitializeHook(async () => { + editorService.fileOpened(model); + }); + + editor.onDidChangeModelContent( + debounce(() => { + editorService.fileChanged(model); + + setTimeout(() => { + const diagnostic = client.diagnostic; + const markers = protocolToMonaco.asDiagnostics(diagnostic.diagnostics); + monaco.editor.setModelMarkers(model, "solidity", markers); + }, 500); + }, 200), + ); +} diff --git a/packages/frontend/src/lib/editor/language/contributes.json b/packages/frontend/src/lib/editor/language/contributes.json new file mode 100644 index 0000000..69162cb --- /dev/null +++ b/packages/frontend/src/lib/editor/language/contributes.json @@ -0,0 +1,68 @@ +{ + "contributes": { + "configuration": { + "type": "object", + "title:": "Solang Solidity Compiler", + "properties": { + "solang.target": { + "scope": "window", + "type": "string", + "enum": [ + "solana", + "polkadot", + "evm" + ], + "default": "solana", + "description": "Chain to build for. The Solidity language changes in subtle ways depending on the target." + }, + "solang.updates.askBeforeDownload": { + "type": "boolean", + "default": false, + "description": "Whether to ask for permission before downloading any files from the Internet" + }, + "solidity.trace.server": { + "scope": "window", + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "verbose" + } + } + }, + "capabilities": { + "hoverProvider": "true", + "formatting": { + "dynamicRegistration": true + } + }, + "languages": [ + { + "id": "solidity", + "aliases": [ + "Solidity", + "solidity" + ], + "extensions": [ + ".sol" + ], + "configuration": "./language_configuration/solidity.configuration.json" + } + ], + "snippets": [ + { + "language": "solidity", + "path": "./snippets_solidity.json" + } + ], + "grammars": [ + { + "language": "solidity", + "scopeName": "source.solidity", + "path": "./syntaxes_solidity.json" + } + ] + } +} \ No newline at end of file diff --git a/packages/frontend/src/lib/editor/language/index.ts b/packages/frontend/src/lib/editor/language/index.ts new file mode 100644 index 0000000..ecc38fe --- /dev/null +++ b/packages/frontend/src/lib/editor/language/index.ts @@ -0,0 +1,268 @@ +import * as monaco from "monaco-editor"; +// import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from ""; +import contributes from "./contributes.json"; +import { solidityTokensProvider, solidityLanguageConfig } from "./solidity_syntax"; +import * as proto from "vscode-languageserver-protocol"; +import { Monaco } from "@monaco-editor/react"; +import Client from "../client"; +import { monacoToProtocol, protocolToMonaco } from "../utils"; + +const themeType = "vs-dark"; +const themeName = "remix-dark"; +const formatColor = (name: string): string => { + let color = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim(); + if (color.length === 4) { + color = color.concat(color.substr(1)); + } + console.log("color ", color); + return color; +}; +// see https://microsoft.github.io/monaco-editor/playground.html#customizing-the-appearence-exposed-colors +const lightColor = formatColor("--light"); +const infoColor = formatColor("--info"); +const darkColor = formatColor("--dark"); +const secondaryColor = formatColor("--secondary"); +const primaryColor = formatColor("--primary"); +const textColor = formatColor("--text") || darkColor; +const textbackground = formatColor("--text-background") || lightColor; + +const blueColor = formatColor("--blue"); +const successColor = formatColor("--success"); +const warningColor = formatColor("--warning"); +const yellowColor = formatColor("--yellow"); +const pinkColor = formatColor("--pink"); +const locationColor = "#9e7e08"; +// const purpleColor = formatColor('--purple') +const dangerColor = formatColor("--danger"); +const greenColor = formatColor("--green"); +const orangeColor = formatColor("--orange"); +const grayColor = formatColor("--gray"); + +// export const protocolToMonaco = new ProtocolToMonacoConverter(monaco); +// const monacoToProtocol = new MonacoToProtocolConverter(monaco); + +let language: null | Language; + +export default class Language implements monaco.languages.ILanguageExtensionPoint { + readonly id: string; + readonly aliases: string[]; + readonly extensions: string[]; + readonly mimetypes: string[]; + + private constructor(client: Client, monaco: Monaco) { + const { id, aliases, extensions, mimetypes } = Language.extensionPoint(); + this.id = id; + this.aliases = aliases; + this.extensions = extensions; + this.mimetypes = mimetypes; + this.registerLanguage(client, monaco); + } + + static extensionPoint(): monaco.languages.ILanguageExtensionPoint & { + aliases: string[]; + extensions: string[]; + mimetypes: string[]; + } { + const id = contributes.contributes.languages[0].id; + const aliases = contributes.contributes.languages[0].aliases; + const extensions = contributes.contributes.languages[0].extensions; + const mimetypes = ["text/x-solidity"]; // This is a common MIME type for Solidity, but you may need to adjust it + + return { id, extensions, aliases, mimetypes }; + } + + private registerLanguage(client: Client, monaco: Monaco): void { + void client; + monaco.languages.register({ id: "solidity" }); + + monaco.languages.setMonarchTokensProvider("solidity", solidityTokensProvider as any); + monaco.languages.setLanguageConfiguration("solidity", solidityLanguageConfig as any); + + monaco.languages.registerCompletionItemProvider(this.id, { + async provideCompletionItems(model, position, context, token): Promise { + void token; + const response = await (client.request(proto.CompletionRequest.type.method, { + textDocument: monacoToProtocol.asTextDocumentIdentifier(model), + position: monacoToProtocol.asPosition(position.column, position.lineNumber), + context: monacoToProtocol.asCompletionContext(context), + } as proto.CompletionParams) as Promise); + console.log(response); + + const word = model.getWordUntilPosition(position); + const result: monaco.languages.CompletionList = protocolToMonaco.asCompletionResult(response, { + startLineNumber: position.lineNumber, + startColumn: word.startColumn, + endLineNumber: position.lineNumber, + endColumn: word.endColumn, + }); + + return result; + }, + }); + + monaco.editor.defineTheme(themeName, { + base: themeType, + inherit: true, // can also be false to completely replace the builtin rules + rules: [ + // global variables + { token: "keyword.abi", foreground: blueColor }, + { token: "keyword.block", foreground: blueColor }, + { token: "keyword.bytes", foreground: blueColor }, + { token: "keyword.msg", foreground: blueColor }, + { token: "keyword.tx", foreground: blueColor }, + + // global functions + { token: "keyword.assert", foreground: blueColor }, + { token: "keyword.require", foreground: blueColor }, + { token: "keyword.revert", foreground: blueColor }, + { token: "keyword.blockhash", foreground: blueColor }, + { token: "keyword.keccak256", foreground: blueColor }, + { token: "keyword.sha256", foreground: blueColor }, + { token: "keyword.ripemd160", foreground: blueColor }, + { token: "keyword.ecrecover", foreground: blueColor }, + { token: "keyword.addmod", foreground: blueColor }, + { token: "keyword.mulmod", foreground: blueColor }, + { token: "keyword.selfdestruct", foreground: blueColor }, + { token: "keyword.type ", foreground: blueColor }, + { token: "keyword.gasleft", foreground: blueColor }, + + // specials + { token: "keyword.super", foreground: infoColor }, + { token: "keyword.this", foreground: infoColor }, + { token: "keyword.virtual", foreground: infoColor }, + + // for state variables + { token: "keyword.constants", foreground: grayColor }, + { token: "keyword.override", foreground: grayColor }, + { token: "keyword.immutable", foreground: grayColor }, + + // data location + { token: "keyword.memory", foreground: locationColor }, + { token: "keyword.storage", foreground: locationColor }, + { token: "keyword.calldata", foreground: locationColor }, + + // for Events + { token: "keyword.indexed", foreground: yellowColor }, + { token: "keyword.anonymous", foreground: yellowColor }, + + // for functions + { token: "keyword.external", foreground: successColor }, + { token: "keyword.internal", foreground: successColor }, + { token: "keyword.private", foreground: successColor }, + { token: "keyword.public", foreground: successColor }, + { token: "keyword.view", foreground: successColor }, + { token: "keyword.pure", foreground: successColor }, + { token: "keyword.payable", foreground: successColor }, + { token: "keyword.nonpayable", foreground: successColor }, + + // Errors + { token: "keyword.Error", foreground: dangerColor }, + { token: "keyword.Panic", foreground: dangerColor }, + + // special functions + { token: "keyword.fallback", foreground: pinkColor }, + { token: "keyword.receive", foreground: pinkColor }, + { token: "keyword.constructor", foreground: pinkColor }, + + // identifiers + { token: "keyword.identifier", foreground: warningColor }, + { token: "keyword.for", foreground: warningColor }, + { token: "keyword.break", foreground: warningColor }, + { token: "keyword.continue", foreground: warningColor }, + { token: "keyword.while", foreground: warningColor }, + { token: "keyword.do", foreground: warningColor }, + { token: "keyword.delete", foreground: warningColor }, + + { token: "keyword.if", foreground: yellowColor }, + { token: "keyword.else", foreground: yellowColor }, + + { token: "keyword.throw", foreground: orangeColor }, + { token: "keyword.catch", foreground: orangeColor }, + { token: "keyword.try", foreground: orangeColor }, + + // returns + { token: "keyword.returns", foreground: greenColor }, + { token: "keyword.return", foreground: greenColor }, + ], + colors: {}, + }); + monaco.editor.setTheme(themeName); + + monaco.languages.registerDocumentSymbolProvider(this.id, { + // eslint-disable-next-line + async provideDocumentSymbols(model, token): Promise { + void token; + const response = await (client.request(proto.DocumentSymbolRequest.type.method, { + textDocument: monacoToProtocol.asTextDocumentIdentifier(model), + } as proto.DocumentSymbolParams) as Promise); + + console.log({ response }); + + const uri = model.uri; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result: monaco.languages.DocumentSymbol[] = protocolToMonaco.asSymbolInformations(response, uri); + + return result; + }, + }); + + monaco.languages.registerHoverProvider(this.id, { + // eslint-disable-next-line + async provideHover(model, position, token): Promise { + void token; + const response = await (client.request(proto.HoverRequest.type.method, { + textDocument: monacoToProtocol.asTextDocumentIdentifier(model), + position: monacoToProtocol.asPosition(position.column, position.lineNumber), + } as proto.HoverParams) as Promise); + console.log(response); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result: monaco.languages.Hover = protocolToMonaco.asHover(response); + + console.log("Hover result: ", result); + + // add handler if hover result is null + let message = ""; + if (result == null) { + message = ""; + } else { + message = result.contents[0].value; + } + + // Create a decoration with the hover result + const decoration: monaco.editor.IModelDeltaDecoration = { + range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column), + options: { + hoverMessage: { value: message }, + }, + }; + + // Apply the decoration to the editor + model.deltaDecorations([], [decoration]); + + return result; + }, + }); + + monaco.editor.onDidCreateModel((model) => { + setTimeout(() => { + console.log("content changed", event); + const diagnostic = client.diagnostic; + + const markers = protocolToMonaco.asDiagnostics(diagnostic.diagnostics); + + monaco.editor.setModelMarkers(model, "solidity", markers); + }, 500); + }); + } + + static initialize(client: Client, monaco: Monaco): Language { + if (null == language) { + language = new Language(client, monaco); + } else { + console.warn("Language already initialized; ignoring"); + } + return language; + } +} diff --git a/packages/frontend/src/lib/editor/language/snippets_solidity.json b/packages/frontend/src/lib/editor/language/snippets_solidity.json new file mode 100644 index 0000000..548a669 --- /dev/null +++ b/packages/frontend/src/lib/editor/language/snippets_solidity.json @@ -0,0 +1,65 @@ +{ + ".source.solidity": { + "pragma solidity":{ + "prefix": "pra", + "body": "pragma solidity ${1:version};" + }, + "import contract": { + "prefix": "im", + "body": "import '${1:contract}';" + }, + "contract declaration": { + "prefix": "con", + "body": "contract ${1:Name} {\n\t$0\n}" + }, + "library declaration": { + "prefix": "lib", + "body": "library ${1:Name} {\n\t$0\n}" + }, + "interface declaration": { + "prefix": "interf", + "body": "interface ${1:Name} {\n\t$0\n}" + }, + "enum declaration": { + "prefix": "enum", + "body": "enum ${1:Name} {${2:item1}, ${3:item2} }" + }, + "mapping declaration":{ + "prefix": "map", + "body": "mapping (${1:type1}=>${2:type2}) ${3:name};" + }, + "constructor declaration": { + "prefix": "const", + "body": "constructor (${1:type} ${2:name}) public {\n\t$0\n}" + }, + "function declaration": { + "prefix": "func", + "body": "function ${1:name}(${2:type} ${3:name}) {\n\t$0\n}" + }, + "function return declaration": { + "prefix": "funcr", + "body": "function ${1:name}(${2:type} ${3:name}) returns (${4:type} ${5:name}) {\n\t$0\n}" + } + , + "function view declaration": { + "prefix": "funcrview", + "body": "function ${1:name}(${2:type} ${3:name}) view public returns (${4:type} ${5:name}) {\n\t$0\n}" + }, + "event declaration": { + "prefix": "ev", + "body": "event ${1:name}(${2:type} ${3:name} $0);" + }, + "modifier declaration": { + "prefix": "mod", + "body": "modifier ${1:name}($2) {\n\t$0_\n}" + }, + "if else statement": { + "prefix": "ife", + "body": "if (${1:condition}) {\n\t$2\n} else {\n\t$0\n}" + }, + "for statement": { + "prefix": "for", + "body": "for (var ${1:index} = 0; $1 < ${2:array}.length; $1${3:++}) {\n\t$0\n}" + } + } +} \ No newline at end of file diff --git a/packages/frontend/src/lib/editor/language/solidity.configuration.json b/packages/frontend/src/lib/editor/language/solidity.configuration.json new file mode 100644 index 0000000..b336454 --- /dev/null +++ b/packages/frontend/src/lib/editor/language/solidity.configuration.json @@ -0,0 +1,23 @@ +{ + "comments": { + "lineComment": "//", + + "blockComment": [ "/*", "*/" ] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "/**", "close": " */", "notIn": ["string"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ] +} \ No newline at end of file diff --git a/packages/frontend/src/lib/editor/language/solidity_syntax.ts b/packages/frontend/src/lib/editor/language/solidity_syntax.ts new file mode 100644 index 0000000..d358833 --- /dev/null +++ b/packages/frontend/src/lib/editor/language/solidity_syntax.ts @@ -0,0 +1,1422 @@ +/* eslint-disable */ +export const solidityLanguageConfig = { + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ['<', '>'] + ], + autoClosingPairs: [ + { open: '"', close: '"', notIn: ['string', 'comment'] }, + { open: "'", close: "'", notIn: ['string', 'comment'] }, + { open: '{', close: '}', notIn: ['string', 'comment'] }, + { open: '[', close: ']', notIn: ['string', 'comment'] }, + { open: '(', close: ')', notIn: ['string', 'comment'] } + ] +} + +export const solidityTokensProvider = { + defaultToken: '', + tokenPostfix: '.sol', + + brackets: [ + { token: 'delimiter.curly', open: '{', close: '}' }, + { token: 'delimiter.parenthesis', open: '(', close: ')' }, + { token: 'delimiter.square', open: '[', close: ']' }, + { token: 'delimiter.angle', open: '<', close: '>' } + ], + + keywords: [ + // Main keywords + 'pragma', + 'solidity', + 'contract', + 'library', + 'using', + 'struct', + 'function', + 'modifier', + 'constructor', + // Built-in types + 'address', + 'string', + 'bool', + // Other types + 'Int', + 'Uint', + 'Byte', + 'Fixed', + 'Ufixed', + // All int + 'int', + 'int8', + 'int16', + 'int24', + 'int32', + 'int40', + 'int48', + 'int56', + 'int64', + 'int72', + 'int80', + 'int88', + 'int96', + 'int104', + 'int112', + 'int120', + 'int128', + 'int136', + 'int144', + 'int152', + 'int160', + 'int168', + 'int176', + 'int184', + 'int192', + 'int200', + 'int208', + 'int216', + 'int224', + 'int232', + 'int240', + 'int248', + 'int256', + // All uint + 'uint', + 'uint8', + 'uint16', + 'uint24', + 'uint32', + 'uint40', + 'uint48', + 'uint56', + 'uint64', + 'uint72', + 'uint80', + 'uint88', + 'uint96', + 'uint104', + 'uint112', + 'uint120', + 'uint128', + 'uint136', + 'uint144', + 'uint152', + 'uint160', + 'uint168', + 'uint176', + 'uint184', + 'uint192', + 'uint200', + 'uint208', + 'uint216', + 'uint224', + 'uint232', + 'uint240', + 'uint248', + 'uint256', + // All Byte + 'byte', + 'bytes', + 'bytes1', + 'bytes2', + 'bytes3', + 'bytes4', + 'bytes5', + 'bytes6', + 'bytes7', + 'bytes8', + 'bytes9', + 'bytes10', + 'bytes11', + 'bytes12', + 'bytes13', + 'bytes14', + 'bytes15', + 'bytes16', + 'bytes17', + 'bytes18', + 'bytes19', + 'bytes20', + 'bytes21', + 'bytes22', + 'bytes23', + 'bytes24', + 'bytes25', + 'bytes26', + 'bytes27', + 'bytes28', + 'bytes29', + 'bytes30', + 'bytes31', + 'bytes32', + // All fixed + 'fixed', + 'fixed0x8', + 'fixed0x16', + 'fixed0x24', + 'fixed0x32', + 'fixed0x40', + 'fixed0x48', + 'fixed0x56', + 'fixed0x64', + 'fixed0x72', + 'fixed0x80', + 'fixed0x88', + 'fixed0x96', + 'fixed0x104', + 'fixed0x112', + 'fixed0x120', + 'fixed0x128', + 'fixed0x136', + 'fixed0x144', + 'fixed0x152', + 'fixed0x160', + 'fixed0x168', + 'fixed0x176', + 'fixed0x184', + 'fixed0x192', + 'fixed0x200', + 'fixed0x208', + 'fixed0x216', + 'fixed0x224', + 'fixed0x232', + 'fixed0x240', + 'fixed0x248', + 'fixed0x256', + 'fixed8x8', + 'fixed8x16', + 'fixed8x24', + 'fixed8x32', + 'fixed8x40', + 'fixed8x48', + 'fixed8x56', + 'fixed8x64', + 'fixed8x72', + 'fixed8x80', + 'fixed8x88', + 'fixed8x96', + 'fixed8x104', + 'fixed8x112', + 'fixed8x120', + 'fixed8x128', + 'fixed8x136', + 'fixed8x144', + 'fixed8x152', + 'fixed8x160', + 'fixed8x168', + 'fixed8x176', + 'fixed8x184', + 'fixed8x192', + 'fixed8x200', + 'fixed8x208', + 'fixed8x216', + 'fixed8x224', + 'fixed8x232', + 'fixed8x240', + 'fixed8x248', + 'fixed16x8', + 'fixed16x16', + 'fixed16x24', + 'fixed16x32', + 'fixed16x40', + 'fixed16x48', + 'fixed16x56', + 'fixed16x64', + 'fixed16x72', + 'fixed16x80', + 'fixed16x88', + 'fixed16x96', + 'fixed16x104', + 'fixed16x112', + 'fixed16x120', + 'fixed16x128', + 'fixed16x136', + 'fixed16x144', + 'fixed16x152', + 'fixed16x160', + 'fixed16x168', + 'fixed16x176', + 'fixed16x184', + 'fixed16x192', + 'fixed16x200', + 'fixed16x208', + 'fixed16x216', + 'fixed16x224', + 'fixed16x232', + 'fixed16x240', + 'fixed24x8', + 'fixed24x16', + 'fixed24x24', + 'fixed24x32', + 'fixed24x40', + 'fixed24x48', + 'fixed24x56', + 'fixed24x64', + 'fixed24x72', + 'fixed24x80', + 'fixed24x88', + 'fixed24x96', + 'fixed24x104', + 'fixed24x112', + 'fixed24x120', + 'fixed24x128', + 'fixed24x136', + 'fixed24x144', + 'fixed24x152', + 'fixed24x160', + 'fixed24x168', + 'fixed24x176', + 'fixed24x184', + 'fixed24x192', + 'fixed24x200', + 'fixed24x208', + 'fixed24x216', + 'fixed24x224', + 'fixed24x232', + 'fixed32x8', + 'fixed32x16', + 'fixed32x24', + 'fixed32x32', + 'fixed32x40', + 'fixed32x48', + 'fixed32x56', + 'fixed32x64', + 'fixed32x72', + 'fixed32x80', + 'fixed32x88', + 'fixed32x96', + 'fixed32x104', + 'fixed32x112', + 'fixed32x120', + 'fixed32x128', + 'fixed32x136', + 'fixed32x144', + 'fixed32x152', + 'fixed32x160', + 'fixed32x168', + 'fixed32x176', + 'fixed32x184', + 'fixed32x192', + 'fixed32x200', + 'fixed32x208', + 'fixed32x216', + 'fixed32x224', + 'fixed40x8', + 'fixed40x16', + 'fixed40x24', + 'fixed40x32', + 'fixed40x40', + 'fixed40x48', + 'fixed40x56', + 'fixed40x64', + 'fixed40x72', + 'fixed40x80', + 'fixed40x88', + 'fixed40x96', + 'fixed40x104', + 'fixed40x112', + 'fixed40x120', + 'fixed40x128', + 'fixed40x136', + 'fixed40x144', + 'fixed40x152', + 'fixed40x160', + 'fixed40x168', + 'fixed40x176', + 'fixed40x184', + 'fixed40x192', + 'fixed40x200', + 'fixed40x208', + 'fixed40x216', + 'fixed48x8', + 'fixed48x16', + 'fixed48x24', + 'fixed48x32', + 'fixed48x40', + 'fixed48x48', + 'fixed48x56', + 'fixed48x64', + 'fixed48x72', + 'fixed48x80', + 'fixed48x88', + 'fixed48x96', + 'fixed48x104', + 'fixed48x112', + 'fixed48x120', + 'fixed48x128', + 'fixed48x136', + 'fixed48x144', + 'fixed48x152', + 'fixed48x160', + 'fixed48x168', + 'fixed48x176', + 'fixed48x184', + 'fixed48x192', + 'fixed48x200', + 'fixed48x208', + 'fixed56x8', + 'fixed56x16', + 'fixed56x24', + 'fixed56x32', + 'fixed56x40', + 'fixed56x48', + 'fixed56x56', + 'fixed56x64', + 'fixed56x72', + 'fixed56x80', + 'fixed56x88', + 'fixed56x96', + 'fixed56x104', + 'fixed56x112', + 'fixed56x120', + 'fixed56x128', + 'fixed56x136', + 'fixed56x144', + 'fixed56x152', + 'fixed56x160', + 'fixed56x168', + 'fixed56x176', + 'fixed56x184', + 'fixed56x192', + 'fixed56x200', + 'fixed64x8', + 'fixed64x16', + 'fixed64x24', + 'fixed64x32', + 'fixed64x40', + 'fixed64x48', + 'fixed64x56', + 'fixed64x64', + 'fixed64x72', + 'fixed64x80', + 'fixed64x88', + 'fixed64x96', + 'fixed64x104', + 'fixed64x112', + 'fixed64x120', + 'fixed64x128', + 'fixed64x136', + 'fixed64x144', + 'fixed64x152', + 'fixed64x160', + 'fixed64x168', + 'fixed64x176', + 'fixed64x184', + 'fixed64x192', + 'fixed72x8', + 'fixed72x16', + 'fixed72x24', + 'fixed72x32', + 'fixed72x40', + 'fixed72x48', + 'fixed72x56', + 'fixed72x64', + 'fixed72x72', + 'fixed72x80', + 'fixed72x88', + 'fixed72x96', + 'fixed72x104', + 'fixed72x112', + 'fixed72x120', + 'fixed72x128', + 'fixed72x136', + 'fixed72x144', + 'fixed72x152', + 'fixed72x160', + 'fixed72x168', + 'fixed72x176', + 'fixed72x184', + 'fixed80x8', + 'fixed80x16', + 'fixed80x24', + 'fixed80x32', + 'fixed80x40', + 'fixed80x48', + 'fixed80x56', + 'fixed80x64', + 'fixed80x72', + 'fixed80x80', + 'fixed80x88', + 'fixed80x96', + 'fixed80x104', + 'fixed80x112', + 'fixed80x120', + 'fixed80x128', + 'fixed80x136', + 'fixed80x144', + 'fixed80x152', + 'fixed80x160', + 'fixed80x168', + 'fixed80x176', + 'fixed88x8', + 'fixed88x16', + 'fixed88x24', + 'fixed88x32', + 'fixed88x40', + 'fixed88x48', + 'fixed88x56', + 'fixed88x64', + 'fixed88x72', + 'fixed88x80', + 'fixed88x88', + 'fixed88x96', + 'fixed88x104', + 'fixed88x112', + 'fixed88x120', + 'fixed88x128', + 'fixed88x136', + 'fixed88x144', + 'fixed88x152', + 'fixed88x160', + 'fixed88x168', + 'fixed96x8', + 'fixed96x16', + 'fixed96x24', + 'fixed96x32', + 'fixed96x40', + 'fixed96x48', + 'fixed96x56', + 'fixed96x64', + 'fixed96x72', + 'fixed96x80', + 'fixed96x88', + 'fixed96x96', + 'fixed96x104', + 'fixed96x112', + 'fixed96x120', + 'fixed96x128', + 'fixed96x136', + 'fixed96x144', + 'fixed96x152', + 'fixed96x160', + 'fixed104x8', + 'fixed104x16', + 'fixed104x24', + 'fixed104x32', + 'fixed104x40', + 'fixed104x48', + 'fixed104x56', + 'fixed104x64', + 'fixed104x72', + 'fixed104x80', + 'fixed104x88', + 'fixed104x96', + 'fixed104x104', + 'fixed104x112', + 'fixed104x120', + 'fixed104x128', + 'fixed104x136', + 'fixed104x144', + 'fixed104x152', + 'fixed112x8', + 'fixed112x16', + 'fixed112x24', + 'fixed112x32', + 'fixed112x40', + 'fixed112x48', + 'fixed112x56', + 'fixed112x64', + 'fixed112x72', + 'fixed112x80', + 'fixed112x88', + 'fixed112x96', + 'fixed112x104', + 'fixed112x112', + 'fixed112x120', + 'fixed112x128', + 'fixed112x136', + 'fixed112x144', + 'fixed120x8', + 'fixed120x16', + 'fixed120x24', + 'fixed120x32', + 'fixed120x40', + 'fixed120x48', + 'fixed120x56', + 'fixed120x64', + 'fixed120x72', + 'fixed120x80', + 'fixed120x88', + 'fixed120x96', + 'fixed120x104', + 'fixed120x112', + 'fixed120x120', + 'fixed120x128', + 'fixed120x136', + 'fixed128x8', + 'fixed128x16', + 'fixed128x24', + 'fixed128x32', + 'fixed128x40', + 'fixed128x48', + 'fixed128x56', + 'fixed128x64', + 'fixed128x72', + 'fixed128x80', + 'fixed128x88', + 'fixed128x96', + 'fixed128x104', + 'fixed128x112', + 'fixed128x120', + 'fixed128x128', + 'fixed136x8', + 'fixed136x16', + 'fixed136x24', + 'fixed136x32', + 'fixed136x40', + 'fixed136x48', + 'fixed136x56', + 'fixed136x64', + 'fixed136x72', + 'fixed136x80', + 'fixed136x88', + 'fixed136x96', + 'fixed136x104', + 'fixed136x112', + 'fixed136x120', + 'fixed144x8', + 'fixed144x16', + 'fixed144x24', + 'fixed144x32', + 'fixed144x40', + 'fixed144x48', + 'fixed144x56', + 'fixed144x64', + 'fixed144x72', + 'fixed144x80', + 'fixed144x88', + 'fixed144x96', + 'fixed144x104', + 'fixed144x112', + 'fixed152x8', + 'fixed152x16', + 'fixed152x24', + 'fixed152x32', + 'fixed152x40', + 'fixed152x48', + 'fixed152x56', + 'fixed152x64', + 'fixed152x72', + 'fixed152x80', + 'fixed152x88', + 'fixed152x96', + 'fixed152x104', + 'fixed160x8', + 'fixed160x16', + 'fixed160x24', + 'fixed160x32', + 'fixed160x40', + 'fixed160x48', + 'fixed160x56', + 'fixed160x64', + 'fixed160x72', + 'fixed160x80', + 'fixed160x88', + 'fixed160x96', + 'fixed168x8', + 'fixed168x16', + 'fixed168x24', + 'fixed168x32', + 'fixed168x40', + 'fixed168x48', + 'fixed168x56', + 'fixed168x64', + 'fixed168x72', + 'fixed168x80', + 'fixed168x88', + 'fixed176x8', + 'fixed176x16', + 'fixed176x24', + 'fixed176x32', + 'fixed176x40', + 'fixed176x48', + 'fixed176x56', + 'fixed176x64', + 'fixed176x72', + 'fixed176x80', + 'fixed184x8', + 'fixed184x16', + 'fixed184x24', + 'fixed184x32', + 'fixed184x40', + 'fixed184x48', + 'fixed184x56', + 'fixed184x64', + 'fixed184x72', + 'fixed192x8', + 'fixed192x16', + 'fixed192x24', + 'fixed192x32', + 'fixed192x40', + 'fixed192x48', + 'fixed192x56', + 'fixed192x64', + 'fixed200x8', + 'fixed200x16', + 'fixed200x24', + 'fixed200x32', + 'fixed200x40', + 'fixed200x48', + 'fixed200x56', + 'fixed208x8', + 'fixed208x16', + 'fixed208x24', + 'fixed208x32', + 'fixed208x40', + 'fixed208x48', + 'fixed216x8', + 'fixed216x16', + 'fixed216x24', + 'fixed216x32', + 'fixed216x40', + 'fixed224x8', + 'fixed224x16', + 'fixed224x24', + 'fixed224x32', + 'fixed232x8', + 'fixed232x16', + 'fixed232x24', + 'fixed240x8', + 'fixed240x16', + 'fixed248x8', + // All ufixed + 'ufixed', + 'ufixed0x8', + 'ufixed0x16', + 'ufixed0x24', + 'ufixed0x32', + 'ufixed0x40', + 'ufixed0x48', + 'ufixed0x56', + 'ufixed0x64', + 'ufixed0x72', + 'ufixed0x80', + 'ufixed0x88', + 'ufixed0x96', + 'ufixed0x104', + 'ufixed0x112', + 'ufixed0x120', + 'ufixed0x128', + 'ufixed0x136', + 'ufixed0x144', + 'ufixed0x152', + 'ufixed0x160', + 'ufixed0x168', + 'ufixed0x176', + 'ufixed0x184', + 'ufixed0x192', + 'ufixed0x200', + 'ufixed0x208', + 'ufixed0x216', + 'ufixed0x224', + 'ufixed0x232', + 'ufixed0x240', + 'ufixed0x248', + 'ufixed0x256', + 'ufixed8x8', + 'ufixed8x16', + 'ufixed8x24', + 'ufixed8x32', + 'ufixed8x40', + 'ufixed8x48', + 'ufixed8x56', + 'ufixed8x64', + 'ufixed8x72', + 'ufixed8x80', + 'ufixed8x88', + 'ufixed8x96', + 'ufixed8x104', + 'ufixed8x112', + 'ufixed8x120', + 'ufixed8x128', + 'ufixed8x136', + 'ufixed8x144', + 'ufixed8x152', + 'ufixed8x160', + 'ufixed8x168', + 'ufixed8x176', + 'ufixed8x184', + 'ufixed8x192', + 'ufixed8x200', + 'ufixed8x208', + 'ufixed8x216', + 'ufixed8x224', + 'ufixed8x232', + 'ufixed8x240', + 'ufixed8x248', + 'ufixed16x8', + 'ufixed16x16', + 'ufixed16x24', + 'ufixed16x32', + 'ufixed16x40', + 'ufixed16x48', + 'ufixed16x56', + 'ufixed16x64', + 'ufixed16x72', + 'ufixed16x80', + 'ufixed16x88', + 'ufixed16x96', + 'ufixed16x104', + 'ufixed16x112', + 'ufixed16x120', + 'ufixed16x128', + 'ufixed16x136', + 'ufixed16x144', + 'ufixed16x152', + 'ufixed16x160', + 'ufixed16x168', + 'ufixed16x176', + 'ufixed16x184', + 'ufixed16x192', + 'ufixed16x200', + 'ufixed16x208', + 'ufixed16x216', + 'ufixed16x224', + 'ufixed16x232', + 'ufixed16x240', + 'ufixed24x8', + 'ufixed24x16', + 'ufixed24x24', + 'ufixed24x32', + 'ufixed24x40', + 'ufixed24x48', + 'ufixed24x56', + 'ufixed24x64', + 'ufixed24x72', + 'ufixed24x80', + 'ufixed24x88', + 'ufixed24x96', + 'ufixed24x104', + 'ufixed24x112', + 'ufixed24x120', + 'ufixed24x128', + 'ufixed24x136', + 'ufixed24x144', + 'ufixed24x152', + 'ufixed24x160', + 'ufixed24x168', + 'ufixed24x176', + 'ufixed24x184', + 'ufixed24x192', + 'ufixed24x200', + 'ufixed24x208', + 'ufixed24x216', + 'ufixed24x224', + 'ufixed24x232', + 'ufixed32x8', + 'ufixed32x16', + 'ufixed32x24', + 'ufixed32x32', + 'ufixed32x40', + 'ufixed32x48', + 'ufixed32x56', + 'ufixed32x64', + 'ufixed32x72', + 'ufixed32x80', + 'ufixed32x88', + 'ufixed32x96', + 'ufixed32x104', + 'ufixed32x112', + 'ufixed32x120', + 'ufixed32x128', + 'ufixed32x136', + 'ufixed32x144', + 'ufixed32x152', + 'ufixed32x160', + 'ufixed32x168', + 'ufixed32x176', + 'ufixed32x184', + 'ufixed32x192', + 'ufixed32x200', + 'ufixed32x208', + 'ufixed32x216', + 'ufixed32x224', + 'ufixed40x8', + 'ufixed40x16', + 'ufixed40x24', + 'ufixed40x32', + 'ufixed40x40', + 'ufixed40x48', + 'ufixed40x56', + 'ufixed40x64', + 'ufixed40x72', + 'ufixed40x80', + 'ufixed40x88', + 'ufixed40x96', + 'ufixed40x104', + 'ufixed40x112', + 'ufixed40x120', + 'ufixed40x128', + 'ufixed40x136', + 'ufixed40x144', + 'ufixed40x152', + 'ufixed40x160', + 'ufixed40x168', + 'ufixed40x176', + 'ufixed40x184', + 'ufixed40x192', + 'ufixed40x200', + 'ufixed40x208', + 'ufixed40x216', + 'ufixed48x8', + 'ufixed48x16', + 'ufixed48x24', + 'ufixed48x32', + 'ufixed48x40', + 'ufixed48x48', + 'ufixed48x56', + 'ufixed48x64', + 'ufixed48x72', + 'ufixed48x80', + 'ufixed48x88', + 'ufixed48x96', + 'ufixed48x104', + 'ufixed48x112', + 'ufixed48x120', + 'ufixed48x128', + 'ufixed48x136', + 'ufixed48x144', + 'ufixed48x152', + 'ufixed48x160', + 'ufixed48x168', + 'ufixed48x176', + 'ufixed48x184', + 'ufixed48x192', + 'ufixed48x200', + 'ufixed48x208', + 'ufixed56x8', + 'ufixed56x16', + 'ufixed56x24', + 'ufixed56x32', + 'ufixed56x40', + 'ufixed56x48', + 'ufixed56x56', + 'ufixed56x64', + 'ufixed56x72', + 'ufixed56x80', + 'ufixed56x88', + 'ufixed56x96', + 'ufixed56x104', + 'ufixed56x112', + 'ufixed56x120', + 'ufixed56x128', + 'ufixed56x136', + 'ufixed56x144', + 'ufixed56x152', + 'ufixed56x160', + 'ufixed56x168', + 'ufixed56x176', + 'ufixed56x184', + 'ufixed56x192', + 'ufixed56x200', + 'ufixed64x8', + 'ufixed64x16', + 'ufixed64x24', + 'ufixed64x32', + 'ufixed64x40', + 'ufixed64x48', + 'ufixed64x56', + 'ufixed64x64', + 'ufixed64x72', + 'ufixed64x80', + 'ufixed64x88', + 'ufixed64x96', + 'ufixed64x104', + 'ufixed64x112', + 'ufixed64x120', + 'ufixed64x128', + 'ufixed64x136', + 'ufixed64x144', + 'ufixed64x152', + 'ufixed64x160', + 'ufixed64x168', + 'ufixed64x176', + 'ufixed64x184', + 'ufixed64x192', + 'ufixed72x8', + 'ufixed72x16', + 'ufixed72x24', + 'ufixed72x32', + 'ufixed72x40', + 'ufixed72x48', + 'ufixed72x56', + 'ufixed72x64', + 'ufixed72x72', + 'ufixed72x80', + 'ufixed72x88', + 'ufixed72x96', + 'ufixed72x104', + 'ufixed72x112', + 'ufixed72x120', + 'ufixed72x128', + 'ufixed72x136', + 'ufixed72x144', + 'ufixed72x152', + 'ufixed72x160', + 'ufixed72x168', + 'ufixed72x176', + 'ufixed72x184', + 'ufixed80x8', + 'ufixed80x16', + 'ufixed80x24', + 'ufixed80x32', + 'ufixed80x40', + 'ufixed80x48', + 'ufixed80x56', + 'ufixed80x64', + 'ufixed80x72', + 'ufixed80x80', + 'ufixed80x88', + 'ufixed80x96', + 'ufixed80x104', + 'ufixed80x112', + 'ufixed80x120', + 'ufixed80x128', + 'ufixed80x136', + 'ufixed80x144', + 'ufixed80x152', + 'ufixed80x160', + 'ufixed80x168', + 'ufixed80x176', + 'ufixed88x8', + 'ufixed88x16', + 'ufixed88x24', + 'ufixed88x32', + 'ufixed88x40', + 'ufixed88x48', + 'ufixed88x56', + 'ufixed88x64', + 'ufixed88x72', + 'ufixed88x80', + 'ufixed88x88', + 'ufixed88x96', + 'ufixed88x104', + 'ufixed88x112', + 'ufixed88x120', + 'ufixed88x128', + 'ufixed88x136', + 'ufixed88x144', + 'ufixed88x152', + 'ufixed88x160', + 'ufixed88x168', + 'ufixed96x8', + 'ufixed96x16', + 'ufixed96x24', + 'ufixed96x32', + 'ufixed96x40', + 'ufixed96x48', + 'ufixed96x56', + 'ufixed96x64', + 'ufixed96x72', + 'ufixed96x80', + 'ufixed96x88', + 'ufixed96x96', + 'ufixed96x104', + 'ufixed96x112', + 'ufixed96x120', + 'ufixed96x128', + 'ufixed96x136', + 'ufixed96x144', + 'ufixed96x152', + 'ufixed96x160', + 'ufixed104x8', + 'ufixed104x16', + 'ufixed104x24', + 'ufixed104x32', + 'ufixed104x40', + 'ufixed104x48', + 'ufixed104x56', + 'ufixed104x64', + 'ufixed104x72', + 'ufixed104x80', + 'ufixed104x88', + 'ufixed104x96', + 'ufixed104x104', + 'ufixed104x112', + 'ufixed104x120', + 'ufixed104x128', + 'ufixed104x136', + 'ufixed104x144', + 'ufixed104x152', + 'ufixed112x8', + 'ufixed112x16', + 'ufixed112x24', + 'ufixed112x32', + 'ufixed112x40', + 'ufixed112x48', + 'ufixed112x56', + 'ufixed112x64', + 'ufixed112x72', + 'ufixed112x80', + 'ufixed112x88', + 'ufixed112x96', + 'ufixed112x104', + 'ufixed112x112', + 'ufixed112x120', + 'ufixed112x128', + 'ufixed112x136', + 'ufixed112x144', + 'ufixed120x8', + 'ufixed120x16', + 'ufixed120x24', + 'ufixed120x32', + 'ufixed120x40', + 'ufixed120x48', + 'ufixed120x56', + 'ufixed120x64', + 'ufixed120x72', + 'ufixed120x80', + 'ufixed120x88', + 'ufixed120x96', + 'ufixed120x104', + 'ufixed120x112', + 'ufixed120x120', + 'ufixed120x128', + 'ufixed120x136', + 'ufixed128x8', + 'ufixed128x16', + 'ufixed128x24', + 'ufixed128x32', + 'ufixed128x40', + 'ufixed128x48', + 'ufixed128x56', + 'ufixed128x64', + 'ufixed128x72', + 'ufixed128x80', + 'ufixed128x88', + 'ufixed128x96', + 'ufixed128x104', + 'ufixed128x112', + 'ufixed128x120', + 'ufixed128x128', + 'ufixed136x8', + 'ufixed136x16', + 'ufixed136x24', + 'ufixed136x32', + 'ufixed136x40', + 'ufixed136x48', + 'ufixed136x56', + 'ufixed136x64', + 'ufixed136x72', + 'ufixed136x80', + 'ufixed136x88', + 'ufixed136x96', + 'ufixed136x104', + 'ufixed136x112', + 'ufixed136x120', + 'ufixed144x8', + 'ufixed144x16', + 'ufixed144x24', + 'ufixed144x32', + 'ufixed144x40', + 'ufixed144x48', + 'ufixed144x56', + 'ufixed144x64', + 'ufixed144x72', + 'ufixed144x80', + 'ufixed144x88', + 'ufixed144x96', + 'ufixed144x104', + 'ufixed144x112', + 'ufixed152x8', + 'ufixed152x16', + 'ufixed152x24', + 'ufixed152x32', + 'ufixed152x40', + 'ufixed152x48', + 'ufixed152x56', + 'ufixed152x64', + 'ufixed152x72', + 'ufixed152x80', + 'ufixed152x88', + 'ufixed152x96', + 'ufixed152x104', + 'ufixed160x8', + 'ufixed160x16', + 'ufixed160x24', + 'ufixed160x32', + 'ufixed160x40', + 'ufixed160x48', + 'ufixed160x56', + 'ufixed160x64', + 'ufixed160x72', + 'ufixed160x80', + 'ufixed160x88', + 'ufixed160x96', + 'ufixed168x8', + 'ufixed168x16', + 'ufixed168x24', + 'ufixed168x32', + 'ufixed168x40', + 'ufixed168x48', + 'ufixed168x56', + 'ufixed168x64', + 'ufixed168x72', + 'ufixed168x80', + 'ufixed168x88', + 'ufixed176x8', + 'ufixed176x16', + 'ufixed176x24', + 'ufixed176x32', + 'ufixed176x40', + 'ufixed176x48', + 'ufixed176x56', + 'ufixed176x64', + 'ufixed176x72', + 'ufixed176x80', + 'ufixed184x8', + 'ufixed184x16', + 'ufixed184x24', + 'ufixed184x32', + 'ufixed184x40', + 'ufixed184x48', + 'ufixed184x56', + 'ufixed184x64', + 'ufixed184x72', + 'ufixed192x8', + 'ufixed192x16', + 'ufixed192x24', + 'ufixed192x32', + 'ufixed192x40', + 'ufixed192x48', + 'ufixed192x56', + 'ufixed192x64', + 'ufixed200x8', + 'ufixed200x16', + 'ufixed200x24', + 'ufixed200x32', + 'ufixed200x40', + 'ufixed200x48', + 'ufixed200x56', + 'ufixed208x8', + 'ufixed208x16', + 'ufixed208x24', + 'ufixed208x32', + 'ufixed208x40', + 'ufixed208x48', + 'ufixed216x8', + 'ufixed216x16', + 'ufixed216x24', + 'ufixed216x32', + 'ufixed216x40', + 'ufixed224x8', + 'ufixed224x16', + 'ufixed224x24', + 'ufixed224x32', + 'ufixed232x8', + 'ufixed232x16', + 'ufixed232x24', + 'ufixed240x8', + 'ufixed240x16', + 'ufixed248x8', + 'event', + 'emit', + 'enum', + 'let', + 'mapping', + 'private', + 'public', + 'external', + 'internal', + 'indexed', + 'anonymous', + 'view', + 'pure', + 'inherited', + 'storage', + 'memory', + 'virtual', + 'calldata', + 'override', + 'abstract', + 'payable', + 'nonpayable', + 'constants', + 'immutable', + 'assert', + 'require', + 'revert', + 'blockhash', + 'keccak256', + 'sha256', + 'ripemd160', + 'ecrecover', + 'addmod', + 'mulmod', + 'selfdestruct', + 'type', + 'gasleft', + 'abi', + 'block', + 'bytes', + 'msg', + 'tx', + 'Error', + 'Panic', + 'exceptions', + 'true', + 'false', + 'var', + 'import', + 'constant', + 'fallback', + 'receive', + 'delete', + 'if', + 'else', + 'for', + 'while', + 'do', + 'break', + 'continue', + 'throw', + 'returns', + 'return', + 'suicide', + 'new', + 'is', + 'this', + 'super', + 'try', + 'catch' + ], + + operators: [ + '=', + '>', + '<', + '!', + '~', + '?', + ':', + '==', + '<=', + '>=', + '!=', + '&&', + '||', + '++', + '--', + '+', + '-', + '*', + '/', + '&', + '|', + '^', + '%', + '<<', + '>>', + '>>>', + '+=', + '-=', + '*=', + '/=', + '&=', + '|=', + '^=', + '%=', + '<<=', + '>>=', + '>>>=' + ], + + // we include these common regular expressions + symbols: /[=>](?!@symbols)/, '@brackets'], + [ + /@symbols/, + { + cases: { + '@operators': 'delimiter', + '@default': '' + } + } + ], + + // numbers + [/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/, 'number.float'], + [/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/, 'number.float'], + [/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/, 'number.hex'], + [/0[0-7']*[0-7](@integersuffix)/, 'number.octal'], + [/0[bB][0-1']*[0-1](@integersuffix)/, 'number.binary'], + [/\d[\d']*\d(@integersuffix)/, 'number'], + [/\d(@integersuffix)/, 'number'], + + // delimiter: after number because of .\d floats + [/[;,.]/, 'delimiter'], + + // strings + [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string + [/"/, 'string', '@string'], + + // characters + [/'[^\\']'/, 'string'], + [/(')(@escapes)(')/, ['string', 'string.escape', 'string']], + [/'/, 'string.invalid'] + ], + + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*\*(?!\/)/, 'comment.doc', '@doccomment'], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'] + ], + + comment: [ + [/[^\/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[\/*]/, 'comment'] + ], + // Identical copy of comment above, except for the addition of .doc + doccomment: [ + [/[^\/*]+/, 'comment.doc'], + [/\*\//, 'comment.doc', '@pop'], + [/[\/*]/, 'comment.doc'] + ], + + string: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'] + ] + } +} diff --git a/packages/frontend/src/lib/editor/language/syntaxes_solidity.json b/packages/frontend/src/lib/editor/language/syntaxes_solidity.json new file mode 100644 index 0000000..37c25bd --- /dev/null +++ b/packages/frontend/src/lib/editor/language/syntaxes_solidity.json @@ -0,0 +1,540 @@ +{ + "fileTypes": [ + "sol" + ], + "name": "Solidity", + "patterns": [ + { + "include": "#natspec" + }, + { + "include": "#comment" + }, + { + "include": "#operator" + }, + { + "include": "#control" + }, + { + "include": "#constant" + }, + { + "include": "#number" + }, + { + "include": "#string" + }, + { + "include": "#type" + }, + { + "include": "#global" + }, + { + "include": "#declaration" + }, + { + "include": "#function-call" + }, + { + "include": "#assembly" + }, + { + "include": "#punctuation" + } + ], + "repository": { + "natspec": { + "patterns": [ + { + "begin": "/\\*\\*", + "end": "\\*/", + "name": "comment.block.documentation.solidity", + "patterns": [ + { + "include": "#natspec-tags" + } + ] + }, + { + "begin": "///", + "end": "$", + "name": "comment.block.documentation.solidity", + "patterns": [ + { + "include": "#natspec-tags" + } + ] + } + ] + }, + "natspec-tags": { + "patterns": [ + { + "include": "#natspec-tag-title" + }, + { + "include": "#natspec-tag-author" + }, + { + "include": "#natspec-tag-notice" + }, + { + "include": "#natspec-tag-dev" + }, + { + "include": "#natspec-tag-param" + }, + { + "include": "#natspec-tag-return" + } + ] + }, + "natspec-tag-title": { + "match": "(@title)\\b", + "name": "storage.type.title.natspec" + }, + "natspec-tag-author": { + "match": "(@author)\\b", + "name": "storage.type.author.natspec" + }, + "natspec-tag-notice": { + "match": "(@notice)\\b", + "name": "storage.type.dev.natspec" + }, + "natspec-tag-dev": { + "match": "(@dev)\\b", + "name": "storage.type.dev.natspec" + }, + "natspec-tag-param": { + "match": "(@param)(\\s+([A-Za-z_]\\w*))?\\b", + "captures": { + "1": { + "name": "storage.type.param.natspec" + }, + "3": { + "name": "variable.other.natspec" + } + } + }, + "natspec-tag-return": { + "match": "(@return)\\b", + "name": "storage.type.return.natspec" + }, + "comment": { + "patterns": [ + { + "include": "#comment-line" + }, + { + "include": "#comment-block" + } + ] + }, + "comment-line": { + "match": "(?(?!>)|>=|\\&\\&|\\|\\||\\:(?!=)|\\?)", + "name": "keyword.operator.logic.solidity" + }, + "operator-mapping": { + "match": "(=>)", + "name": "keyword.operator.mapping.solidity" + }, + "operator-arithmetic": { + "match": "(\\+|\\-|\\/|\\*)", + "name": "keyword.operator.arithmetic.solidity" + }, + "operator-binary": { + "match": "(\\^|\\&|\\||<<|>>)", + "name": "keyword.operator.binary.solidity" + }, + "operator-assignment": { + "match": "(\\:?=)", + "name": "keyword.operator.assignment.solidity" + }, + "control": { + "patterns": [ + { + "include": "#control-flow" + }, + { + "include": "#control-using" + }, + { + "include": "#control-import" + }, + { + "include": "#control-pragma" + }, + { + "include": "#control-underscore" + }, + { + "include": "#control-other" + } + ] + }, + "control-flow": { + "match": "\\b(if|else|for|while|do|break|continue|throw|returns?)\\b", + "name": "keyword.control.flow.solidity" + }, + "control-using": { + "match": "\\b(using)\\b", + "name": "keyword.control.using.solidity" + }, + "control-import": { + "match": "\\b(import)\\b", + "name": "keyword.control.import.solidity" + }, + "control-pragma": { + "match": "\\b(pragma)(?:\\s+([A-Za-z_]\\w+)\\s+([^\\s]+))?\\b", + "captures": { + "1": { + "name": "keyword.control.pragma.solidity" + }, + "2": { + "name": "entity.name.tag.pragma.solidity" + }, + "3": { + "name": "constant.other.pragma.solidity" + } + } + }, + "control-underscore": { + "match": "\\b(_)\\b", + "name": "constant.other.underscore.solidity" + }, + "control-other": { + "match": "\\b(new|delete|emit)\\b", + "name": "keyword.control.solidity" + }, + "constant": { + "patterns": [ + { + "include": "#constant-boolean" + }, + { + "include": "#constant-time" + }, + { + "include": "#constant-currency" + } + ] + }, + "constant-boolean": { + "match": "\\b(true|false)\\b", + "name": "constant.language.boolean.solidity" + }, + "constant-time": { + "match": "\\b(seconds|minutes|hours|days|weeks|years)\\b", + "name": "constant.language.time.solidity" + }, + "constant-currency": { + "match": "\\b(ether|wei)\\b", + "name": "constant.language.currency.solidity" + }, + "number": { + "patterns": [ + { + "include": "#number-decimal" + }, + { + "include": "#number-hex" + } + ] + }, + "number-decimal": { + "match": "\\b(\\d+(\\.\\d+)?)\\b", + "name": "constant.numeric.decimal.solidity" + }, + "number-hex": { + "match": "\\b(0[xX][a-fA-F0-9]+)\\b", + "name": "constant.numeric.hexadecimal.solidity" + }, + "string": { + "patterns": [ + { + "match": "\\\".*?\\\"", + "name": "string.quoted.double.solidity" + }, + { + "match": "\\'.*?\\'", + "name": "string.quoted.single.solidity" + } + ] + }, + "type": { + "patterns": [ + { + "include": "#type-primitive" + } + ] + }, + "type-primitive": { + "match": "\\b(address|string\\d*|bytes\\d*|int\\d*|uint\\d*|bool|hash\\d*)\\b", + "name": "support.type.primitive.solidity" + }, + "global": { + "patterns": [ + { + "include": "#global-variables" + }, + { + "include": "#global-functions" + } + ] + }, + "global-variables": { + "patterns": [ + { + "match": "\\b(msg|block|tx|now)\\b", + "name": "variable.language.transaction.solidity" + }, + { + "match": "\\b(this)\\b", + "name": "variable.language.this.solidity" + }, + { + "match": "\\b(super)\\b", + "name": "variable.language.super.solidity" + } + ] + }, + "global-functions": { + "patterns": [ + { + "match": "\\b(require|assert|revert)\\b", + "name": "keyword.control.exceptions.solidity" + }, + { + "match": "\\b(selfdestruct|suicide)\\b", + "name": "keyword.control.contract.solidity" + }, + { + "match": "\\b(addmod|mulmod|keccak256|sha256|sha3|ripemd160|ecrecover)\\b", + "name": "support.function.math.solidity" + }, + { + "match": "\\b(blockhash|gasleft)\\b", + "name": "variable.language.transaction.solidity" + } + ] + }, + "declaration": { + "patterns": [ + { + "include": "#declaration-contract" + }, + { + "include": "#declaration-interface" + }, + { + "include": "#declaration-library" + }, + { + "include": "#declaration-struct" + }, + { + "include": "#declaration-event" + }, + { + "include": "#declaration-enum" + }, + { + "include": "#declaration-function" + }, + { + "include": "#declaration-constructor" + }, + { + "include": "#declaration-modifier" + }, + { + "include": "#declaration-mapping" + } + ] + }, + "declaration-contract": { + "patterns": [ + { + "match": "\\b(contract)(\\s+([A-Za-z_]\\w*))?\\b", + "captures": { + "1": { + "name": "storage.type.contract.solidity" + }, + "3": { + "name": "entity.name.type.contract.solidity" + } + } + }, + { + "match": "\\b(is)\\b", + "name": "storage.modifier.is.solidity" + } + ] + }, + "declaration-interface": { + "match": "\\b(interface)(\\s+([A-Za-z_]\\w*))?\\b", + "captures": { + "1": { + "name": "storage.type.interface.solidity" + }, + "3": { + "name": "entity.name.type.interface.solidity" + } + } + }, + "declaration-library": { + "match": "\\b(library)(\\s+([A-Za-z_]\\w*))?\\b", + "captures": { + "1": { + "name": "storage.type.library.solidity" + }, + "3": { + "name": "entity.name.type.library.solidity" + } + } + }, + "declaration-struct": { + "match": "\\b(struct)(\\s+([A-Za-z_]\\w*))?\\b", + "captures": { + "1": { + "name": "storage.type.struct.solidity" + }, + "3": { + "name": "entity.name.type.struct.solidity" + } + } + }, + "declaration-event": { + "match": "\\b(event)(\\s+([A-Za-z_]\\w*))?\\b", + "captures": { + "1": { + "name": "storage.type.event.solidity" + }, + "3": { + "name": "entity.name.type.event.solidity" + } + } + }, + "declaration-constructor": { + "match": "\\b(constructor)\\b", + "captures": { + "1": { + "name": "storage.type.constructor.solidity" + } + } + }, + "declaration-enum": { + "match": "\\b(enum)(\\s+([A-Za-z_]\\w*))?\\b", + "captures": { + "1": { + "name": "storage.type.enum.solidity" + }, + "3": { + "name": "entity.name.type.enum.solidity" + } + } + }, + "declaration-function": { + "patterns": [ + { + "match": "\\b(function)\\s+([A-Za-z_]\\w*)\\b", + "captures": { + "1": { + "name": "storage.type.function.solidity" + }, + "2": { + "name": "entity.name.function.solidity" + } + } + }, + { + "match": "\\b(private|public|internal|external|constant|pure|view|payable|nonpayable|inherited|indexed|storage|memory|virtual|override)\\b", + "name": "storage.type.mofifier.solidity" + } + ] + }, + "declaration-modifier": { + "match": "\\b(modifier)(\\s+([A-Za-z_]\\w*))?\\b", + "captures": { + "1": { + "name": "storage.type.modifier.solidity" + }, + "3": { + "name": "entity.name.function.solidity" + } + } + }, + "declaration-mapping": { + "match": "\\b(mapping)\\b", + "name": "storage.type.mapping.solidity" + }, + "function-call": { + "match": "\\b([A-Za-z_]\\w*)\\s*\\(", + "captures": { + "1": { + "name": "entity.name.function.solidity" + } + } + }, + "assembly": { + "patterns": [ + { + "match": "\\b(assembly)\\b", + "name": "keyword.control.assembly.solidity" + }, + { + "match": "\\b(let)\\b", + "name": "storage.type.assembly.solidity" + } + ] + }, + "punctuation": { + "patterns": [ + { + "match": ";", + "name": "punctuation.terminator.statement.solidity" + }, + { + "match": "\\.", + "name": "punctuation.accessor.solidity" + }, + { + "match": ",", + "name": "punctuation.separator.solidity" + } + ] + } + }, + "scopeName": "source.solidity", + "uuid": "123" +} \ No newline at end of file diff --git a/packages/frontend/src/lib/editor/server.ts b/packages/frontend/src/lib/editor/server.ts new file mode 100644 index 0000000..9d43a41 --- /dev/null +++ b/packages/frontend/src/lib/editor/server.ts @@ -0,0 +1,31 @@ +import init, { InitOutput, serve, ServerConfig } from "@/lib/wasm/demo_lsp_browser"; +import { FromServer, IntoServer } from "./codec"; + +let server: null | Server; + +export default class Server { + readonly initOutput: InitOutput; + readonly #intoServer: IntoServer; + readonly #fromServer: FromServer; + + private constructor(initOutput: InitOutput, intoServer: IntoServer, fromServer: FromServer) { + this.initOutput = initOutput; + this.#intoServer = intoServer; + this.#fromServer = fromServer; + } + + static async initialize(intoServer: IntoServer, fromServer: FromServer): Promise { + if (null == server) { + const initOutput = await init(); + server = new Server(initOutput, intoServer, fromServer); + } else { + console.warn("Server already initialized; ignoring"); + } + return server; + } + + async start(): Promise { + const config = new ServerConfig(this.#intoServer, this.#fromServer); + await serve(config); + } +} diff --git a/packages/frontend/src/lib/editor/services.ts b/packages/frontend/src/lib/editor/services.ts new file mode 100644 index 0000000..1045ef8 --- /dev/null +++ b/packages/frontend/src/lib/editor/services.ts @@ -0,0 +1,47 @@ +import { Monaco } from "@monaco-editor/react"; +import Client from "./client"; +import { + DidChangeTextDocumentNotification, + DidChangeTextDocumentParams, + DidOpenTextDocumentNotification, + DidOpenTextDocumentParams, +} from "vscode-languageserver-protocol"; +import { editor } from "monaco-editor-core"; +import Language from "./language"; +import { monacoToProtocol } from "./utils"; + +export class EditorService { + constructor(private client: Client) {} + + public fileOpened(model: editor.ITextModel): void { + const params: DidOpenTextDocumentParams = { + textDocument: { + uri: model.uri.toString(), + languageId: model.getLanguageId(), + version: 0, + text: model.getValue(), + }, + }; + + this.client.notify(DidOpenTextDocumentNotification.type.method, params); + } + + public fileChanged(model: editor.ITextModel): void { + const content = model.getValue(); + + const params: DidChangeTextDocumentParams = { + textDocument: { + uri: model.uri.toString(), + version: 0, + }, + contentChanges: [ + { + range: monacoToProtocol.asRange(model.getFullModelRange()), + text: content!, + }, + ], + }; + + this.client.notify(DidChangeTextDocumentNotification.type.method, params); + } +} diff --git a/packages/frontend/src/lib/editor/tracer.ts b/packages/frontend/src/lib/editor/tracer.ts new file mode 100644 index 0000000..7ee236e --- /dev/null +++ b/packages/frontend/src/lib/editor/tracer.ts @@ -0,0 +1,17 @@ +import * as proto from "vscode-languageserver-protocol"; + +// const clientChannel = document.getElementById("channel-client") as HTMLTextAreaElement; +// const serverChannel = document.getElementById("channel-server") as HTMLTextAreaElement; + +export default class Tracer { + static client(message: string): void { + // clientChannel.value += message; + // clientChannel.value += "\n"; + } + + static server(input: string | proto.Message): void { + const message: string = typeof input === "string" ? input : JSON.stringify(input); + // serverChannel.value += message; + // serverChannel.value += "\n"; + } +} diff --git a/packages/frontend/src/lib/editor/utils.ts b/packages/frontend/src/lib/editor/utils.ts new file mode 100644 index 0000000..cc187fd --- /dev/null +++ b/packages/frontend/src/lib/editor/utils.ts @@ -0,0 +1,5 @@ +import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from "./converter"; +import * as monaco from "monaco-editor"; + +export const protocolToMonaco = new ProtocolToMonacoConverter(monaco); +export const monacoToProtocol = new MonacoToProtocolConverter(monaco); diff --git a/packages/frontend/src/lib/googlebackup.ts b/packages/frontend/src/lib/googlebackup.ts new file mode 100644 index 0000000..f31ebcf --- /dev/null +++ b/packages/frontend/src/lib/googlebackup.ts @@ -0,0 +1,97 @@ +import { logger } from "@/state/utils"; +import { ExpNodeType, FolderType } from "@/types/explorer"; + +async function createGoogleFolder(accessToken: string, folderName: string, parentFolderId?: string): Promise { + // Create a folder in Google Drive + const response = await fetch("https://www.googleapis.com/drive/v3/files", { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: folderName, + mimeType: "application/vnd.google-apps.folder", + parents: parentFolderId ? [parentFolderId] : [], + }), + }); + + console.log(response); + + if (!response.ok) { + logger.error(`Failed to Create FOLDER: ${folderName}`); + throw new Error("Failed to create folder"); + } + + const data = await response.json(); + + logger.info(`Successfully Created FOLDER: ${folderName}`); + return data.id as string; +} + +async function createGoogleFile({ + content, + folderId, + name, + accessToken, +}: { + folderId: string; + name: string; + content: string; + accessToken: string; +}) { + const file = new Blob([content], { type: "text/plain" }); + const metadata = { + name, + mimeType: "text/plain", + parents: [folderId], + }; + + const formData = new FormData(); + + formData.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" })); + formData.append("file", file); + + const response = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart", { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: formData, + }); + + if (!response.ok) { + logger.error(`Failed to Create FILE: ${name}`); + return false; + } + const data = await response.json(); + logger.info(`Successfully Created FILE: ${name}`); + + return true; +} + +async function createGoogleBackup( + folder: FolderType, + accessToken: string, + files: Record, + parentFolderId?: string, +) { + const folderId = await createGoogleFolder(accessToken, folder.name, parentFolderId); + + for (const key in folder.items) { + const item = folder.items[key]; + + if (item.type === ExpNodeType.FOLDER) { + await createGoogleBackup(item, accessToken, files, folderId); + } else { + await createGoogleFile({ + content: files[item.path], + folderId, + name: item.name, + accessToken, + }); + } + } +} + +export default createGoogleBackup; diff --git a/packages/frontend/src/lib/theme.ts b/packages/frontend/src/lib/theme.ts new file mode 100644 index 0000000..2275fb1 --- /dev/null +++ b/packages/frontend/src/lib/theme.ts @@ -0,0 +1,14 @@ +export function getSystemTheme(): "dark" | "light" { + // Check if the `window` and `matchMedia` APIs are available + if (typeof window !== "undefined" && window.matchMedia) { + const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); + return darkModeQuery.matches ? "dark" : "light"; + } + + // Default fallback if the system theme can't be determined + return "light"; +} + +export function isDarkTheme(): boolean { + return getSystemTheme() === "dark"; +} diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts new file mode 100644 index 0000000..f5c253a --- /dev/null +++ b/packages/frontend/src/lib/utils.ts @@ -0,0 +1,46 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export const downloadBlob = (code: number[]): void => { + const blob = new Blob([new Uint8Array(code).buffer]); + + const a = document.createElement("a"); + a.download = "result.contract"; + a.href = URL.createObjectURL(blob); + a.dataset.downloadurl = ["application/json", a.download, a.href].join(":"); + a.style.display = "none"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + setTimeout(() => { + URL.revokeObjectURL(a.href); + }, 1500); +}; + +/** + * Generates a random ID. + * @param length - The desired length of the ID. Default is 10. + * @returns A randomly generated ID as a string. + */ +export function generateRandomId(length: number = 10): string { + const characters: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result: string = ''; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + + +export function onEnter(callback: () => void) { + return (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + callback(); + } + }; +} diff --git a/packages/frontend/src/lib/wasm/demo_lsp_browser.d.ts b/packages/frontend/src/lib/wasm/demo_lsp_browser.d.ts new file mode 100644 index 0000000..1af3a41 --- /dev/null +++ b/packages/frontend/src/lib/wasm/demo_lsp_browser.d.ts @@ -0,0 +1,88 @@ +/* tslint:disable */ +/* eslint-disable */ +export function serve(config: ServerConfig): Promise; +/** + * The `ReadableStreamType` enum. + * + * *This API requires the following crate features to be activated: `ReadableStreamType`* + */ +type ReadableStreamType = "bytes"; +export class IntoUnderlyingByteSource { + private constructor(); + free(): void; + start(controller: ReadableByteStreamController): void; + pull(controller: ReadableByteStreamController): Promise; + cancel(): void; + readonly type: ReadableStreamType; + readonly autoAllocateChunkSize: number; +} +export class IntoUnderlyingSink { + private constructor(); + free(): void; + write(chunk: any): Promise; + close(): Promise; + abort(reason: any): Promise; +} +export class IntoUnderlyingSource { + private constructor(); + free(): void; + pull(controller: ReadableStreamDefaultController): Promise; + cancel(): void; +} +export class ServerConfig { + free(): void; + constructor(into_server: AsyncIterator, from_server: WritableStream); +} + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly __wbg_serverconfig_free: (a: number, b: number) => void; + readonly serverconfig_new: (a: any, b: any) => number; + readonly serve: (a: number) => any; + readonly __wbg_intounderlyingbytesource_free: (a: number, b: number) => void; + readonly intounderlyingbytesource_type: (a: number) => number; + readonly intounderlyingbytesource_autoAllocateChunkSize: (a: number) => number; + readonly intounderlyingbytesource_start: (a: number, b: any) => void; + readonly intounderlyingbytesource_pull: (a: number, b: any) => any; + readonly intounderlyingbytesource_cancel: (a: number) => void; + readonly __wbg_intounderlyingsource_free: (a: number, b: number) => void; + readonly intounderlyingsource_pull: (a: number, b: any) => any; + readonly intounderlyingsource_cancel: (a: number) => void; + readonly __wbg_intounderlyingsink_free: (a: number, b: number) => void; + readonly intounderlyingsink_write: (a: number, b: any) => any; + readonly intounderlyingsink_close: (a: number) => any; + readonly intounderlyingsink_abort: (a: number, b: any) => any; + readonly __externref_table_alloc: () => number; + readonly __wbindgen_export_1: WebAssembly.Table; + readonly __wbindgen_exn_store: (a: number) => void; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_export_6: WebAssembly.Table; + readonly closure731_externref_shim: (a: number, b: number, c: any) => void; + readonly closure1257_externref_shim: (a: number, b: number, c: any, d: any) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. +* +* @returns {InitOutput} +*/ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. +* +* @returns {Promise} +*/ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/packages/frontend/src/lib/wasm/demo_lsp_browser.js b/packages/frontend/src/lib/wasm/demo_lsp_browser.js new file mode 100644 index 0000000..dc55111 --- /dev/null +++ b/packages/frontend/src/lib/wasm/demo_lsp_browser.js @@ -0,0 +1,742 @@ +let wasm; + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_export_1.set(idx, obj); + return idx; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + console.log(e); + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedDataViewMemory0 = null; + +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => { + wasm.__wbindgen_export_6.get(state.dtor)(state.a, state.b) +}); + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_6.get(state.dtor)(a, state.b); + CLOSURE_DTORS.unregister(state); + } else { + state.a = a; + } + } + }; + real.original = state; + CLOSURE_DTORS.register(real, state, state); + return real; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function _assertClass(instance, klass) { + if (!(instance instanceof klass)) { + throw new Error(`expected instance of ${klass.name}`); + } +} +/** + * @param {ServerConfig} config + * @returns {Promise} + */ +export function serve(config) { + _assertClass(config, ServerConfig); + var ptr0 = config.__destroy_into_raw(); + const ret = wasm.serve(ptr0); + return ret; +} + +function __wbg_adapter_26(arg0, arg1, arg2) { + wasm.closure731_externref_shim(arg0, arg1, arg2); +} + +function __wbg_adapter_91(arg0, arg1, arg2, arg3) { + wasm.closure1257_externref_shim(arg0, arg1, arg2, arg3); +} + +const __wbindgen_enum_ReadableStreamType = ["bytes"]; + +const IntoUnderlyingByteSourceFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingbytesource_free(ptr >>> 0, 1)); + +export class IntoUnderlyingByteSource { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + IntoUnderlyingByteSourceFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_intounderlyingbytesource_free(ptr, 0); + } + /** + * @returns {ReadableStreamType} + */ + get type() { + const ret = wasm.intounderlyingbytesource_type(this.__wbg_ptr); + return __wbindgen_enum_ReadableStreamType[ret]; + } + /** + * @returns {number} + */ + get autoAllocateChunkSize() { + const ret = wasm.intounderlyingbytesource_autoAllocateChunkSize(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @param {ReadableByteStreamController} controller + */ + start(controller) { + wasm.intounderlyingbytesource_start(this.__wbg_ptr, controller); + } + /** + * @param {ReadableByteStreamController} controller + * @returns {Promise} + */ + pull(controller) { + const ret = wasm.intounderlyingbytesource_pull(this.__wbg_ptr, controller); + return ret; + } + cancel() { + const ptr = this.__destroy_into_raw(); + wasm.intounderlyingbytesource_cancel(ptr); + } +} + +const IntoUnderlyingSinkFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingsink_free(ptr >>> 0, 1)); + +export class IntoUnderlyingSink { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + IntoUnderlyingSinkFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_intounderlyingsink_free(ptr, 0); + } + /** + * @param {any} chunk + * @returns {Promise} + */ + write(chunk) { + const ret = wasm.intounderlyingsink_write(this.__wbg_ptr, chunk); + return ret; + } + /** + * @returns {Promise} + */ + close() { + const ptr = this.__destroy_into_raw(); + const ret = wasm.intounderlyingsink_close(ptr); + return ret; + } + /** + * @param {any} reason + * @returns {Promise} + */ + abort(reason) { + const ptr = this.__destroy_into_raw(); + const ret = wasm.intounderlyingsink_abort(ptr, reason); + return ret; + } +} + +const IntoUnderlyingSourceFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingsource_free(ptr >>> 0, 1)); + +export class IntoUnderlyingSource { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + IntoUnderlyingSourceFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_intounderlyingsource_free(ptr, 0); + } + /** + * @param {ReadableStreamDefaultController} controller + * @returns {Promise} + */ + pull(controller) { + const ret = wasm.intounderlyingsource_pull(this.__wbg_ptr, controller); + return ret; + } + cancel() { + const ptr = this.__destroy_into_raw(); + wasm.intounderlyingsource_cancel(ptr); + } +} + +const ServerConfigFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_serverconfig_free(ptr >>> 0, 1)); + +export class ServerConfig { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + ServerConfigFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_serverconfig_free(ptr, 0); + } + /** + * @param {AsyncIterator} into_server + * @param {WritableStream} from_server + */ + constructor(into_server, from_server) { + const ret = wasm.serverconfig_new(into_server, from_server); + this.__wbg_ptr = ret >>> 0; + ServerConfigFinalization.register(this, this.__wbg_ptr, this); + return this; + } +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_buffer_61b7ce01341d7f88 = function(arg0) { + const ret = arg0.buffer; + return ret; + }; + imports.wbg.__wbg_buffer_dc5dbfa8d5fb28cf = function(arg0) { + const ret = arg0.buffer; + return ret; + }; + imports.wbg.__wbg_byobRequest_1fc36a0c1e98611b = function(arg0) { + const ret = arg0.byobRequest; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_byteLength_1b2d953758afc500 = function(arg0) { + const ret = arg0.byteLength; + return ret; + }; + imports.wbg.__wbg_byteOffset_7ef484c6c1d473e9 = function(arg0) { + const ret = arg0.byteOffset; + return ret; + }; + imports.wbg.__wbg_call_500db948e69c7330 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.call(arg1, arg2); + return ret; + }, arguments) }; + imports.wbg.__wbg_call_b0d8e36992d9900d = function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_close_59511bda900d85a8 = function() { return handleError(function (arg0) { + arg0.close(); + }, arguments) }; + imports.wbg.__wbg_close_65cb23eb0316f916 = function() { return handleError(function (arg0) { + arg0.close(); + }, arguments) }; + imports.wbg.__wbg_close_7b3c2334f3731ec4 = function(arg0) { + const ret = arg0.close(); + return ret; + }; + imports.wbg.__wbg_done_f22c1561fa919baa = function(arg0) { + const ret = arg0.done; + return ret; + }; + imports.wbg.__wbg_enqueue_3997a55771b5212a = function() { return handleError(function (arg0, arg1) { + arg0.enqueue(arg1); + }, arguments) }; + imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }; + imports.wbg.__wbg_getWriter_dd1c7a1972bcd348 = function() { return handleError(function (arg0) { + const ret = arg0.getWriter(); + return ret; + }, arguments) }; + imports.wbg.__wbg_instanceof_Uint8Array_28af5bc19d6acad8 = function(arg0) { + let result; + try { + result = arg0 instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_length_65d1cd11729ced11 = function(arg0) { + const ret = arg0.length; + return ret; + }; + imports.wbg.__wbg_log_464d1b2190ca1e04 = function(arg0) { + console.log(arg0); + }; + imports.wbg.__wbg_new_3d446df9155128ef = function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_91(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return ret; + } finally { + state0.a = state0.b = 0; + } + }; + imports.wbg.__wbg_new_3ff5b33b1ce712df = function(arg0) { + const ret = new Uint8Array(arg0); + return ret; + }; + imports.wbg.__wbg_new_6799ef630abee97c = function(arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { + const ret = new Error(); + return ret; + }; + imports.wbg.__wbg_newnoargs_fd9e4bf8be2bc16d = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbg_newwithbyteoffsetandlength_ba35896968751d91 = function(arg0, arg1, arg2) { + const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0); + return ret; + }; + imports.wbg.__wbg_next_97adbc0d5bb171e7 = function() { return handleError(function (arg0) { + const ret = arg0.next(); + return ret; + }, arguments) }; + imports.wbg.__wbg_queueMicrotask_2181040e064c0dc8 = function(arg0) { + queueMicrotask(arg0); + }; + imports.wbg.__wbg_queueMicrotask_ef9ac43769cbcc4f = function(arg0) { + const ret = arg0.queueMicrotask; + return ret; + }; + imports.wbg.__wbg_ready_26e7f2af8ced9ce5 = function(arg0) { + const ret = arg0.ready; + return ret; + }; + imports.wbg.__wbg_releaseLock_7878dddc005f738f = function(arg0) { + arg0.releaseLock(); + }; + imports.wbg.__wbg_resolve_0bf7c44d641804f9 = function(arg0) { + const ret = Promise.resolve(arg0); + return ret; + }; + imports.wbg.__wbg_respond_88fe7338392675f2 = function() { return handleError(function (arg0, arg1) { + arg0.respond(arg1 >>> 0); + }, arguments) }; + imports.wbg.__wbg_set_23d69db4e5c66a6e = function(arg0, arg1, arg2) { + arg0.set(arg1, arg2 >>> 0); + }; + imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_0be7472e492ad3e3 = function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_THIS_1a6eb482d12c9bfb = function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_SELF_1dc398a895c82351 = function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_WINDOW_ae1c80c7eea8d64a = function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_then_0438fad860fe38e1 = function(arg0, arg1) { + const ret = arg0.then(arg1); + return ret; + }; + imports.wbg.__wbg_then_0ffafeddf0e182a4 = function(arg0, arg1, arg2) { + const ret = arg0.then(arg1, arg2); + return ret; + }; + imports.wbg.__wbg_toString_a491ccf7be1ca5c9 = function(arg0) { + const ret = arg0.toString(); + return ret; + }; + imports.wbg.__wbg_value_4c32fd138a88eee2 = function(arg0) { + const ret = arg0.value; + return ret; + }; + imports.wbg.__wbg_view_a03cbb1d55c73e57 = function(arg0) { + const ret = arg0.view; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_write_0aea81ae26043440 = function(arg0, arg1) { + const ret = arg0.write(arg1); + return ret; + }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = arg0.original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper4837 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 732, __wbg_adapter_26); + return ret; + }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_export_1; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + ; + }; + imports.wbg.__wbindgen_is_function = function(arg0) { + const ret = typeof(arg0) === 'function'; + return ret; + }; + imports.wbg.__wbindgen_is_object = function(arg0) { + const val = arg0; + const ret = typeof(val) === 'object' && val !== null; + return ret; + }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = arg0 === undefined; + return ret; + }; + imports.wbg.__wbindgen_memory = function() { + const ret = wasm.memory; + return ret; + }; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + + return imports; +} + +function __wbg_init_memory(imports, memory) { + +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('demo_lsp_browser_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/packages/frontend/src/lib/wasm/demo_lsp_browser_bg.wasm b/packages/frontend/src/lib/wasm/demo_lsp_browser_bg.wasm new file mode 100644 index 0000000..f520d0d Binary files /dev/null and b/packages/frontend/src/lib/wasm/demo_lsp_browser_bg.wasm differ diff --git a/packages/frontend/src/lib/wasm/demo_lsp_browser_bg.wasm.d.ts b/packages/frontend/src/lib/wasm/demo_lsp_browser_bg.wasm.d.ts new file mode 100644 index 0000000..dcaff6a --- /dev/null +++ b/packages/frontend/src/lib/wasm/demo_lsp_browser_bg.wasm.d.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const __wbg_serverconfig_free: (a: number, b: number) => void; +export const serverconfig_new: (a: any, b: any) => number; +export const serve: (a: number) => any; +export const __wbg_intounderlyingbytesource_free: (a: number, b: number) => void; +export const intounderlyingbytesource_type: (a: number) => number; +export const intounderlyingbytesource_autoAllocateChunkSize: (a: number) => number; +export const intounderlyingbytesource_start: (a: number, b: any) => void; +export const intounderlyingbytesource_pull: (a: number, b: any) => any; +export const intounderlyingbytesource_cancel: (a: number) => void; +export const __wbg_intounderlyingsource_free: (a: number, b: number) => void; +export const intounderlyingsource_pull: (a: number, b: any) => any; +export const intounderlyingsource_cancel: (a: number) => void; +export const __wbg_intounderlyingsink_free: (a: number, b: number) => void; +export const intounderlyingsink_write: (a: number, b: any) => any; +export const intounderlyingsink_close: (a: number) => any; +export const intounderlyingsink_abort: (a: number, b: any) => any; +export const __externref_table_alloc: () => number; +export const __wbindgen_export_1: WebAssembly.Table; +export const __wbindgen_exn_store: (a: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_export_6: WebAssembly.Table; +export const closure731_externref_shim: (a: number, b: number, c: any) => void; +export const closure1257_externref_shim: (a: number, b: number, c: any, d: any) => void; +export const __wbindgen_start: () => void; diff --git a/packages/frontend/src/state/context.ts b/packages/frontend/src/state/context.ts new file mode 100644 index 0000000..3368379 --- /dev/null +++ b/packages/frontend/src/state/context.ts @@ -0,0 +1,34 @@ +import { ExpNodeType, FolderType } from "@/types/explorer"; +import { LogType } from "@/types/log"; +import { Monaco } from "@monaco-editor/react"; + +export const context = { + monaco: null as Monaco | null, + preferences: { + theme: "vs-dark", + fontSize: 14, + autoSave: true, + autoFormat: true, + }, + currentFile: "home" as string | null, + logs: [] as LogType[], + tabs: new Set(), + files: {} as Record, + explorer: { + type: ExpNodeType.FOLDER, + open: true, + name: "explorer", + path: "explorer", + items: { + src: { + type: ExpNodeType.FOLDER, + open: true, + name: "src", + path: "explorer.items.src", + items: {}, + }, + }, + } satisfies FolderType, +}; + +export type Context = typeof context; diff --git a/packages/frontend/src/state/events.ts b/packages/frontend/src/state/events.ts new file mode 100644 index 0000000..cc8b44c --- /dev/null +++ b/packages/frontend/src/state/events.ts @@ -0,0 +1,127 @@ +import { ExpNodeType, FileType, FolderType } from "@/types/explorer"; +import set from "lodash/set"; +import get from "lodash/get"; +import unset from "lodash/unset"; +import { Context } from "./context"; +import { Monaco } from "@monaco-editor/react"; +import { createPath } from "./utils"; +import { MessageType } from "vscode-languageserver-protocol"; +import { nanoid } from "nanoid"; + +export const events = { + toggleFolder: (context: Context, event: { path: string }) => { + const folder = get(context, event.path) as FolderType; + folder.open = !folder.open; + }, + changeContent: (context: Context, event: { content: string }) => { + const path = context.currentFile; + if (path) { + context.files[path] = event.content; + } + }, + setMonaco: (context: Context, event: { monaco: Monaco }) => { + context.monaco = event.monaco; + }, + setCurrentPath: (context: Context, event: { path: string }) => { + context.currentFile = event.path; + events.addTab(context, { path: event.path }); + }, + removeNestedTabs: (context: Context, event: { path: string }) => { + const path = event.path; + const tabs = context.tabs; + for (const tab of tabs) { + if (tab.startsWith(path)) { + tabs.delete(tab); + } + } + }, + addFile(context: Context, event: { basePath: string; name: string; content: string }) { + const path = createPath(event.basePath, event.name); + const file = { + type: ExpNodeType.FILE, + name: event.name, + path: path, + } satisfies FileType; + + set(context, path, file); + context.files[path] = event.content; + context.currentFile = path; + events.addTab(context, { path }); + }, + addFiles(context: Context, event: { basePath: string; files: { name: string; content: string }[] }) { + for (const file of event.files) { + events.addFile(context, { basePath: event.basePath, name: file.name, content: file.content }); + } + }, + addFolder(context: Context, event: { basePath: string; name: string }) { + const folder = get(context, event.basePath) as FolderType; + folder.items[event.name] = { + items: {}, + name: event.name, + open: true, + path: createPath(event.basePath, event.name), + type: ExpNodeType.FOLDER, + } satisfies FolderType; + }, + + deleteFile(context: Context, event: { path: string; basePath: string }) { + const folder = get(context, event.basePath) as FolderType; + const file = get(context, event.path) as FileType; + + events.removeTab(context, { path: event.path }); + delete folder.items[file.name]; + delete context.files[event.path]; + }, + deleteFolder(context: Context, event: { path: string }) { + unset(context, event.path); + events.removeNestedTabs(context, event); + }, + renameFile(context: Context, event: { path: string; name: string; basePath: string }) { + const newPath = createPath(event.basePath, event.name); + events.addFile(context, { + basePath: event.basePath, + name: event.name, + content: context.files[event.path], + }); + + if (context.currentFile === event.path) { + context.currentFile = newPath; + } + + events.deleteFile(context, { path: event.path, basePath: event.basePath }); + }, + renameFolder(context: Context, event: { path: string; name: string; basePath: string }) { + const newPath = createPath(event.basePath, event.name); + const folder = get(context, event.path) as FolderType; + const newFolder = { + name: event.name, + items: folder.items, + open: folder.open, + path: newPath, + type: ExpNodeType.FOLDER, + } satisfies FolderType; + + set(context, newPath, newFolder); + events.deleteFolder(context, { path: event.path }); + }, + removeTab(context: Context, event: { path: string }) { + context.tabs.delete(event.path); + + if (event.path === context.currentFile) { + context.currentFile = Array.from(context.tabs).pop() || null; + } + }, + addTab(context: Context, event: { path: string }) { + context.tabs.add(event.path); + }, + addLog(context: Context, event: { logType: MessageType; message: string }) { + context.logs.push({ + id: nanoid(), + type: event.logType, + message: event.message, + }); + }, + changeFontSize(context: Context, event: { fontSize: number }) { + context.preferences.fontSize = isNaN(event.fontSize) ? 14 : event.fontSize; + }, +}; diff --git a/packages/frontend/src/state/hooks.ts b/packages/frontend/src/state/hooks.ts new file mode 100644 index 0000000..67ed44b --- /dev/null +++ b/packages/frontend/src/state/hooks.ts @@ -0,0 +1,50 @@ +import { FileType, FolderType } from "@/types/explorer"; +import { useSelector } from "@xstate/store/react"; +import get from "lodash/get"; +import { store } from "."; + +export function useExplorer(path: string) { + const open = useSelector(store, (state) => get(state.context, path).open); + const name = useSelector(store, (state) => get(state.context, path).name); + useSelector(store, (state) => { + const folder = get(state.context, path) as FolderType; + const keys = Object.keys(folder.items); + return `${keys.length}:${keys.join(",")}`; + }); + const state = store.getSnapshot().context; + const folder = get(state, path) as FolderType; + + return { open, items: folder.items, name }; +} + +export function useExplorerItem(path: string) { + return useSelector(store, (state) => get(state.context, path)) as FileType; +} + +export function useMonaco() { + return useSelector(store, (state) => state.context.monaco); +} + +export function useCurrentFile() { + const path = useSelector(store, (state) => state.context.currentFile); + const file = useSelector(store, (state) => { + if (!path) { + return null; + } + + return get(state.context, path) as FileType; + }); + + return file; +} + +export function useFileContent() { + const path = useSelector(store, (state) => state.context.currentFile); + return useSelector(store, (state) => { + if (!path) { + return ""; + } + + return state.context.files[path]; + }); +} diff --git a/packages/frontend/src/state/index.ts b/packages/frontend/src/state/index.ts new file mode 100644 index 0000000..0f97626 --- /dev/null +++ b/packages/frontend/src/state/index.ts @@ -0,0 +1,16 @@ +import { createStoreWithProducer } from "@xstate/store"; +import { createBrowserInspector } from "@statelyai/inspect"; +import { produce, enableMapSet } from "immer"; +import { context } from "./context"; +import { events } from "./events"; + +enableMapSet(); + +export const store = createStoreWithProducer(produce, { + context, + on: events, +}); + +const sub = store.inspect((inspectionEvent) => { + console.log(inspectionEvent); +}); diff --git a/packages/frontend/src/state/inistate.ts b/packages/frontend/src/state/inistate.ts new file mode 100644 index 0000000..41faba2 --- /dev/null +++ b/packages/frontend/src/state/inistate.ts @@ -0,0 +1,16 @@ +import { store } from "."; +import { defaultCode } from "./initstate"; + +function initState() { + store.send({ + type: "addFile", + basePath: "explorer.items.src", + name: "main.sol", + content: defaultCode, + }); + + store.send({ type: "setCurrentPath", path: "explorer.items.src.items['main.sol']" }); + store.send({ type: "setCurrentPath", path: "home" }); +} + +export default initState; diff --git a/packages/frontend/src/state/initstate.ts b/packages/frontend/src/state/initstate.ts new file mode 100644 index 0000000..8d762f3 --- /dev/null +++ b/packages/frontend/src/state/initstate.ts @@ -0,0 +1,21 @@ +export const defaultCode = `contract flipper { + bool private value; + + /// Constructor that initializes the \`bool\` value to the given \`init_value\`. + constructor(bool initvalue) { + value = initvalue; + } + + /// A message that can be called on instantiated contracts. + /// This one flips the value of the stored \`bool\` from \`true\` + /// to \`false\` and vice versa. + function flip() public { + value = !value; + } + + /// Simply returns the current value of our \`bool\`. + function get() public view returns (bool) { + return value; + } +} +`; \ No newline at end of file diff --git a/packages/frontend/src/state/utils.ts b/packages/frontend/src/state/utils.ts new file mode 100644 index 0000000..20a9567 --- /dev/null +++ b/packages/frontend/src/state/utils.ts @@ -0,0 +1,21 @@ +import { MessageType } from "vscode-languageserver-protocol"; +import { store } from "."; + +export function createPath(basePath: string, name: string) { + return basePath + ".items" + `['${name}']`; +} + +export const logger = { + error(message: string) { + store.send({ type: "addLog", logType: MessageType.Error, message }); + }, + info(message: string) { + store.send({ type: "addLog", logType: MessageType.Info, message }); + }, + warning(message: string) { + store.send({ type: "addLog", logType: MessageType.Warning, message }); + }, + log(message: string) { + store.send({ type: "addLog", logType: MessageType.Log, message }); + }, +}; diff --git a/packages/frontend/src/types/explorer.ts b/packages/frontend/src/types/explorer.ts new file mode 100644 index 0000000..593bb78 --- /dev/null +++ b/packages/frontend/src/types/explorer.ts @@ -0,0 +1,22 @@ +import { editor } from "monaco-editor-core"; + +export enum ExpNodeType { + FILE = "FILE", + FOLDER = "FOLDER", +} + +export type ExplorerNode = FileType | FolderType; + +export interface FileType { + type: ExpNodeType.FILE; + path: string; + name: string; +} + +export interface FolderType { + type: ExpNodeType.FOLDER; + path: string; + name: string; + open: boolean; + items: Record; +} diff --git a/packages/frontend/src/types/log.ts b/packages/frontend/src/types/log.ts new file mode 100644 index 0000000..2b217a2 --- /dev/null +++ b/packages/frontend/src/types/log.ts @@ -0,0 +1,15 @@ +import { MessageType } from "vscode-languageserver-protocol"; + +export interface LogType { + id: string; + type: MessageType; + message: string; +} + +export const MessageTypeName = { + [MessageType.Error]: "Error", + [MessageType.Info]: "Info", + [MessageType.Warning]: "Warning", + [MessageType.Log]: "Log", + [MessageType.Debug]: "Debug", +}; diff --git a/packages/frontend/tailwind.config.ts b/packages/frontend/tailwind.config.ts new file mode 100644 index 0000000..c1ba796 --- /dev/null +++ b/packages/frontend/tailwind.config.ts @@ -0,0 +1,84 @@ +import type { Config } from "tailwindcss"; + +export default { + darkMode: ["class"], + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + } + } + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json new file mode 100644 index 0000000..421d132 --- /dev/null +++ b/packages/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../script.ts"], + "exclude": ["node_modules"] +}