From d1894995f840de57ca59c707c3db1727eed619ce Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sun, 13 Nov 2022 20:35:21 -0600 Subject: [PATCH 1/4] website(docs): More playground IDE features --- website/package.json | 3 +- website/pnpm-lock.yaml | 13 +- website/src/pages/playground.astro | 2 +- website/src/playground/CodeMirror.tsx | 111 +++- website/src/playground/Collapsible.tsx | 18 +- website/src/playground/Playground.tsx | 290 ++++++--- website/src/playground/PlaygroundLoader.tsx | 107 ++-- .../src/playground/components/CodePane.tsx | 108 ---- .../playground/components/DiagnosticsPane.tsx | 45 ++ .../src/playground/components/Resizable.tsx | 150 +++++ .../playground/components/SettingsPane.tsx | 378 +---------- website/src/playground/components/Tabs.tsx | 85 +++ website/src/playground/styles/_code.scss | 7 + .../src/playground/styles/_diagnostics.scss | 53 ++ website/src/playground/styles/_results.scss | 77 +++ website/src/playground/styles/_settings.scss | 208 +++++++ website/src/playground/styles/_tabs.scss | 66 ++ website/src/playground/styles/index.scss | 86 ++- .../playground/tabs/DiagnosticsConsoleTab.tsx | 20 + .../playground/tabs/DiagnosticsListTab.tsx | 107 ++++ .../src/playground/tabs/DiagnosticsTab.tsx | 18 - .../src/playground/tabs/FormatterCodeTab.tsx | 30 +- .../src/playground/tabs/FormatterIRTab.tsx | 17 +- website/src/playground/tabs/SettingsTab.tsx | 586 ++++++++++++++++++ website/src/playground/types.ts | 135 ++-- website/src/playground/utils.ts | 327 +++++++--- .../src/playground/workers/prettierWorker.ts | 16 +- website/src/playground/workers/romeWorker.ts | 152 +++-- website/src/styles/index.scss | 2 + website/src/styles/playground.scss | 363 ----------- website/src/svg/error.svg | 1 + website/src/svg/info.svg | 43 ++ website/src/svg/warning.svg | 41 ++ 33 files changed, 2444 insertions(+), 1221 deletions(-) delete mode 100644 website/src/playground/components/CodePane.tsx create mode 100644 website/src/playground/components/DiagnosticsPane.tsx create mode 100644 website/src/playground/components/Resizable.tsx create mode 100644 website/src/playground/components/Tabs.tsx create mode 100644 website/src/playground/styles/_code.scss create mode 100644 website/src/playground/styles/_diagnostics.scss create mode 100644 website/src/playground/styles/_results.scss create mode 100644 website/src/playground/styles/_settings.scss create mode 100644 website/src/playground/styles/_tabs.scss create mode 100644 website/src/playground/tabs/DiagnosticsConsoleTab.tsx create mode 100644 website/src/playground/tabs/DiagnosticsListTab.tsx delete mode 100644 website/src/playground/tabs/DiagnosticsTab.tsx create mode 100644 website/src/playground/tabs/SettingsTab.tsx delete mode 100644 website/src/styles/playground.scss create mode 100644 website/src/svg/error.svg create mode 100644 website/src/svg/info.svg create mode 100644 website/src/svg/warning.svg diff --git a/website/package.json b/website/package.json index cbd5e65fc0c..cf0b44b1b74 100644 --- a/website/package.json +++ b/website/package.json @@ -18,6 +18,7 @@ "@astrojs/react": "^1.2.2", "@astrojs/rss": "^1.0.3", "@codemirror/lang-javascript": "^6.1.0", + "@codemirror/lint": "^6.0.0", "@codemirror/state": "6.1.2", "@codemirror/view": "6.4.0", "@docsearch/css": "^3.3.0", @@ -29,7 +30,6 @@ "@types/prettier": "^2.7.1", "@types/react": "^17.0.33", "@types/react-dom": "^17.0.10", - "@types/react-tabs": "^2.3.4", "@uiw/react-codemirror": "^4.12.4", "@vitejs/plugin-react": "^2.1.0", "astro": "^1.6.7", @@ -45,7 +45,6 @@ "prettier": "^2.7.1", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-tabs": "^4.2.1", "rehype-autolink-headings": "^6.1.1", "rehype-slug": "^5.1.0", "rehype-toc": "^3.0.2", diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 81c90208c23..d5143a8392b 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -6,6 +6,7 @@ specifiers: '@astrojs/react': ^1.2.2 '@astrojs/rss': ^1.0.3 '@codemirror/lang-javascript': ^6.1.0 + '@codemirror/lint': ^6.0.0 '@codemirror/state': 6.1.2 '@codemirror/view': 6.4.0 '@docsearch/css': ^3.3.0 @@ -50,6 +51,7 @@ dependencies: '@astrojs/react': 1.2.2_74b3ggvk3akyhuq4eydyx3fqim '@astrojs/rss': 1.0.3 '@codemirror/lang-javascript': 6.1.1 + '@codemirror/lint': 6.0.0 '@codemirror/state': 6.1.2 '@codemirror/view': 6.4.0 '@docsearch/css': 3.3.0 @@ -62,7 +64,7 @@ dependencies: '@types/react': 17.0.52 '@types/react-dom': 17.0.18 '@types/react-tabs': 2.3.4 - '@uiw/react-codemirror': 4.13.2_3umfte35vda6ohottwifs3cpsq + '@uiw/react-codemirror': 4.13.2_ai5dpkfsh6bew42tf7jao77pla '@vitejs/plugin-react': 2.2.0_vite@3.1.8 astro: 1.6.7_ajklay5k626t46b6fyghkbup3i astro-compress: 1.1.3 @@ -1320,9 +1322,10 @@ packages: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: false - /@uiw/codemirror-extensions-basic-setup/4.13.2_ebfztrivxsawr3nzgdquexqq6q: + /@uiw/codemirror-extensions-basic-setup/4.13.2_taect3vlcl4b4hfcnqjzfij36u: resolution: {integrity: sha512-ATJA7WaZ5g7/0teNBQRIalK3RxfU9X7hviwh6BOelaeJrJjkY6x+jVaACYywP1GStsIKcjNCumakpmRPO9w4sQ==} peerDependencies: + '@codemirror/lint': '>=6.0.0' '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' dependencies: @@ -1335,7 +1338,7 @@ packages: '@codemirror/view': 6.4.0 dev: false - /@uiw/react-codemirror/4.13.2_3umfte35vda6ohottwifs3cpsq: + /@uiw/react-codemirror/4.13.2_ai5dpkfsh6bew42tf7jao77pla: resolution: {integrity: sha512-sMfb8F82yLSqyq1gEUlr14E4KGXDfvsCnkGLLwJZqsq/TvJTRVcy0GX87wn3GXBtGhNp5TvGFUDaCnFzbU4bIQ==} peerDependencies: '@codemirror/state': '>=6.0.0' @@ -1348,10 +1351,12 @@ packages: '@codemirror/state': 6.1.2 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.4.0 - '@uiw/codemirror-extensions-basic-setup': 4.13.2_ebfztrivxsawr3nzgdquexqq6q + '@uiw/codemirror-extensions-basic-setup': 4.13.2_taect3vlcl4b4hfcnqjzfij36u codemirror: 6.0.1 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 + transitivePeerDependencies: + - '@codemirror/lint' dev: false /@vitejs/plugin-react/2.2.0_vite@3.1.8: diff --git a/website/src/pages/playground.astro b/website/src/pages/playground.astro index 4dcaabbe9d3..b7fc7cb55d9 100644 --- a/website/src/pages/playground.astro +++ b/website/src/pages/playground.astro @@ -1,7 +1,7 @@ --- import BaseLayout from "/BaseLayout.astro"; import NavigationSidebar from "/components/NavigationSidebar.astro"; -import "/styles/playground.scss"; +import "/playground/styles/index.scss"; --- diff --git a/website/src/playground/CodeMirror.tsx b/website/src/playground/CodeMirror.tsx index 3950fe4a333..409cd83194c 100644 --- a/website/src/playground/CodeMirror.tsx +++ b/website/src/playground/CodeMirror.tsx @@ -1,15 +1,112 @@ +import type { Diagnostic as RomeDiagnostic } from "@rometools/wasm-web"; import type { ReactCodeMirrorProps, ReactCodeMirrorRef, } from "@uiw/react-codemirror"; +import type { Extension } from "@codemirror/state"; +import type { Diagnostic as CodeMirrorDiagnostic } from "@codemirror/lint"; +import { EditorView } from "@codemirror/view"; import RealCodeMirror from "@uiw/react-codemirror"; -import { forwardRef } from "react"; +import { forwardRef, useEffect, useMemo, useState } from "react"; import { useTheme } from "./utils"; +import { lintGutter, setDiagnostics } from "@codemirror/lint"; -export default forwardRef( - function CodeMirror(props, ref) { - const theme = useTheme(); +interface Props extends ReactCodeMirrorProps { + diagnostics?: RomeDiagnostic[]; +} - return ; - }, -); +function getDiagnosticMessage(diagnostic: RomeDiagnostic): string { + let buf = ""; + for (const elem of diagnostic.message) { + buf += elem.content; + } + return buf; +} + +function romeDiagnosticsToCodeMirror( + rome: RomeDiagnostic[], +): CodeMirrorDiagnostic[] { + const codeMirror: CodeMirrorDiagnostic[] = []; + + for (const diag of rome) { + const span = diag.location?.span; + if (span === undefined) { + continue; + } + + let severity: CodeMirrorDiagnostic["severity"]; + switch (diag.severity) { + case "Error": + case "Fatal": { + severity = "error"; + break; + } + + case "Information": { + severity = "info"; + break; + } + + case "Warning": { + severity = "warning"; + break; + } + + default: { + severity = "error"; + } + } + + codeMirror.push({ + from: span[0], + to: span[1], + severity, + message: getDiagnosticMessage(diag), + }); + } + + return codeMirror; +} + +function getDefaultExtensions(extensions: Extension[] = []) { + return [EditorView.lineWrapping, ...extensions]; +} + +export default forwardRef(function CodeMirror( + { diagnostics, ...props }, + ref, +) { + const theme = useTheme(); + + let [editor, setEditor] = useState(); + + function onCreateEditor(editor: EditorView) { + setEditor(editor); + } + + const extensions = useMemo(() => { + if (diagnostics === undefined) { + return getDefaultExtensions(props.extensions); + } + + return [lintGutter(), ...getDefaultExtensions(props.extensions)]; + }, [diagnostics, props.extensions]); + + useEffect(() => { + if (editor !== undefined && diagnostics !== undefined) { + editor.dispatch( + setDiagnostics(editor.state, romeDiagnosticsToCodeMirror(diagnostics)), + ); + } + }, [editor, diagnostics]); + + return ( + + ); +}); diff --git a/website/src/playground/Collapsible.tsx b/website/src/playground/Collapsible.tsx index d05690b03fc..3eaf010defa 100644 --- a/website/src/playground/Collapsible.tsx +++ b/website/src/playground/Collapsible.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { classnames } from "./utils"; interface Props { className?: string; @@ -10,19 +11,18 @@ export default function Collapsible(props: Props) { const [visible, setVisible] = useState(true); function onClick() { - setVisible; - //setVisible(!visible); + setVisible(!visible); } - let className = visible ? "" : "collapsed"; - - if (props.className != null) { - className += ` ${props.className}`; - } + const className = classnames(!visible && "collapsed", props.className); return ( -
-

+
+

{props.heading}

{visible &&
{props.children}
} diff --git a/website/src/playground/Playground.tsx b/website/src/playground/Playground.tsx index 2c1217a70ce..ba30827228b 100644 --- a/website/src/playground/Playground.tsx +++ b/website/src/playground/Playground.tsx @@ -1,47 +1,68 @@ -import type { PlaygroundProps, RomeAstSyntacticData } from "./types"; +import { + emptyPrettierOutput, + emptyRomeOutput, + PlaygroundProps, + RomeAstSyntacticData, +} from "./types"; import type { ReactCodeMirrorRef } from "@uiw/react-codemirror"; import CodeMirror from "./CodeMirror"; import type { ViewUpdate } from "@codemirror/view"; import * as codeMirrorLangRomeAST from "codemirror-lang-rome-ast"; import { javascript } from "@codemirror/lang-javascript"; -import { Tab, TabList, TabPanel, Tabs } from "react-tabs"; import SettingsPane from "./components/SettingsPane"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + createRef, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { EditorSelection } from "@codemirror/state"; import SyntaxTab from "./tabs/SyntaxTab"; import ControlFlowTab from "./tabs/ControlFlowTab"; -import DiagnosticsTab from "./tabs/DiagnosticsTab"; import FormatterCodeTab from "./tabs/FormatterCodeTab"; import FormatterIRTab from "./tabs/FormatterIRTab"; -import { useWindowSize } from "./utils"; -import CodePane from "./components/CodePane"; +import { + getCurrentCode, + getFileState, + isJSXFilename, + isTypeScriptFilename, + useWindowSize, +} from "./utils"; +import Resizable from "./components/Resizable"; +import DiagnosticsPane from "./components/DiagnosticsPane"; +import Tabs from "./components/Tabs"; +import DiagnosticsConsoleTab from "./tabs/DiagnosticsConsoleTab"; +import DiagnosticsListTab from "./tabs/DiagnosticsListTab"; +import SettingsTab from "./tabs/SettingsTab"; export default function PlaygroundLoader({ setPlaygroundState, resetPlaygroundState, - playgroundState: { code, ...settings }, - prettierOutput, - romeOutput: { - cst, - ast, - formatted_code, - formatter_ir, - errors, - control_flow_graph, - }, + playgroundState, }: PlaygroundProps) { - const { jsx: isJsx, typescript: isTypeScript } = settings; const [clipboardStatus, setClipboardStatus] = useState< "success" | "failed" | "normal" >("normal"); - const extensions = useMemo( + + const file = getFileState(playgroundState, playgroundState.currentFile); + const romeOutput = file.rome; + const prettierOutput = file.prettier; + + console.log(file); + + const codeMirrorExtensions = useMemo( () => [ javascript({ - jsx: isJsx, - typescript: isTypeScript, + jsx: isJSXFilename(playgroundState.currentFile), + typescript: isTypeScriptFilename(playgroundState.currentFile), }), ], - [isJsx, isTypeScript], + [ + isJSXFilename(playgroundState.currentFile), + isTypeScriptFilename(playgroundState.currentFile), + ], ); const romeAstSyntacticDataRef = useRef(null); @@ -52,7 +73,7 @@ export default function PlaygroundLoader({ if (clipboardStatus !== "normal") { setClipboardStatus("normal"); } - }, [formatter_ir]); + }, [romeOutput.formatter.ir]); const onUpdate = useCallback((viewUpdate: ViewUpdate) => { const cursorPosition = viewUpdate.state.selection.ranges[0]?.from ?? 0; @@ -67,11 +88,12 @@ export default function PlaygroundLoader({ }, []); useEffect(() => { - scrollAstNodeIntoView(settings.cursorPosition); - }, [settings.cursorPosition]); + scrollAstNodeIntoView(playgroundState.cursorPosition); + }, [playgroundState.cursorPosition]); // We update the syntactic data of `RomeJsAst` only AstSource(`Display` string of our original AstRepresentation) changed. useEffect(() => { + const ast = romeOutput.syntax.ast; let tree = codeMirrorLangRomeAST.parser.parse(ast); let rangeMap = new Map(); romeAstSyntacticDataRef.current = { @@ -105,99 +127,183 @@ export default function PlaygroundLoader({ } }, }); - }, [ast]); + }, [romeOutput.syntax.ast]); - const onChange = useCallback((value) => { - setPlaygroundState((state) => ({ ...state, code: value })); + const onChange = useCallback((value: string) => { + setPlaygroundState((state) => ({ + ...state, + files: { + ...state.files, + [state.currentFile]: { + ...getFileState(state, state.currentFile), + content: value, + }, + }, + })); }, []); const { width } = useWindowSize(); const hasNarrowViewport = width !== undefined && width <= 1000; + const editorRef = createRef(); + + const code = getCurrentCode(playgroundState) ?? ""; + const editor = ( ); - const settingsPane = ( - setPlaygroundState((state) => ({ ...state, tab }))} + tabs={[ + { + key: "code", + title: "Code", + visible: hasNarrowViewport, + children: editor, + }, + { + key: "diagnostics", + title: "Diagnostics", + visible: hasNarrowViewport, + children: ( + + ), + }, + { + key: "formatter", + title: "Formatter", + children: ( + + ), + }, + { + key: "formatter-ir", + title: "Formatter IR", + children: ( + + ), + }, + { + key: "syntax", + title: "Syntax", + children: ( + + ), + }, + { + key: "cfg", + title: "Control Flow Graph", + children: ( + + ), + }, + { + key: "Console", + title: "Console", + visible: hasNarrowViewport, + children: ( + + ), + }, + { + key: "settings", + title: "Settings", + visible: hasNarrowViewport, + children: ( + + ), + }, + ]} /> ); + if (hasNarrowViewport) { + return results; + } + return ( <> - {!hasNarrowViewport && settingsPane} - - {!hasNarrowViewport && {editor}} - - - - {hasNarrowViewport && Code} - {hasNarrowViewport && Settings} - Formatter - Diagnostics - Syntax - IR - Control Flow Graph - - {hasNarrowViewport && {editor}} - {hasNarrowViewport && {settingsPane}} - - + +
+ {editor} + + - - - - - - - - - - - - - - + +
+ + + {results} + ); function scrollAstNodeIntoView(cursorPosition: number) { - if (astPanelCodeMirrorRef.current && romeAstSyntacticDataRef.current) { - let codemirror = astPanelCodeMirrorRef.current; - let syntacticData = romeAstSyntacticDataRef.current; - let { view } = codemirror; - let { rangeMap } = syntacticData; - for (let [sourceRange, displaySourceRange] of rangeMap.entries()) { - if ( - cursorPosition >= sourceRange[0] && - cursorPosition <= sourceRange[1] - ) { - view?.dispatch({ - scrollIntoView: true, - selection: EditorSelection.create([ - EditorSelection.range( - displaySourceRange[0], - displaySourceRange[1], - ), - EditorSelection.cursor(displaySourceRange[0]), - ]), - }); - } + if ( + astPanelCodeMirrorRef.current == null || + romeAstSyntacticDataRef.current == null + ) { + return; + } + + const view = astPanelCodeMirrorRef.current.view; + const rangeMap = romeAstSyntacticDataRef.current.rangeMap; + + for (let [sourceRange, displaySourceRange] of rangeMap.entries()) { + if ( + cursorPosition >= sourceRange[0] && + cursorPosition <= sourceRange[1] + ) { + view?.dispatch({ + scrollIntoView: true, + selection: EditorSelection.create([ + EditorSelection.range(displaySourceRange[0], displaySourceRange[1]), + EditorSelection.cursor(displaySourceRange[0]), + ]), + }); } } } diff --git a/website/src/playground/PlaygroundLoader.tsx b/website/src/playground/PlaygroundLoader.tsx index 2d80d66c46f..b8b0ef3d3a4 100644 --- a/website/src/playground/PlaygroundLoader.tsx +++ b/website/src/playground/PlaygroundLoader.tsx @@ -1,28 +1,24 @@ import { useEffect, useState, useRef } from "react"; -import { LoadingState, RomeOutput } from "./types"; -import { defaultRomeConfig } from "./types"; -import { usePlaygroundState } from "./utils"; +import { LoadingState } from "./types"; +import { getCurrentCode, getFileState, usePlaygroundState } from "./utils"; import Playground from "./Playground"; import LoadingScreen from "./components/LoadingScreen"; +function throttle(callback: () => void): () => void { + const timeout = setTimeout(callback, 100); + + return () => { + clearTimeout(timeout); + }; +} + function App() { const [loadingState, setLoadingState] = useState(LoadingState.Loading); - const [romeConfig, setRomeConfig] = useState(defaultRomeConfig); - const [playgroundState, setPlaygroundState, resetPlaygroundState] = - usePlaygroundState(romeConfig); + const [state, setPlaygroundState, resetPlaygroundState] = + usePlaygroundState(); const romeWorkerRef = useRef(null); const prettierWorkerRef = useRef(null); - const [romeOutput, setRomeOutput] = useState({ - ast: "", - cst: "", - errors: "", - formatted_code: "", - formatter_ir: "", - control_flow_graph: "", - }); - const [prettierOutput, setPrettierOutput] = useState({ code: "", ir: "" }); - useEffect(() => { romeWorkerRef.current = new Worker( new URL("./workers/romeWorker", import.meta.url), @@ -38,14 +34,21 @@ function App() { case "init": { const loadingState = event.data.loadingState as LoadingState; setLoadingState(loadingState); - if (loadingState === LoadingState.Success) { - setRomeConfig({ ...romeConfig }); - } break; } - case "formatted": { - setRomeOutput(event.data.romeOutput); + case "updated": { + const { filename, romeOutput } = event.data; + setPlaygroundState((state) => ({ + ...state, + files: { + ...state.files, + [filename]: { + ...getFileState(state, filename), + rome: romeOutput, + }, + }, + })); break; } @@ -57,7 +60,17 @@ function App() { prettierWorkerRef.current.addEventListener("message", (event) => { switch (event.data.type) { case "formatted": { - setPrettierOutput(event.data.prettierOutput); + const { filename, prettierOutput } = event.data; + setPlaygroundState((state) => ({ + ...state, + files: { + ...state.files, + [filename]: { + ...getFileState(state, filename), + prettier: prettierOutput, + }, + }, + })); break; } @@ -76,25 +89,51 @@ function App() { }; }, []); + // Dispatch updated settings + useEffect(() => { + if (loadingState !== LoadingState.Success) { + return; + } + + return throttle(() => { + romeWorkerRef.current?.postMessage({ + type: "updateSettings", + settings: state.settings, + }); + + prettierWorkerRef.current?.postMessage({ + type: "updateSettings", + settings: state.settings, + }); + }); + }, [loadingState, state.settings]); + + // Dispatch updated code to Prettier useEffect(() => { if (loadingState !== LoadingState.Success) { return; } - // Throttle the formatting so that it doesn't run on every keystroke to prevent that the - // workers are busy formatting outdated code. - let timeout = setTimeout(() => { - romeWorkerRef.current?.postMessage({ type: "format", playgroundState }); + return throttle(() => { prettierWorkerRef.current?.postMessage({ type: "format", - playgroundState, + filename: state.currentFile, + code: getCurrentCode(state), }); - }, 100); - return () => { - clearTimeout(timeout); - }; - }, [loadingState, playgroundState]); + romeWorkerRef.current?.postMessage({ + type: "update", + cursorPosition: state.cursorPosition, + filename: state.currentFile, + code: getCurrentCode(state), + }); + }); + }, [ + loadingState, + state.currentFile, + state.cursorPosition, + getCurrentCode(state), + ]); switch (loadingState) { case LoadingState.Error: @@ -108,9 +147,7 @@ function App() { ); } diff --git a/website/src/playground/components/CodePane.tsx b/website/src/playground/components/CodePane.tsx deleted file mode 100644 index e52507fe827..00000000000 --- a/website/src/playground/components/CodePane.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type React from "react"; -import { useState, createRef, useEffect } from "react"; -import { createLocalStorage } from "../utils"; - -interface Props { - children: JSX.Element; -} - -const RESIZE_TOLERANCE = 10; -const MINIMUM_SIZE = 200; - -const codePaneSizeStore = createLocalStorage("code-width"); - -export default function CodePane({ children }: Props) { - let [rawWidth, setWidth] = useState( - codePaneSizeStore.getNumber(), - ); - let [isResizing, setIsResizing] = useState(false); - let [canResize, setCanResize] = useState(false); - - const width = - rawWidth === undefined ? undefined : Math.max(MINIMUM_SIZE, rawWidth); - - const ref: React.RefObject = createRef(); - - let cursor = canResize || isResizing ? "col-resize" : undefined; - - useEffect(() => { - // Detect if we can resize - function onMouseMove(event: MouseEvent) { - const container = ref.current; - if (container == null) { - return; - } - - const mouseX = event.pageX; - const containerX = container.offsetLeft + container.clientWidth; - const distance = Math.abs(containerX - mouseX); - - if (isResizing) { - event.preventDefault(); - const width = mouseX - container.offsetLeft; - setWidth(width); - codePaneSizeStore.set(width); - } - - if (distance <= RESIZE_TOLERANCE) { - setCanResize(true); - } else if (canResize) { - setCanResize(false); - } - } - - function onContextMenu(e: MouseEvent) { - if (isResizing || canResize) { - e.preventDefault(); - codePaneSizeStore.clear(); - setWidth(undefined); - setCanResize(false); - setIsResizing(false); - } - } - - function onMouseDown() { - if (canResize) { - setIsResizing(true); - } - } - - function onMouseUp() { - setIsResizing(false); - } - - window.addEventListener("contextmenu", onContextMenu); - window.addEventListener("mousedown", onMouseDown); - window.addEventListener("mouseup", onMouseUp); - window.addEventListener("mousemove", onMouseMove); - - return () => { - window.removeEventListener("contextmenu", onContextMenu); - window.removeEventListener("mousedown", onMouseDown); - window.removeEventListener("mouseup", onMouseUp); - window.removeEventListener("mousemove", onMouseMove); - }; - }); - - useEffect(() => { - if (cursor === undefined) { - document.body.style.removeProperty("cursor"); - } else { - document.body.style.cursor = cursor; - } - - return () => { - document.body.style.removeProperty("cursor"); - }; - }, [cursor]); - - return ( -
- {children} -
- ); -} diff --git a/website/src/playground/components/DiagnosticsPane.tsx b/website/src/playground/components/DiagnosticsPane.tsx new file mode 100644 index 00000000000..06bf242c64a --- /dev/null +++ b/website/src/playground/components/DiagnosticsPane.tsx @@ -0,0 +1,45 @@ +import type { Diagnostic } from "@rometools/wasm-web"; +import type { ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { useState } from "react"; +import DiagnosticsConsoleTab from "../tabs/DiagnosticsConsoleTab"; +import DiagnosticsListTab from "../tabs/DiagnosticsListTab"; +import Tabs from "./Tabs"; + +interface Props { + editorRef: React.RefObject; + console: string; + diagnostics: Diagnostic[]; +} + +export default function DiagnosticsPane({ + editorRef, + diagnostics, + console, +}: Props) { + const [tab, setTab] = useState("diagnostics"); + + return ( + + ), + }, + { + key: "console", + title: "Console", + children: , + }, + ]} + /> + ); +} diff --git a/website/src/playground/components/Resizable.tsx b/website/src/playground/components/Resizable.tsx new file mode 100644 index 00000000000..a4d8cdfe623 --- /dev/null +++ b/website/src/playground/components/Resizable.tsx @@ -0,0 +1,150 @@ +import type React from "react"; +import { useMemo, useState, createRef, useEffect } from "react"; +import { createLocalStorage } from "../utils"; + +interface Props { + name: string; + direction: "top" | "right" | "left"; + className: string; + children: React.ReactNode; +} + +const RESIZE_TOLERANCE = 5; +const MINIMUM_SIZE = 100; + +type ResizeHandler = { + styleProperty: string; + resizingCursor: string; + calculateDistance: (container: HTMLDivElement, event: MouseEvent) => number; + calculateSize: (container: HTMLDivElement, event: MouseEvent) => number; +}; + +const handlers: { + [key in Props["direction"]]: ResizeHandler; +} = { + top: { + resizingCursor: "row-resize", + styleProperty: "height", + calculateDistance: (container, event) => container.offsetTop - event.pageY, + calculateSize: (container, event) => + event.pageY - (container.offsetTop + container.clientHeight), + }, + left: { + resizingCursor: "col-resize", + styleProperty: "width", + calculateDistance: (container, event) => container.offsetLeft - event.pageX, + calculateSize: (container, event) => + container.offsetLeft + container.clientWidth - event.pageX, + }, + right: { + resizingCursor: "col-resize", + styleProperty: "width", + calculateDistance: (container, event) => + container.offsetLeft + container.clientWidth - event.pageX, + calculateSize: (container, event) => event.pageX - container.offsetLeft, + }, +}; + +export default function Resizable({ + name, + direction, + className, + children, +}: Props) { + const sizeStore = useMemo(() => createLocalStorage(`${name}-size`), [name]); + + const [isResizing, setIsResizing] = useState(false); + const [canResize, setCanResize] = useState(false); + + let [rawSize, setSize] = useState(sizeStore.getNumber()); + + const size = + rawSize === undefined ? undefined : Math.max(MINIMUM_SIZE, rawSize); + + const ref: React.RefObject = createRef(); + + const handler = handlers[direction]; + + let cursor = canResize || isResizing ? handler.resizingCursor : undefined; + + useEffect(() => { + function onMouseMove(event: MouseEvent) { + const container = ref.current; + if (container == null) { + return; + } + + const distance = Math.abs(handler.calculateDistance(container, event)); + + if (isResizing) { + event.preventDefault(); + const size = Math.abs(handler.calculateSize(container, event)); + setSize(size); + sizeStore.set(size); + } + + if (distance <= RESIZE_TOLERANCE) { + setCanResize(true); + } else if (canResize) { + setCanResize(false); + } + } + + function onContextMenu(e: MouseEvent) { + if (isResizing || canResize) { + e.preventDefault(); + sizeStore.clear(); + setSize(undefined); + setCanResize(false); + setIsResizing(false); + } + } + + function onMouseDown() { + if (canResize) { + setIsResizing(true); + } + } + + function onMouseUp() { + setIsResizing(false); + } + + window.addEventListener("contextmenu", onContextMenu); + window.addEventListener("mousedown", onMouseDown); + window.addEventListener("mouseup", onMouseUp); + window.addEventListener("mousemove", onMouseMove); + + return () => { + window.removeEventListener("contextmenu", onContextMenu); + window.removeEventListener("mousedown", onMouseDown); + window.removeEventListener("mouseup", onMouseUp); + window.removeEventListener("mousemove", onMouseMove); + }; + }); + + useEffect(() => { + if (cursor === undefined) { + document.body.style.removeProperty("cursor"); + } else { + document.body.style.cursor = cursor; + } + + return () => { + document.body.style.removeProperty("cursor"); + }; + }, [cursor]); + + return ( +
+ {children} +
+ ); +} diff --git a/website/src/playground/components/SettingsPane.tsx b/website/src/playground/components/SettingsPane.tsx index 022c785ef6d..824ffa297ea 100644 --- a/website/src/playground/components/SettingsPane.tsx +++ b/website/src/playground/components/SettingsPane.tsx @@ -1,59 +1,11 @@ -import { - IndentStyle, - PlaygroundSettings, - PlaygroundState, - QuoteProperties, - QuoteStyle, - SourceType, - TrailingComma, -} from "../types"; -import type { Dispatch, SetStateAction } from "react"; +import type { SettingsTabProps } from "../tabs/SettingsTab"; import { useState, useEffect } from "react"; -import { createLocalStorage, createSetter } from "../utils"; - -interface Props { - settings: PlaygroundSettings; - setPlaygroundState: Dispatch>; - onReset: () => void; -} +import { classNames, createLocalStorage } from "../utils"; +import SettingsTab from "../tabs/SettingsTab"; const isCollapsedStore = createLocalStorage("settings-collapsed"); -export default function SettingsPane({ - setPlaygroundState, - onReset, - settings: { - lineWidth, - indentWidth, - indentStyle, - quoteStyle, - quoteProperties, - trailingComma, - sourceType, - typescript: isTypeScript, - jsx: isJsx, - enabledNurseryRules, - enabledLinting, - }, -}: Props) { - const setIsTypeScript = createSetter(setPlaygroundState, "typescript"); - const setIsJsx = createSetter(setPlaygroundState, "jsx"); - const setSourceType = createSetter(setPlaygroundState, "sourceType"); - const setLineWidth = createSetter(setPlaygroundState, "lineWidth"); - const setIndentWidth = createSetter(setPlaygroundState, "indentWidth"); - const setIndentStyle = createSetter(setPlaygroundState, "indentStyle"); - const setQuoteStyle = createSetter(setPlaygroundState, "quoteStyle"); - const setQuoteProperties = createSetter( - setPlaygroundState, - "quoteProperties", - ); - const setTrailingComma = createSetter(setPlaygroundState, "trailingComma"); - const setEnabledNurseryRules = createSetter( - setPlaygroundState, - "enabledNurseryRules", - ); - const setEnabledLinting = createSetter(setPlaygroundState, "enabledLinting"); - +export default function SettingsPane(props: SettingsTabProps) { const [isCollapsed, setIsCollapsed] = useState(isCollapsedStore.getBoolean()); function collapseToggle() { @@ -68,43 +20,11 @@ export default function SettingsPane({
{!isCollapsed && (
-
- -
- - - +
)}
@@ -115,289 +35,3 @@ export default function SettingsPane({
); } - -function SyntaxSettings({ - sourceType, - setSourceType, - isTypeScript, - setIsTypeScript, - isJsx, - setIsJsx, -}: { - sourceType: SourceType; - setSourceType: (sourceType: SourceType) => void; - isTypeScript: boolean; - setIsTypeScript: (value: boolean) => void; - isJsx: boolean; - setIsJsx: (value: boolean) => void; -}) { - return ( - <> -

Syntax

-
-
- - -
- -
- { - setIsTypeScript(e.target.checked); - }} - disabled={sourceType === SourceType.Script} - /> - -
- -
- setIsJsx(e.target.checked)} - disabled={sourceType === SourceType.Script} - /> - -
-
- - ); -} - -function FormatterSettings({ - lineWidth, - setLineWidth, - indentStyle, - setIndentStyle, - indentWidth, - setIndentWidth, - quoteStyle, - setQuoteStyle, - quoteProperties, - setQuoteProperties, - trailingComma, - setTrailingComma, -}: { - lineWidth: number; - setLineWidth: (value: number) => void; - indentStyle: IndentStyle; - setIndentStyle: (value: IndentStyle) => void; - indentWidth: number; - setIndentWidth: (value: number) => void; - quoteStyle: QuoteStyle; - setQuoteStyle: (value: QuoteStyle) => void; - quoteProperties: QuoteProperties; - setQuoteProperties: (value: QuoteProperties) => void; - trailingComma: TrailingComma; - setTrailingComma: (value: TrailingComma) => void; -}) { - return ( - <> -

Formatter

-
- - -
- - -
- -
- - { - setIndentWidth(parseInt(e.target.value)); - }} - /> -
- -
- - -
- -
- - -
- -
- - -
-
- - ); -} - -function LinterSettings({ - enabledNurseryRules, - setEnabledNurseryRules, - enabledLinting, - setEnabledLinting, -}: { - enabledNurseryRules: boolean; - setEnabledNurseryRules: (value: boolean) => void; - enabledLinting: boolean; - setEnabledLinting: (value: boolean) => void; -}) { - return ( - <> -

Linter

-
-
- setEnabledLinting(e.target.checked)} - /> - -
-
- setEnabledNurseryRules(e.target.checked)} - /> - -
-
- - ); -} - -function LineWidthInput({ - lineWidth, - setLineWidth, -}: { - lineWidth: number; - setLineWidth: (lineWidth: number) => void; -}) { - const [showCustom, setShowCustom] = useState( - lineWidth !== 80 && lineWidth !== 120, - ); - - return ( - <> -
- - -
-
- - - - - -
- - {showCustom && ( - { - setLineWidth(parseInt(e.target.value)); - }} - /> - )} -
-
- - ); -} diff --git a/website/src/playground/components/Tabs.tsx b/website/src/playground/components/Tabs.tsx new file mode 100644 index 00000000000..15185afa7a9 --- /dev/null +++ b/website/src/playground/components/Tabs.tsx @@ -0,0 +1,85 @@ +import type React from "react"; +import { classnames } from "../utils"; + +interface Tab { + key: string; + title: React.ReactNode; + visible?: boolean; + children: React.ReactNode; +} + +interface Props { + className?: string; + selectedTab: string; + onSelect: (key: string) => void; + tabs: Tab[]; +} + +export default function Tabs({ + className, + tabs, + selectedTab, + onSelect, +}: Props) { + return ( +
+
    + {tabs.map((tab) => { + if (tab.visible === false) { + return; + } + + const isSelected = tab.key === selectedTab; + + function onClick() { + if (!isSelected) { + onSelect(tab.key); + } + } + + return ( + + ); + })} +
+ + {tabs.map((tab) => { + if (tab.visible === false) { + return; + } + + const isSelected = tab.key === selectedTab; + + return ( +
+ {isSelected && tab.children} +
+ ); + })} +
+ ); +} diff --git a/website/src/playground/styles/_code.scss b/website/src/playground/styles/_code.scss new file mode 100644 index 00000000000..2afbfdab089 --- /dev/null +++ b/website/src/playground/styles/_code.scss @@ -0,0 +1,7 @@ +@import "../../styles/_variables"; +@import "../../styles/_mixins"; + +.code-pane { + display: flex; + flex-direction: column; +} diff --git a/website/src/playground/styles/_diagnostics.scss b/website/src/playground/styles/_diagnostics.scss new file mode 100644 index 00000000000..b62ff541062 --- /dev/null +++ b/website/src/playground/styles/_diagnostics.scss @@ -0,0 +1,53 @@ +@import "../../styles/_variables"; +@import "../../styles/_mixins"; + +.diagnostics-pane { + height: 200px; + border-top: 1px solid var(--playground-border); + + .empty-panel { + background-color: #282c34; + } +} + +.diagnostics-tabs { + height: 100%; + display: flex; + flex-direction: column; +} + +.diagnostics-console { + font-size: 14px; +} + +ul.diagnostics-list { + line-height: 30px; + font-size: 14px; + height: 100%; + overflow: auto; + background-color: #282c34; + + li { + padding-right: 10px; + cursor: pointer; + display: flex; + + img { + margin: 7px 10px; + width: 16px; + height: 16px; + } + + .linecol { + margin-left: auto; + } + + &:nth-child(2n) { + background-color: rgba(#000, 10%); + } + + &:hover { + background-color: rgba(#000, 20%); + } + } +} diff --git a/website/src/playground/styles/_results.scss b/website/src/playground/styles/_results.scss new file mode 100644 index 00000000000..77e9f65767e --- /dev/null +++ b/website/src/playground/styles/_results.scss @@ -0,0 +1,77 @@ +@import "../../styles/_variables"; +@import "../../styles/_mixins"; + +.results-pane { + border-left: 1px solid var(--playground-border); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.results-tabs { + height: 100%; +} + +.mermaid { + border-top: 1px solid var(--playground-border); + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + background-color: rgba(255, 255, 255, 0.1); +} + +.collapsible-container { + display: flex; + flex-direction: column; + flex-grow: 1; + flex-shrink: 1; + flex-basis: 0px; + height: 0; + + &.collapsed { + flex-grow: 0; + flex-shrink: 0; + flex-basis: content; + height: auto; + } +} + +.collapsible-content { + display: flex; + flex-direction: column; + flex: 1; + overflow: auto; +} + +.diff-hint { + margin-left: auto; + opacity: 0.8; + + .insertions { + color: green; + } + + .deletions { + color: red; + } + + .error { + color: red; + font-weight: bold; + } +} + +.cm-theme-light, .cm-theme-dark { + height: 100%; + overflow: auto; +} + +.cm-editor { + height: 100%; +} + +.cm-focused { + outline: none !important; +} diff --git a/website/src/playground/styles/_settings.scss b/website/src/playground/styles/_settings.scss new file mode 100644 index 00000000000..69e2ddab2a4 --- /dev/null +++ b/website/src/playground/styles/_settings.scss @@ -0,0 +1,208 @@ +@import "../../styles/_variables"; +@import "../../styles/_mixins"; + +.settings-pane { + flex-shrink: 0; + overflow: auto; + font-size: 14px; + display: flex; + border-right: 1px solid var(--playground-border); + background-color: var(--container-color); + + @include dark-mode { + background-color: rgba(255, 255, 255, 0.2); + } + + .fields { + width: 250px; + height: 100%; + overflow: auto; + } + + .collapser { + width: 5px; + height: 100%; + background-color: var(--hard-border-color); + flex-shrink: 0; + opacity: 0.5; + cursor: w-resize; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 5px; + + .dot { + width: 3px; + height: 3px; + background-color: rgba(#000, 50%); + border-radius: 50%; + } + + &.collapsed { + width: 10px; + cursor: e-resize; + + .dot { + width: 5px; + height: 5px; + } + } + + &:hover { + opacity: 1; + } + } +} + +.settings-tab-buttons { + display: flex; + gap: 10px; +} + +.settings-tab { + section { + padding: 10px; + } + + input[type=checkbox] { + margin-right: 10px; + } + + select, input[type=number] { + width: 100px; + } + + select, input[type=number], .input-container { + margin-left: auto; + } + + .input-container { + display: flex; + flex-direction: column; + flex-shrink: 0; + width: auto; + } + + input[type=text], input[type=number] { + border-radius: 6px; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 2px 10px; + + @include dark-mode { + color: inherit; + background-color: rgba(255, 255, 255, 0.3); + border-color: rgba(0, 0, 0, 0.5); + } + } + + select { + @include button; + border-radius: 6px; + padding: 2px 6px; + color: inherit; + border: 1px solid rgba(0, 0, 0, 0.2); + + @include dark-mode { + border-color: rgba(255, 255, 255, 0.1); + } + } + + + button { + @include button; + border-radius: 0; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 2px 6px; + color: inherit; + border-radius: 6px; + + @include dark-mode { + border-color: rgba(255, 255, 255, 0.1); + } + + &:disabled { + opacity: 0.5; + font-weight: bold; + } + } + + .button-group button { + border-radius: 0; + + &:disabled:not(:last-child) { + border-right: none; + } + + &:not(:first-child):not(&:disabled + button) { + border-left: none; + } + + &:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + } + + &:last-child { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + } + + .field-row { + display: flex; + line-height: 25px; + margin-bottom: 10px; + + label { + width: 100%; + } + + &.disabled { + opacity: 0.4; + + &, * { + cursor: not-allowed; + } + } + } +} + +.react-tabs__tab-panel .settings-pane { + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.1) !important; + border-top: 1px solid var(--playground-border); +} + +.file-view { + padding-bottom: 10px; + + .files-heading { + display: flex; + align-items: center; + + button { + margin-left: auto; + } + } + + > input[type=text] { + width: 100%; + padding: 5px; + margin: 5px; + } + + .files-list { + padding-top: 10px; + + li { + padding: 4px 10px; + + &.active { + font-weight: bold; + background: rgba(#000, 10%); + } + } + } +} diff --git a/website/src/playground/styles/_tabs.scss b/website/src/playground/styles/_tabs.scss new file mode 100644 index 00000000000..5f69b4cc2c2 --- /dev/null +++ b/website/src/playground/styles/_tabs.scss @@ -0,0 +1,66 @@ +@import "../../styles/_variables"; +@import "../../styles/_mixins"; + +.react-tabs__tab-panel { + display: none; + flex-direction: column; + height: 100%; + overflow: hidden; + + > pre[class*=language-] { + margin: 0; + height: 100%; + background-color: #282c34 !important; + } + + > iframe { + height: 100%; + } +} + +.empty-panel { + display: flex; + width: 100%; + height: 100%; + background-color: var(--background-color); + justify-content: center; + align-items: center; +} + +.react-tabs__tab-list { + display: flex; + flex-direction: row; + flex-grow: 0; + flex-shrink: 0; + height: auto; + line-height: 35px; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + background-color: var(--container-color); + font-size: 14px; + + @include dark-mode { + background-color: rgba(255, 255, 255, 0.2); + } + + li { + padding: 0 10px; + border-right: 1px solid var(--hard-border-color); + flex-shrink: 0; + + &:hover { + cursor: pointer; + background-color: rgba(0, 0, 0, 0.1); + } + } +} + +.react-tabs__tab--selected { + font-weight: bold; + background-color: rgba(0, 0, 0, 0.1); +} + +.react-tabs__tab-panel--selected { + display: flex; +} diff --git a/website/src/playground/styles/index.scss b/website/src/playground/styles/index.scss index a54b077aa96..3b75209a543 100644 --- a/website/src/playground/styles/index.scss +++ b/website/src/playground/styles/index.scss @@ -1,7 +1,21 @@ -body.playground { - height: 100vh; +@import "../../styles/_variables"; +@import "../../styles/_mixins"; +@import "_code"; +@import "_diagnostics"; +@import "_results"; +@import "_tabs"; +@import "_settings"; + +html, body { + width: 100%; + height: 100%; +} + +body { display: flex; flex-direction: column; + overscroll-behavior-y: none; + height: 100vh; width: 100%; .loading { @@ -17,3 +31,71 @@ body.playground { display: flex; } } + +.docs-sidebar { + display: none; + + &.mobile-active { + display: block; + } +} + +#root { + display: flex; + width: 100%; + height: calc(100vh - 80px); +} + +h2 { + padding: 10px; + display: flex; + line-height: 18px; + font-size: 14px; + font-weight: normal; + box-shadow: inset 0 1px 0 var(--hard-border-color), inset 0 -1px 0 var(--hard-border-color); + background-color: rgba(255, 255, 255, 0.15); + + &.rome { + background-color: rgba($color-4, 30%); + } + + &.prettier { + background-color: rgba(#355F75, 40%); + } + + img { + vertical-align: middle; + height: 18px; + width: 18px; + margin-right: 10px; + } + + &.collapsible { + cursor: pointer; + + &:hover { + opacity: 0.9; + } + + &.collapsed { + opacity: 0.8; + } + } +} + +.code-pane, .results-pane, .settings-pane { + height: 100%; +} + +.code-pane, .results-pane { + width: 100%; + min-width: 0; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + width: 100%; +} diff --git a/website/src/playground/tabs/DiagnosticsConsoleTab.tsx b/website/src/playground/tabs/DiagnosticsConsoleTab.tsx new file mode 100644 index 00000000000..fc4451fa2eb --- /dev/null +++ b/website/src/playground/tabs/DiagnosticsConsoleTab.tsx @@ -0,0 +1,20 @@ +interface Props { + console: string; +} + +export default function DiagnosticsConsoleTab({ console }: Props) { + if (console === "") { + return
No diagnostics present
; + } + + return ( + <> +
+				
+			
+ + ); +} diff --git a/website/src/playground/tabs/DiagnosticsListTab.tsx b/website/src/playground/tabs/DiagnosticsListTab.tsx new file mode 100644 index 00000000000..17fe8528d6a --- /dev/null +++ b/website/src/playground/tabs/DiagnosticsListTab.tsx @@ -0,0 +1,107 @@ +import type { ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import type { Diagnostic } from "@rometools/wasm-web"; +import { EditorSelection } from "@codemirror/state"; +import infoIcon from "../../svg/info.svg"; +import errorIcon from "../../svg/error.svg"; +import warningIcon from "../../svg/warning.svg"; + +interface Props { + editorRef: React.RefObject; + diagnostics: Diagnostic[]; +} + +function renderDiagnosticMessage(diagnostic: Diagnostic) { + const parts: JSX.Element[] = []; + + for (let i = 0; i < diagnostic.message.length; i++) { + const part = diagnostic.message[i]!; + let text = part.content; + + // Capitalize diagnostic messages... + // TODO normalize this inside of rome itself + if (i === 0) { + text = text[0]?.toUpperCase() + text.slice(1); + } + + let content: JSX.Element = {text}; + + for (const elem of part.elements) { + if (elem === "Emphasis") { + content = {content}; + } else if (elem === "Underline") { + content = {content}; + } else if (elem === "Italic") { + content = {content}; + } + } + + parts.push(content); + } + + return parts; +} + +function DiagnosticIcon({ severity }: { severity: Diagnostic["severity"] }) { + switch (severity) { + case "Information": + return ; + + case "Warning": + return ; + + default: + return ; + } +} + +function DiagnosticListItem({ + editorRef, + diagnostic, +}: { + diagnostic: Diagnostic; + editorRef: React.RefObject; +}) { + const span = diagnostic.location?.span; + + function onClick() { + const view = editorRef.current?.view; + if (view === undefined) { + return; + } + + if (span === undefined) { + return; + } + + view.dispatch({ + scrollIntoView: true, + selection: EditorSelection.create([ + EditorSelection.range(span[0], span[1]), + EditorSelection.cursor(span[0]), + ]), + }); + } + + return ( +
  • + + {renderDiagnosticMessage(diagnostic)} +
  • + ); +} + +export default function DiagnosticsListTab({ editorRef, diagnostics }: Props) { + if (diagnostics.length === 0) { + return
    No diagnostics present
    ; + } + + return ( +
      + {diagnostics.map((diag, i) => { + return ( + + ); + })} +
    + ); +} diff --git a/website/src/playground/tabs/DiagnosticsTab.tsx b/website/src/playground/tabs/DiagnosticsTab.tsx deleted file mode 100644 index 21c75ecfdb7..00000000000 --- a/website/src/playground/tabs/DiagnosticsTab.tsx +++ /dev/null @@ -1,18 +0,0 @@ -interface Props { - errors: string; -} - -export default function DiagnosticsTab({ errors }: Props) { - if (errors === "") { - return
    No diagnostics present
    ; - } - - return ( -
    -			
    -		
    - ); -} diff --git a/website/src/playground/tabs/FormatterCodeTab.tsx b/website/src/playground/tabs/FormatterCodeTab.tsx index ca81f4362e1..c58bbd8a08f 100644 --- a/website/src/playground/tabs/FormatterCodeTab.tsx +++ b/website/src/playground/tabs/FormatterCodeTab.tsx @@ -3,9 +3,10 @@ import Collapsible from "../Collapsible"; import PrettierHeader from "../components/PrettierHeader"; import RomeHeader from "../components/RomeHeader"; import fastDiff from "fast-diff"; +import type { PrettierOutput } from "../types"; interface Props { - prettier: string; + prettier: PrettierOutput; rome: string; extensions: any[]; } @@ -46,7 +47,12 @@ export default function FormatterCodeTab({ prettier, extensions, }: Props) { - const hint = calculateHint(prettier, rome); + let hint; + if (prettier.type === "SUCCESS") { + hint = calculateHint(prettier.code, rome); + } else { + hint = Error; + } return ( <> @@ -67,12 +73,20 @@ export default function FormatterCodeTab({ } > - + {prettier.type === "ERROR" ? ( + + ) : ( + + )} ); diff --git a/website/src/playground/tabs/FormatterIRTab.tsx b/website/src/playground/tabs/FormatterIRTab.tsx index d82ab87eddd..469e0e93503 100644 --- a/website/src/playground/tabs/FormatterIRTab.tsx +++ b/website/src/playground/tabs/FormatterIRTab.tsx @@ -3,9 +3,10 @@ import Collapsible from "../Collapsible"; import PrettierHeader from "../components/PrettierHeader"; import RomeHeader from "../components/RomeHeader"; import { romeAst as RomeFormatterIr } from "lang-rome-formatter-ir"; +import type { PrettierOutput } from "../types"; interface Props { - prettier: string; + prettier: PrettierOutput; rome: string; } @@ -22,11 +23,15 @@ export default function FormatterIRTab({ rome, prettier }: Props) { /> }> - + {prettier.type === "ERROR" ? ( + + ) : ( + + )} ); diff --git a/website/src/playground/tabs/SettingsTab.tsx b/website/src/playground/tabs/SettingsTab.tsx new file mode 100644 index 00000000000..acba54cef98 --- /dev/null +++ b/website/src/playground/tabs/SettingsTab.tsx @@ -0,0 +1,586 @@ +import { + IndentStyle, + PlaygroundSettings, + PlaygroundState, + QuoteProperties, + QuoteStyle, + SourceType, + TrailingComma, +} from "../types"; +import type { Dispatch, SetStateAction } from "react"; +import React, { useState } from "react"; +import { + modifyFilename, + createPlaygroundSettingsSetter, + isJSXFilename, + isScriptFilename, + isTypeScriptFilename, + classnames, + getFileState, +} from "../utils"; + +export interface SettingsTabProps { + state: PlaygroundState; + setPlaygroundState: Dispatch>; + onReset: () => void; +} + +export default function SettingsTab({ + setPlaygroundState, + onReset, + state: { + singleFileMode, + currentFile, + files, + settings: { + lineWidth, + indentWidth, + indentStyle, + quoteStyle, + quoteProperties, + trailingComma, + enabledNurseryRules, + enabledLinting, + }, + }, +}: SettingsTabProps) { + const setLineWidth = createPlaygroundSettingsSetter( + setPlaygroundState, + "lineWidth", + ); + const setIndentWidth = createPlaygroundSettingsSetter( + setPlaygroundState, + "indentWidth", + ); + const setIndentStyle = createPlaygroundSettingsSetter( + setPlaygroundState, + "indentStyle", + ); + const setQuoteStyle = createPlaygroundSettingsSetter( + setPlaygroundState, + "quoteStyle", + ); + const setQuoteProperties = createPlaygroundSettingsSetter( + setPlaygroundState, + "quoteProperties", + ); + const setTrailingComma = createPlaygroundSettingsSetter( + setPlaygroundState, + "trailingComma", + ); + const setEnabledNurseryRules = createPlaygroundSettingsSetter( + setPlaygroundState, + "enabledNurseryRules", + ); + const setEnabledLinting = createPlaygroundSettingsSetter( + setPlaygroundState, + "enabledLinting", + ); + + function setCurrentFilename(newFilename: string) { + setPlaygroundState((state) => { + if (state.currentFile === newFilename) { + return state; + } + + const files: PlaygroundState["files"] = { + ...state.files, + [newFilename]: state.files[state.currentFile]!, + }; + delete files[state.currentFile]; + return { + ...state, + currentFile: newFilename, + files, + }; + }); + } + + function createFile(filename: string) { + if ( + !isScriptFilename(filename) && + !isJSXFilename(filename) && + !isTypeScriptFilename(filename) + ) { + filename = `${filename}.js`; + } + + setPlaygroundState((state) => ({ + ...state, + currentFile: filename, + files: { + ...state.files, + [filename]: getFileState({ files: {} }, filename), + }, + })); + } + + function setCurrentFile(currentFile: string) { + setPlaygroundState((state) => ({ + ...state, + currentFile, + })); + } + + function toggleSingleFileMode() { + setPlaygroundState((state) => ({ + ...state, + singleFileMode: !state.singleFileMode, + })); + } + + return ( +
    +
    + + +
    + + {!singleFileMode && ( + + )} + + + +
    + ); +} + +function FileView({ + currentFile, + createFile, + setCurrentFile, + files, +}: { + createFile: (filename: string) => void; + setCurrentFile: (filename: string) => void; + currentFile: string; + files: string[]; +}) { + const [isCreatingFile, setCreatingFile] = useState(false); + + return ( +
    +

    + Files + +

    + +
      + {files.map((filename, i) => { + return ( + { + setCurrentFile(filename); + }} + /> + ); + })} +
    + + {isCreatingFile && ( + { + createFile(filename); + setCreatingFile(false); + }} + onCancel={() => setCreatingFile(false)} + /> + )} +
    + ); +} + +function FileViewItem({ + filename, + isActive, + onClick, +}: { + filename: string; + isActive: boolean; + onClick: () => void; +}) { + return ( +
  • + {filename} +
  • + ); +} + +function NewFileInput({ + createFile, + onCancel, +}: { + createFile: (filename: string) => void; + onCancel: () => void; +}) { + const [filename, setFilename] = useState(""); + + function onKeyDown(e: React.KeyboardEvent) { + if (e.key === "Escape") { + onCancel(); + } + + if (e.key === "Enter") { + createFile(filename); + } + } + + function onBlur() { + if (filename === "") { + onCancel(); + } else { + createFile(filename); + } + } + + function onChange(e: React.ChangeEvent) { + setFilename(e.target.value); + } + + return ( + + ); +} + +function SyntaxSettings({ + filename, + setFilename, +}: { + filename: string; + setFilename: (filename: string) => void; +}) { + const isScript = isScriptFilename(filename); + + return ( + <> +

    Syntax

    +
    +
    + + +
    + +
    + { + setFilename( + modifyFilename(filename, { + jsx: isJSXFilename(filename), + typescript: e.target.checked, + script: false, + }), + ); + }} + disabled={isScript} + /> + +
    + +
    + { + setFilename( + modifyFilename(filename, { + jsx: e.target.checked, + typescript: isTypeScriptFilename(filename), + script: false, + }), + ); + }} + disabled={isScript} + /> + +
    +
    + + ); +} + +function FormatterSettings({ + lineWidth, + setLineWidth, + indentStyle, + setIndentStyle, + indentWidth, + setIndentWidth, + quoteStyle, + setQuoteStyle, + quoteProperties, + setQuoteProperties, + trailingComma, + setTrailingComma, +}: { + lineWidth: number; + setLineWidth: (value: number) => void; + indentStyle: IndentStyle; + setIndentStyle: (value: IndentStyle) => void; + indentWidth: number; + setIndentWidth: (value: number) => void; + quoteStyle: QuoteStyle; + setQuoteStyle: (value: QuoteStyle) => void; + quoteProperties: QuoteProperties; + setQuoteProperties: (value: QuoteProperties) => void; + trailingComma: TrailingComma; + setTrailingComma: (value: TrailingComma) => void; +}) { + return ( + <> +

    Formatter

    +
    + + +
    + + +
    + +
    + + { + setIndentWidth(parseInt(e.target.value)); + }} + /> +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + + ); +} + +function LinterSettings({ + enabledNurseryRules, + setEnabledNurseryRules, + enabledLinting, + setEnabledLinting, +}: { + enabledNurseryRules: boolean; + setEnabledNurseryRules: (value: boolean) => void; + enabledLinting: boolean; + setEnabledLinting: (value: boolean) => void; +}) { + return ( + <> +

    Linter

    +
    +
    + setEnabledLinting(e.target.checked)} + /> + +
    +
    + setEnabledNurseryRules(e.target.checked)} + /> + +
    +
    + + ); +} + +function LineWidthInput({ + lineWidth, + setLineWidth, +}: { + lineWidth: number; + setLineWidth: (lineWidth: number) => void; +}) { + const [showCustom, setShowCustom] = useState( + lineWidth !== 80 && lineWidth !== 120, + ); + + return ( + <> +
    + + +
    +
    + + + + + +
    + + {showCustom && ( + { + setLineWidth(parseInt(e.target.value)); + }} + /> + )} +
    +
    + + ); +} diff --git a/website/src/playground/types.ts b/website/src/playground/types.ts index 24aba12f19e..349c2e50abb 100644 --- a/website/src/playground/types.ts +++ b/website/src/playground/types.ts @@ -1,3 +1,4 @@ +import type { Diagnostic } from "@rometools/wasm-web"; import type { Dispatch, SetStateAction } from "react"; import type { parser } from "codemirror-lang-rome-ast"; @@ -5,98 +6,136 @@ export enum IndentStyle { Tab = "tab", Space = "space", } + export enum SourceType { Module = "module", Script = "script", } + export enum QuoteStyle { Double = "double", Single = "single", } + export enum QuoteProperties { AsNeeded = "as-needed", Preserve = "preserve", } + export enum TrailingComma { All = "all", ES5 = "es5", None = "none", } + export enum LoadingState { Loading, Success, Error, } +export type PrettierOutput = + | { type: "SUCCESS"; code: string; ir: string } + | { type: "ERROR"; stack: string }; + +export const emptyPrettierOutput: PrettierOutput = { + type: "SUCCESS", + code: "", + ir: "", +}; + export interface RomeOutput { - ast: string; - cst: string; - errors: string; - formatted_code: string; - formatter_ir: string; - control_flow_graph: string; + syntax: { + ast: string; + cst: string; + }; + diagnostics: { + console: string; + list: Diagnostic[]; + }; + formatter: { + code: string; + ir: string; + }; + analysis: { + controlFlowGraph: string; + }; } -export interface PlaygroundState { - code: string; +export const emptyRomeOutput: RomeOutput = { + syntax: { + ast: "", + cst: "", + }, + diagnostics: { + console: "", + list: [], + }, + formatter: { + code: "", + ir: "", + }, + analysis: { + controlFlowGraph: "", + }, +}; + +export interface PlaygroundSettings { lineWidth: number; indentStyle: IndentStyle; indentWidth: number; quoteStyle: QuoteStyle; quoteProperties: QuoteProperties; - sourceType: SourceType; trailingComma: TrailingComma; - typescript: boolean; - jsx: boolean; - cursorPosition: number; enabledNurseryRules: boolean; - enabledLinting: boolean; + enabledLinting: boolean; } -// change `lineWidth` and `indentWidth` to string type, just to fits our `usePlaygroundState` fallback usage -export type RomeConfiguration = Omit< - PlaygroundState, - "code" | "lineWidth" | "indentWidth" -> & { lineWidth: string; indentWidth: string }; - -export const defaultRomeConfig: RomeConfiguration = { - lineWidth: "80", - indentWidth: "2", - indentStyle: IndentStyle.Tab, - quoteStyle: QuoteStyle.Double, - quoteProperties: QuoteProperties.AsNeeded, - sourceType: SourceType.Module, - trailingComma: TrailingComma.All, - typescript: true, - jsx: true, +export interface PlaygroundFileState { + content: string; + prettier: PrettierOutput; + rome: RomeOutput; +} + +export interface PlaygroundState { + currentFile: string; + singleFileMode: boolean; + tab: string; + cursorPosition: number; + files: Record; + settings: PlaygroundSettings; +} + +export const defaultPlaygroundState: PlaygroundState = { cursorPosition: 0, - enabledNurseryRules: true, - enabledLinting: true, + tab: "formatter", + currentFile: "main.tsx", + singleFileMode: false, + files: { + "main.tsx": { + content: "", + prettier: emptyPrettierOutput, + rome: emptyRomeOutput, + }, + }, + settings: { + lineWidth: 80, + indentWidth: 2, + indentStyle: IndentStyle.Tab, + quoteStyle: QuoteStyle.Double, + quoteProperties: QuoteProperties.AsNeeded, + trailingComma: TrailingComma.All, + enabledNurseryRules: true, + enabledLinting: true, + }, }; export interface PlaygroundProps { setPlaygroundState: Dispatch>; resetPlaygroundState: () => void; playgroundState: PlaygroundState; - prettierOutput: { code: string; ir: string }; - romeOutput: RomeOutput; } -export type PlaygroundSettings = Pick< - PlaygroundState, - | "lineWidth" - | "indentWidth" - | "indentStyle" - | "quoteStyle" - | "quoteProperties" - | "sourceType" - | "trailingComma" - | "typescript" - | "jsx" - | "enabledNurseryRules" - | "enabledLinting" ->; - export type Tree = ReturnType; type RangeMapKey = [number, number]; type RangeMapValue = [number, number]; diff --git a/website/src/playground/utils.ts b/website/src/playground/utils.ts index 890ab97cfbf..3dafe6de6ef 100644 --- a/website/src/playground/utils.ts +++ b/website/src/playground/utils.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction, useEffect, useState } from "react"; -import prettier, { Options } from "prettier"; +import prettier, { Options as PrettierOptions } from "prettier"; import type { ThemeName } from "../frontend-scripts/util"; // @ts-ignore import parserBabel from "prettier/esm/parser-babel"; @@ -8,9 +8,13 @@ import { PlaygroundState, QuoteStyle, QuoteProperties, - RomeConfiguration, - SourceType, TrailingComma, + defaultPlaygroundState, + PrettierOutput, + PlaygroundSettings, + emptyPrettierOutput, + emptyRomeOutput, + PlaygroundFileState, } from "./types"; import { getCurrentTheme } from "../frontend-scripts/util"; @@ -103,81 +107,117 @@ export function createLocalStorage(name: string): { const lastSearchStore = createLocalStorage("last-search"); -export function usePlaygroundState( - defaultRomeConfig: RomeConfiguration, -): [PlaygroundState, Dispatch>, () => void] { +const FILE_QUERY_KEY_REGEX = /^files\.(.*?)$/; + +export function usePlaygroundState(): [ + PlaygroundState, + Dispatch>, + () => void, +] { const searchQuery = window.location.search === "" ? lastSearchStore.get() ?? "" : window.location.search; - const initialSearchParams = new URLSearchParams(searchQuery); - - const initState = (searchParams: URLSearchParams) => ({ - code: - window.location.hash !== "#" - ? decodeCode(window.location.hash.substring(1)) - : "", - lineWidth: parseInt( - searchParams.get("lineWidth") ?? defaultRomeConfig.lineWidth, - ), - indentStyle: - (searchParams.get("indentStyle") as IndentStyle) ?? - defaultRomeConfig.indentStyle, - quoteStyle: - (searchParams.get("quoteStyle") as QuoteStyle) ?? - defaultRomeConfig.quoteStyle, - quoteProperties: - (searchParams.get("quoteProperties") as QuoteProperties) ?? - defaultRomeConfig.quoteProperties, - trailingComma: - (searchParams.get("trailingComma") as TrailingComma) ?? - defaultRomeConfig.trailingComma, - indentWidth: parseInt( - searchParams.get("indentWidth") ?? defaultRomeConfig.indentWidth, - ), - typescript: - searchParams.get("typescript") === "true" || defaultRomeConfig.typescript, - jsx: searchParams.get("jsx") === "true" || defaultRomeConfig.jsx, - sourceType: - (searchParams.get("sourceType") as SourceType) ?? - defaultRomeConfig.sourceType, - cursorPosition: 0, - enabledNurseryRules: - searchParams.get("enabledNurseryRules") === "true" || - defaultRomeConfig.enabledNurseryRules, - enabledLinting: - searchParams.get("enabledLinting") === "true" || - defaultRomeConfig.enabledLinting, - }); - const [playgroundState, setPlaygroundState] = useState( - initState(initialSearchParams), + + function initState( + searchParams: URLSearchParams, + ignoreFiles: boolean, + ): PlaygroundState { + let singleFileMode = true; + let hasFiles = false; + let files: PlaygroundState["files"] = {}; + + if (!ignoreFiles) { + // Populate files + for (const [key, value] of searchParams) { + const match = key.match(FILE_QUERY_KEY_REGEX); + if (match != null) { + const filename = match[1]!; + files[filename] = { + content: decodeCode(value), + rome: emptyRomeOutput, + prettier: emptyPrettierOutput, + }; + singleFileMode = false; + hasFiles = true; + } + } + + // Single file mode + if (searchParams.get("code")) { + const ext = getExtension({ + typescript: searchParams.get("typescript") === "true", + jsx: searchParams.get("jsx") === "true", + script: searchParams.get("script") === "true", + }); + files[`main.${ext}`] = { + content: decodeCode(searchParams.get("code") ?? ""), + rome: emptyRomeOutput, + prettier: emptyPrettierOutput, + }; + hasFiles = true; + } + } + + if (!hasFiles) { + files = defaultPlaygroundState.files; + } + + return { + cursorPosition: 0, + tab: searchParams.get("tab") ?? "formatter", + singleFileMode, + currentFile: Object.keys(files)[0] ?? "main.js", + files, + settings: { + lineWidth: parseInt( + searchParams.get("lineWidth") ?? + String(defaultPlaygroundState.settings.lineWidth), + ), + indentStyle: + (searchParams.get("indentStyle") as IndentStyle) ?? + defaultPlaygroundState.settings.indentStyle, + quoteStyle: + (searchParams.get("quoteStyle") as QuoteStyle) ?? + defaultPlaygroundState.settings.quoteStyle, + quoteProperties: + (searchParams.get("quoteProperties") as QuoteProperties) ?? + defaultPlaygroundState.settings.quoteProperties, + trailingComma: + (searchParams.get("trailingComma") as TrailingComma) ?? + defaultPlaygroundState.settings.trailingComma, + indentWidth: parseInt( + searchParams.get("indentWidth") ?? + String(defaultPlaygroundState.settings.indentWidth), + ), + enabledNurseryRules: + searchParams.get("enabledNurseryRules") === "true" || + defaultPlaygroundState.settings.enabledNurseryRules, + enabledLinting: + searchParams.get("enabledLinting") === "true" || + defaultPlaygroundState.settings.enabledLinting, + }, + }; + } + + const [playgroundState, setPlaygroundState] = useState(() => + initState(new URLSearchParams(searchQuery), window.location.search === ""), ); function resetPlaygroundState() { - setPlaygroundState(initState(new URLSearchParams(""))); + setPlaygroundState(initState(new URLSearchParams(""), false)); } useEffect(() => { - setPlaygroundState(initState(initialSearchParams)); - }, [defaultRomeConfig]); - - useEffect(() => { - const { code } = playgroundState; - const rawQueryParams: Record = { - ...playgroundState, - cursorPosition: undefined, + ...playgroundState.settings, }; - if (rawQueryParams.code === "") { - rawQueryParams.code = undefined; - } - // Eliminate default values const queryStringObj: Record = {}; for (const key in rawQueryParams) { const defaultValue = String( - defaultRomeConfig[key as keyof typeof defaultRomeConfig], + defaultPlaygroundState.settings[key as keyof PlaygroundSettings], ); const rawValue = rawQueryParams[key]; const value = String(rawValue); @@ -187,12 +227,41 @@ export function usePlaygroundState( } } + if ( + playgroundState.singleFileMode && + Object.keys(playgroundState.files).length === 1 + ) { + // Single file mode + const code = getCurrentCode(playgroundState); + if (code) { + queryStringObj.code = encodeCode(code); + } + + if (!isTypeScriptFilename(playgroundState.currentFile)) { + queryStringObj.typescript = "false"; + } + + if (!isJSXFilename(playgroundState.currentFile)) { + queryStringObj.jsx = "false"; + } + + if (isScriptFilename(playgroundState.currentFile)) { + queryStringObj.script = "true"; + } + } else { + // Populate files + for (const filename in playgroundState.files) { + const content = playgroundState.files[filename]?.content ?? ""; + queryStringObj[`files.${filename}`] = encodeCode(content); + } + } + const queryString = new URLSearchParams(queryStringObj).toString(); lastSearchStore.set(queryString); let url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`; if (queryString !== "") { - url += `?${queryString}#${encodeCode(code)}`; + url += `?${queryString}`; } window.history.replaceState({ path: url }, "", url); @@ -201,14 +270,38 @@ export function usePlaygroundState( return [playgroundState, setPlaygroundState, resetPlaygroundState]; } -export function createSetter( +export function getCurrentCode(state: PlaygroundState): string { + return state.files[state.currentFile]?.content ?? ""; +} + +export function getFileState( + state: Pick, + filename: string, +): PlaygroundFileState { + return ( + state.files[filename] ?? { + content: "", + rome: emptyRomeOutput, + prettier: emptyPrettierOutput, + } + ); +} + +export function createPlaygroundSettingsSetter< + Key extends keyof PlaygroundSettings, +>( setPlaygroundState: Dispatch>, field: Key, -): (value: PlaygroundState[Key]) => void { - return function (param: PlaygroundState[typeof field]) { +): (value: PlaygroundSettings[Key]) => void { + return function (param: PlaygroundSettings[typeof field]) { setPlaygroundState((state) => { - let nextState = { ...state, [field]: param }; - return nextState; + return { + ...state, + settings: { + ...state.settings, + [field]: param, + }, + }; }); }; } @@ -224,9 +317,9 @@ export function formatWithPrettier( quoteProperties: QuoteProperties; trailingComma: TrailingComma; }, -): { code: string; ir: string } { +): PrettierOutput { try { - const prettierOptions: Options = { + const prettierOptions: PrettierOptions = { useTabs: options.indentStyle === IndentStyle.Tab, tabWidth: options.indentWidth, printWidth: options.lineWidth, @@ -238,7 +331,7 @@ export function formatWithPrettier( }; // @ts-ignore - let debug = prettier.__debug; + const debug = prettier.__debug; const document = debug.printToDoc(code, prettierOptions); // formatDoc must be before printDocToString because printDocToString mutates the document and breaks the ir @@ -246,15 +339,29 @@ export function formatWithPrettier( parser: "babel", plugins: [parserBabel], }); + const formattedCode = debug.printDocToString( document, prettierOptions, ).formatted; - return { code: formattedCode, ir }; + + return { + type: "SUCCESS", + code: formattedCode, + ir, + }; } catch (err: any) { - console.error(err); - const code = err.toString(); - return { code, ir: `Error: Invalid code\n${err.message}` }; + if (err instanceof SyntaxError) { + return { + type: "ERROR", + stack: err.message, + }; + } else { + return { + type: "ERROR", + stack: err.stack, + }; + } } } @@ -273,7 +380,11 @@ export function encodeCode(code: string): string { } export function decodeCode(encoded: string): string { - return fromBinary(atob(encoded)); + try { + return fromBinary(atob(encoded)); + } catch { + return ""; + } } // convert a Unicode string to a string in which @@ -304,3 +415,69 @@ function fromBinary(binary: string) { } return result; } + +export function classnames(...names: (undefined | boolean | string)[]): string { + let out = ""; + for (const name of names) { + if (name === undefined || typeof name === "boolean") { + continue; + } + + if (out !== "") { + out += " "; + } + out += name; + } + return out; +} + +export function isTypeScriptFilename(filename: string): boolean { + return filename.endsWith(".ts") || filename.endsWith(".tsx"); +} + +export function isJSXFilename(filename: string): boolean { + return filename.endsWith(".tsx") || filename.endsWith(".jsx"); +} + +export function isScriptFilename(filename: string): boolean { + return filename.endsWith(".js"); +} + +export function modifyFilename( + filename: string, + opts: ExtensionOptions, +): string { + const ext = getExtension(opts); + const parts = filename.split("."); + parts.pop(); + parts.push(ext); + return parts.join("."); +} + +type ExtensionOptions = { + jsx: boolean; + typescript: boolean; + script: boolean; +}; + +export function getExtension(opts: ExtensionOptions): string { + let ext = ""; + + if (opts.script) { + ext = "js"; + } else { + ext = "mjs"; + } + + if (opts.typescript) { + if (opts.jsx) { + ext = "tsx"; + } else { + ext = "ts"; + } + } else if (opts.jsx) { + ext = "jsx"; + } + + return ext; +} diff --git a/website/src/playground/workers/prettierWorker.ts b/website/src/playground/workers/prettierWorker.ts index 27470cccd2c..7b5facfc83f 100644 --- a/website/src/playground/workers/prettierWorker.ts +++ b/website/src/playground/workers/prettierWorker.ts @@ -1,11 +1,17 @@ import { formatWithPrettier } from "../utils"; -import type { PlaygroundState } from "../types"; +import { defaultPlaygroundState, PlaygroundSettings } from "../types"; + +let settings = defaultPlaygroundState.settings; self.addEventListener("message", (e) => { switch (e.data.type) { + case "updateSettings": { + settings = e.data.settings as PlaygroundSettings; + break; + } + case "format": { const { - code, lineWidth, indentStyle, indentWidth, @@ -13,7 +19,10 @@ self.addEventListener("message", (e) => { quoteProperties, typescript: isTypeScript, trailingComma, - } = e.data.playgroundState as PlaygroundState; + } = settings; + const code = e.data.code as string; + const filename = e.data.filename as string; + const prettierOutput = formatWithPrettier(code, { lineWidth, indentStyle, @@ -26,6 +35,7 @@ self.addEventListener("message", (e) => { self.postMessage({ type: "formatted", + filename, prettierOutput, }); diff --git a/website/src/playground/workers/romeWorker.ts b/website/src/playground/workers/romeWorker.ts index e45a313bb0c..ba44405607a 100644 --- a/website/src/playground/workers/romeWorker.ts +++ b/website/src/playground/workers/romeWorker.ts @@ -8,6 +8,7 @@ import init, { import { IndentStyle, LoadingState, + PlaygroundSettings, PlaygroundState, QuoteProperties, QuoteStyle, @@ -16,37 +17,25 @@ import { } from "../types"; let workspace: Workspace | null = null; +let fileCounter = 0; -const PATH_SCRIPT = { - path: "main.js", - id: 0, +type File = { + filename: string; + id: number; + content: string; + version: number; }; -const PATHS_MODULE = [ - { path: "main.mjs", id: 1 }, - { path: "main.jsx", id: 2 }, - { path: "main.ts", id: 3 }, - { path: "main.tsx", id: 4 }, -]; - -function getPathForType( - sourceType: SourceType, - isTypeScript: boolean, - isJsx: boolean, -): RomePath { - if (sourceType === SourceType.Script) { - return PATH_SCRIPT; - } +const files: Map = new Map(); - return PATHS_MODULE[Number(isTypeScript) * 2 + Number(isJsx)]!; -} +let configuration: undefined | Configuration; -type CurrentFile = { - path: RomePath; - version: number; -}; - -let currentFile: CurrentFile | null = null; +function getPathForFile(file: File) { + return { + path: file.filename, + id: file.id, + }; +} self.addEventListener("message", async (e) => { switch (e.data.type) { @@ -65,30 +54,24 @@ self.addEventListener("message", async (e) => { break; } - case "format": { + case "updateSettings": { if (!workspace) { console.error("Workspace was not initialized"); break; } - const playgroundState: PlaygroundState = e.data.playgroundState; const { - code, lineWidth, indentStyle, indentWidth, quoteStyle, quoteProperties, - typescript: isTypeScript, - jsx: isJsx, - sourceType, - cursorPosition, enabledNurseryRules, - enabledLinting, + enabledLinting, trailingComma, - } = playgroundState; + } = e.data.settings as PlaygroundSettings; - const configuration: Configuration = { + configuration = { formatter: { enabled: true, formatWithErrors: true, @@ -96,9 +79,11 @@ self.addEventListener("message", async (e) => { indentStyle: indentStyle === IndentStyle.Tab ? "tab" : "space", indentSize: indentWidth, }, + linter: { enabled: enabledLinting, }, + javascript: { formatter: { quoteStyle: quoteStyle === QuoteStyle.Double ? "double" : "single", @@ -110,6 +95,7 @@ self.addEventListener("message", async (e) => { }, }, }; + if (enabledNurseryRules) { configuration.linter = { enabled: enabledLinting, @@ -121,62 +107,80 @@ self.addEventListener("message", async (e) => { }, }; } + workspace.updateSettings({ configuration, }); + break; + } + + case "update": { + if (!workspace) { + console.error("Workspace was not initialized"); + break; + } + + const { filename, code, cursorPosition } = e.data; - const path = getPathForType(sourceType, isTypeScript, isJsx); - if (currentFile && currentFile?.path === path) { - workspace.changeFile({ - path, - version: currentFile.version++, + let file = files.get(filename); + if (file === undefined) { + file = { + filename, + version: 0, + content: code, + id: fileCounter++, + }; + + workspace.openFile({ + path: getPathForFile(file), + version: 0, content: code, }); } else { - if (currentFile) { - workspace.closeFile({ - path: currentFile.path, - }); - } - - currentFile = { - path, - version: 0, + file = { + filename, + id: file.id, + version: file.version + 1, + content: code, }; workspace.openFile({ - path, - version: currentFile.version++, + path: getPathForFile(file), + version: file.version, content: code, }); } + files.set(filename, file); + const path = getPathForFile(file); - const syntax_tree = workspace.getSyntaxTree({ + const syntaxTree = workspace.getSyntaxTree({ path, }); - const control_flow_graph = workspace.getControlFlowGraph({ + + const controlFlowGraph = workspace.getControlFlowGraph({ path, cursor: cursorPosition, }); - const formatter_ir = workspace.getFormatterIr({ + + const formatterIr = workspace.getFormatterIr({ path, }); const categories: RuleCategories = []; - if (configuration.formatter?.enabled) { - categories.push("Syntax"); + if (configuration?.formatter?.enabled) { + categories.push("Syntax"); } - if (configuration.linter?.enabled) { - categories.push("Lint"); + if (configuration?.linter?.enabled) { + categories.push("Lint"); } - const diagnostics = workspace.pullDiagnostics({ + const diagnosticsResult = workspace.pullDiagnostics({ path, categories: categories, max_diagnostics: Number.MAX_SAFE_INTEGER, }); const printer = new DiagnosticPrinter(path.path, code); - for (const diag of diagnostics.diagnostics) { + for (const diag of diagnosticsResult.diagnostics) { printer.print(diag); } @@ -185,16 +189,28 @@ self.addEventListener("message", async (e) => { }); const romeOutput: RomeOutput = { - ast: syntax_tree.ast, - cst: syntax_tree.cst, - errors: printer.finish(), - formatted_code: printed.code, - formatter_ir, - control_flow_graph, + syntax: { + // Replace 4 spaced indentation with 2 + // TODO replace this in Rome itself + ast: syntaxTree.ast.replace(/ /g, " "), + cst: syntaxTree.cst, + }, + diagnostics: { + console: printer.finish(), + list: diagnosticsResult.diagnostics, + }, + formatter: { + code: printed.code, + ir: formatterIr, + }, + analysis: { + controlFlowGraph, + }, }; self.postMessage({ - type: "formatted", + type: "updated", + filename, romeOutput, }); break; diff --git a/website/src/styles/index.scss b/website/src/styles/index.scss index 4991a48d7bd..ec8a1440557 100644 --- a/website/src/styles/index.scss +++ b/website/src/styles/index.scss @@ -32,6 +32,7 @@ --table-even-row-color: rgba(0, 0, 0, 0.05); --inline-code-background-color: rgba(0, 0, 0, 0.08); --label-font-color: #{$gray-4}; + --playground-border: rgba(39, 39, 42, 0.15); } @include dark-mode { @@ -52,4 +53,5 @@ --search-input-background-color: rgba(0, 0, 0, 0.09); --table-even-row-color: rgba(0, 0, 0, 0.09); --inline-code-background-color: rgba(68, 73, 80, 0.7); + --playground-border: #1a1a1c; } diff --git a/website/src/styles/playground.scss b/website/src/styles/playground.scss deleted file mode 100644 index 00153e3cf0a..00000000000 --- a/website/src/styles/playground.scss +++ /dev/null @@ -1,363 +0,0 @@ -@import "_variables"; -@import "_mixins"; - -body { - display: flex; - flex-direction: column; - overscroll-behavior-y: none; -} - -#root { - display: flex; - width: 100%; - height: calc(100vh - 80px); -} - -html, body { - width: 100%; - height: 100%; -} - -.code-pane, .preview-pane, .settings-pane { - height: 100%; -} - -.code-pane, .preview-pane { - width: 100%; - min-width: 0; -} - -.settings-pane { - flex-shrink: 0; - background-color: var(--container-color); - overflow: auto; - font-size: 14px; - display: flex; - - @include dark-mode { - background-color: rgba(255, 255, 255, 0.2); - } - - .fields { - width: 250px; - } - - .collapser { - width: 5px; - height: 100%; - background-color: var(--hard-border-color); - flex-shrink: 0; - opacity: 0.5; - cursor: w-resize; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - gap: 5px; - - .dot { - width: 3px; - height: 3px; - background-color: rgba(#000, 50%); - border-radius: 50%; - } - - &.collapsed { - width: 10px; - cursor: e-resize; - - .dot { - width: 5px; - height: 5px; - } - } - - &:hover { - opacity: 1; - } - } - - section { - padding: 10px; - } - - input[type=checkbox] { - margin-right: 10px; - } - - select, input[type=number] { - width: 100px; - } - - select, input[type=number], .input-container { - margin-left: auto; - } - - .input-container { - display: flex; - flex-direction: column; - } - - input[type=text], input[type=number] { - border-radius: 6px; - border: 1px solid rgba(0, 0, 0, 0.2); - padding: 2px 10px; - - @include dark-mode { - color: inherit; - background-color: rgba(255, 255, 255, 0.3); - border-color: rgba(0, 0, 0, 0.5); - } - } - - select { - @include button; - border-radius: 6px; - padding: 2px 6px; - color: inherit; - border: 1px solid rgba(0, 0, 0, 0.2); - - @include dark-mode { - border-color: rgba(255, 255, 255, 0.1); - } - } - - - button { - @include button; - border-radius: 0; - border: 1px solid rgba(0, 0, 0, 0.2); - padding: 2px 6px; - color: inherit; - border-radius: 6px; - - @include dark-mode { - border-color: rgba(255, 255, 255, 0.1); - } - - &:disabled { - opacity: 0.5; - font-weight: bold; - } - } - - .button-group button { - border-radius: 0; - - &:disabled:not(:last-child) { - border-right: none; - } - - &:not(:first-child):not(&:disabled + button) { - border-left: none; - } - - &:first-child { - border-top-left-radius: 6px; - border-bottom-left-radius: 6px; - } - - &:last-child { - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; - } - } - - .field-row { - display: flex; - line-height: 25px; - margin-bottom: 10px; - - &.disabled { - opacity: 0.4; - - &, * { - cursor: not-allowed; - } - } - } -} - -.react-tabs__tab-panel .settings-pane { - width: 100%; - height: 100%; - background-color: rgba(255, 255, 255, 0.1) !important; - border-top: 1px solid var(--hard-border-color); -} - -.preview-pane { - border-left: 1px solid var(--hard-border-color); - display: flex; - flex-direction: column; -} - -.mermaid { - border-top: 1px solid var(--hard-border-color); - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - background-color: rgba(255, 255, 255, 0.1); -} - -.playground-body { - .docs-sidebar { - display: none; - - &.mobile-active { - display: block; - } - } -} - -#root h2 { - padding: 10px; - display: flex; - line-height: 18px; - font-size: inherit; - font-weight: normal; - box-shadow: inset 0 1px 0 var(--hard-border-color), inset 0 -1px 0 var(--hard-border-color); - background-color: rgba(255, 255, 255, 0.15); - - &.rome { - background-color: rgba($color-4, 30%); - } - - &.prettier { - background-color: rgba(#355F75, 40%); - } - - img { - vertical-align: middle; - height: 18px; - width: 18px; - margin-right: 10px; - } - - &.collapsible { - cursor: pointer; - - &:hover { - background-color: rgba(0, 0, 0, 0.05); - } - } - - &.collapsed { - background-color: rgba(0, 0, 0, 0.05); - border-bottom: none; - } -} - -.loading { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - width: 100%; -} - -.react-tabs__tab-panel { - display: none; - overflow-y: auto; - flex-direction: column; - height: 100%; - - > pre[class*=language-] { - margin: 0; - height: 100%; - background-color: #282c34 !important; - } - - > iframe { - height: 100%; - } -} - -.collapsible-container { - display: flex; - flex-direction: column; - flex: 1; - height: 50%; - - &.collapsed { - height: auto; - } -} - -.diff-hint { - margin-left: auto; - - .insertions { - color: green; - } - - .deletions { - color: red; - } -} - -.collapsible-content { - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; -} - -.cm-theme-light, .cm-theme-dark { - height: 100%; - overflow: auto; -} - -.cm-editor { - height: 100%; -} - -.cm-focused { - outline: none !important; -} - -.empty-panel { - display: flex; - width: 100%; - height: 100%; - background-color: var(--background-color); - justify-content: center; - align-items: center; -} - -.react-tabs__tab-list { - display: flex; - flex-direction: row; - flex-grow: 0; - height: auto; - line-height: 40px; - white-space: nowrap; - overflow-x: auto; - overflow-y: hidden; - background-color: var(--container-color); - - @include dark-mode { - background-color: rgba(255, 255, 255, 0.2); - } - - li { - padding: 0 10px; - border-right: 1px solid var(--hard-border-color); - flex-shrink: 0; - - &:hover { - cursor: pointer; - background-color: rgba(0, 0, 0, 0.1); - } - } -} - -.react-tabs__tab--selected { - font-weight: bold; - background-color: rgba(0, 0, 0, 0.05); -} - -.react-tabs__tab-panel--selected { - display: flex; -} diff --git a/website/src/svg/error.svg b/website/src/svg/error.svg new file mode 100644 index 00000000000..b51cd82f738 --- /dev/null +++ b/website/src/svg/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/src/svg/info.svg b/website/src/svg/info.svg new file mode 100644 index 00000000000..83d52fec3f5 --- /dev/null +++ b/website/src/svg/info.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/src/svg/warning.svg b/website/src/svg/warning.svg new file mode 100644 index 00000000000..e7cde0d7a31 --- /dev/null +++ b/website/src/svg/warning.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3840a9a7716264050076ca887cde630220b615e4 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sun, 13 Nov 2022 20:41:24 -0600 Subject: [PATCH 2/4] Regenerate pnpm lock --- website/pnpm-lock.yaml | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index d5143a8392b..df40382b09d 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -18,7 +18,6 @@ specifiers: '@types/prettier': ^2.7.1 '@types/react': ^17.0.33 '@types/react-dom': ^17.0.10 - '@types/react-tabs': ^2.3.4 '@uiw/react-codemirror': ^4.12.4 '@vitejs/plugin-react': ^2.1.0 astro: ^1.6.7 @@ -34,7 +33,6 @@ specifiers: prettier: ^2.7.1 react: ^17.0.2 react-dom: ^17.0.2 - react-tabs: ^4.2.1 rehype-autolink-headings: ^6.1.1 rehype-slug: ^5.1.0 rehype-toc: ^3.0.2 @@ -63,7 +61,6 @@ dependencies: '@types/prettier': 2.7.1 '@types/react': 17.0.52 '@types/react-dom': 17.0.18 - '@types/react-tabs': 2.3.4 '@uiw/react-codemirror': 4.13.2_ai5dpkfsh6bew42tf7jao77pla '@vitejs/plugin-react': 2.2.0_vite@3.1.8 astro: 1.6.7_ajklay5k626t46b6fyghkbup3i @@ -79,7 +76,6 @@ dependencies: prettier: 2.7.1 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - react-tabs: 4.2.1_react@17.0.2 rehype-autolink-headings: 6.1.1 rehype-slug: 5.1.0 rehype-toc: 3.0.2 @@ -1292,12 +1288,6 @@ packages: '@types/react': 17.0.52 dev: false - /@types/react-tabs/2.3.4: - resolution: {integrity: sha512-HQzhKW+RF/7h14APw/2cu4Nnt+GmsTvfBKbFdn/NbYpb8Q+iB65wIkPHz4VRKDxWtOpNFpOUtzt5r0LRmQMfOA==} - dependencies: - '@types/react': 17.0.52 - dev: false - /@types/react/17.0.52: resolution: {integrity: sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==} dependencies: @@ -1792,11 +1782,6 @@ packages: engines: {node: '>=0.8'} dev: false - /clsx/1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - dev: false - /codemirror-lang-rome-ast/0.0.6: resolution: {integrity: sha512-2E4E4rNcj6XxLsVjCZJeOsdEXpfdgiejuLrbq6O4muXzf0KSv5X0ScQrs3LK5LmGwoQUC0AowYbfdj3lsaLWkQ==} dependencies: @@ -4912,14 +4897,6 @@ packages: sisteransi: 1.0.5 dev: false - /prop-types/15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - dev: false - /property-information/6.1.1: resolution: {integrity: sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==} dev: false @@ -4956,25 +4933,11 @@ packages: scheduler: 0.20.2 dev: false - /react-is/16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: false - /react-refresh/0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} dev: false - /react-tabs/4.2.1_react@17.0.2: - resolution: {integrity: sha512-nQcEN3KrAsSry6f9Jz2oyMQsnh+sLEy31YjlskL/mnI3KU/c7BeyD1VzHZmmcJ15UEFu12pYOXYkdTzZ0uyIbw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-0 || ^18.0.0 - dependencies: - clsx: 1.2.1 - prop-types: 15.8.1 - react: 17.0.2 - dev: false - /react/17.0.2: resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} engines: {node: '>=0.10.0'} From 8a8dcaff14ce8167e77848bf2aecc112bc32f8f8 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sun, 13 Nov 2022 20:42:37 -0600 Subject: [PATCH 3/4] Fix ts errors --- website/src/playground/Playground.tsx | 4 +--- website/src/playground/tabs/SettingsTab.tsx | 1 - website/src/playground/workers/prettierWorker.ts | 5 ++--- website/src/playground/workers/romeWorker.ts | 4 +--- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/website/src/playground/Playground.tsx b/website/src/playground/Playground.tsx index ba30827228b..d8f7a9a21c1 100644 --- a/website/src/playground/Playground.tsx +++ b/website/src/playground/Playground.tsx @@ -1,6 +1,4 @@ -import { - emptyPrettierOutput, - emptyRomeOutput, +import type { PlaygroundProps, RomeAstSyntacticData, } from "./types"; diff --git a/website/src/playground/tabs/SettingsTab.tsx b/website/src/playground/tabs/SettingsTab.tsx index acba54cef98..9bdec3e5d2f 100644 --- a/website/src/playground/tabs/SettingsTab.tsx +++ b/website/src/playground/tabs/SettingsTab.tsx @@ -1,6 +1,5 @@ import { IndentStyle, - PlaygroundSettings, PlaygroundState, QuoteProperties, QuoteStyle, diff --git a/website/src/playground/workers/prettierWorker.ts b/website/src/playground/workers/prettierWorker.ts index 7b5facfc83f..a74eaeb7608 100644 --- a/website/src/playground/workers/prettierWorker.ts +++ b/website/src/playground/workers/prettierWorker.ts @@ -1,4 +1,4 @@ -import { formatWithPrettier } from "../utils"; +import { formatWithPrettier, isTypeScriptFilename } from "../utils"; import { defaultPlaygroundState, PlaygroundSettings } from "../types"; let settings = defaultPlaygroundState.settings; @@ -17,7 +17,6 @@ self.addEventListener("message", (e) => { indentWidth, quoteStyle, quoteProperties, - typescript: isTypeScript, trailingComma, } = settings; const code = e.data.code as string; @@ -27,7 +26,7 @@ self.addEventListener("message", (e) => { lineWidth, indentStyle, indentWidth, - language: isTypeScript ? "ts" : "js", + language: isTypeScriptFilename(filename) ? "ts" : "js", quoteStyle, quoteProperties, trailingComma, diff --git a/website/src/playground/workers/romeWorker.ts b/website/src/playground/workers/romeWorker.ts index ba44405607a..6b095c37293 100644 --- a/website/src/playground/workers/romeWorker.ts +++ b/website/src/playground/workers/romeWorker.ts @@ -9,11 +9,9 @@ import { IndentStyle, LoadingState, PlaygroundSettings, - PlaygroundState, QuoteProperties, QuoteStyle, RomeOutput, - SourceType, } from "../types"; let workspace: Workspace | null = null; @@ -30,7 +28,7 @@ const files: Map = new Map(); let configuration: undefined | Configuration; -function getPathForFile(file: File) { +function getPathForFile(file: File): RomePath { return { path: file.filename, id: file.id, From e5e392b656ad87eb9dfc299e8de6c322d984f979 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sun, 13 Nov 2022 21:02:23 -0600 Subject: [PATCH 4/4] Fix --- website/src/frontend-scripts/util.ts | 3 +-- website/src/playground/Playground.tsx | 5 +---- website/src/playground/tabs/DiagnosticsListTab.tsx | 6 +++--- website/src/playground/tabs/SettingsTab.tsx | 14 +++++++++++--- website/src/playground/workers/romeWorker.ts | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/website/src/frontend-scripts/util.ts b/website/src/frontend-scripts/util.ts index 81adefb1704..2b698e543f4 100644 --- a/website/src/frontend-scripts/util.ts +++ b/website/src/frontend-scripts/util.ts @@ -8,8 +8,7 @@ export const matchesDark: undefined | MediaQueryList = export function getCurrentTheme(): ThemeName { let currentScheme = window.localStorage.getItem("data-theme"); if (currentScheme == null) { - currentScheme = - matchesDark !== undefined && matchesDark.matches ? "dark" : "light"; + currentScheme = matchesDark?.matches ? "dark" : "light"; } return currentScheme === "dark" ? "dark" : "light"; } diff --git a/website/src/playground/Playground.tsx b/website/src/playground/Playground.tsx index d8f7a9a21c1..2c9f44d9d24 100644 --- a/website/src/playground/Playground.tsx +++ b/website/src/playground/Playground.tsx @@ -1,7 +1,4 @@ -import type { - PlaygroundProps, - RomeAstSyntacticData, -} from "./types"; +import type { PlaygroundProps, RomeAstSyntacticData } from "./types"; import type { ReactCodeMirrorRef } from "@uiw/react-codemirror"; import CodeMirror from "./CodeMirror"; import type { ViewUpdate } from "@codemirror/view"; diff --git a/website/src/playground/tabs/DiagnosticsListTab.tsx b/website/src/playground/tabs/DiagnosticsListTab.tsx index 17fe8528d6a..1267802289b 100644 --- a/website/src/playground/tabs/DiagnosticsListTab.tsx +++ b/website/src/playground/tabs/DiagnosticsListTab.tsx @@ -44,13 +44,13 @@ function renderDiagnosticMessage(diagnostic: Diagnostic) { function DiagnosticIcon({ severity }: { severity: Diagnostic["severity"] }) { switch (severity) { case "Information": - return ; + return Info; case "Warning": - return ; + return Warning; default: - return ; + return Error; } } diff --git a/website/src/playground/tabs/SettingsTab.tsx b/website/src/playground/tabs/SettingsTab.tsx index 9bdec3e5d2f..b99a17ad38a 100644 --- a/website/src/playground/tabs/SettingsTab.tsx +++ b/website/src/playground/tabs/SettingsTab.tsx @@ -82,11 +82,13 @@ export default function SettingsTab({ return state; } + const { [state.currentFile]: _, ...otherFiles } = state.files; + const files: PlaygroundState["files"] = { - ...state.files, + ...otherFiles, [newFilename]: state.files[state.currentFile]!, }; - delete files[state.currentFile]; + return { ...state, currentFile: newFilename, @@ -97,6 +99,7 @@ export default function SettingsTab({ function createFile(filename: string) { if ( + // rome-ignore lint(complexity/useSimplifiedLogicExpression): Not sure how else to do this !isScriptFilename(filename) && !isJSXFilename(filename) && !isTypeScriptFilename(filename) @@ -233,7 +236,11 @@ function FileViewItem({ onClick: () => void; }) { return ( -
  • +
  • {filename}
  • ); @@ -273,6 +280,7 @@ function NewFileInput({ return ( { syntax: { // Replace 4 spaced indentation with 2 // TODO replace this in Rome itself - ast: syntaxTree.ast.replace(/ /g, " "), + ast: syntaxTree.ast.replace(/ {4}/g, " "), cst: syntaxTree.cst, }, diagnostics: {