Skip to content

Commit

Permalink
fix: Update React Editor component
Browse files Browse the repository at this point in the history
  • Loading branch information
robertoraggi committed Sep 11, 2023
1 parent 374a814 commit cf6cf4f
Show file tree
Hide file tree
Showing 13 changed files with 3,052 additions and 2,251 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
/build*/
out
.venv
3 changes: 3 additions & 0 deletions packages/cxx-storybook/.storybook/preview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.sb-main-fullscreen #storybook-root {
height: 100svh;
}
14 changes: 14 additions & 0 deletions packages/cxx-storybook/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import React from "react";
import type { Preview } from "@storybook/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./preview.css";

const queryClient = new QueryClient();

const preview: Preview = {
parameters: {
Expand All @@ -10,6 +15,15 @@ const preview: Preview = {
},
},
},
decorators: [
(Story) => {
return React.createElement(
QueryClientProvider,
{ client: queryClient },
React.createElement(Story)
);
},
],
};

export default preview;
4,643 changes: 2,621 additions & 2,022 deletions packages/cxx-storybook/package-lock.json

Large diffs are not rendered by default.

53 changes: 29 additions & 24 deletions packages/cxx-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,42 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@codemirror/commands": "^6.2.4",
"@codemirror/commands": "^6.2.5",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lint": "^6.3.0",
"@codemirror/lint": "^6.4.1",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.14.0",
"@storybook/addon-essentials": "^7.0.24",
"@storybook/addon-interactions": "^7.0.24",
"@storybook/addon-links": "^7.0.24",
"@storybook/blocks": "^7.0.24",
"@storybook/react": "^7.0.24",
"@storybook/react-vite": "^7.0.24",
"@codemirror/view": "^6.18.0",
"@storybook/addon-essentials": "^7.4.0",
"@storybook/addon-interactions": "^7.4.0",
"@storybook/addon-links": "^7.4.0",
"@storybook/blocks": "^7.4.0",
"@storybook/react": "^7.4.0",
"@storybook/react-vite": "^7.4.0",
"@storybook/testing-library": "^0.0.14-next.2",
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react": "^4.0.0",
"@tanstack/react-query": "^4.35.0",
"@types/lodash": "^4.14.198",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-react": "^4.0.4",
"codemirror": "^6.0.1",
"cxx-frontend": "file:../cxx-frontend",
"eslint": "^8.38.0",
"eslint": "^8.48.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"eslint-plugin-storybook": "^0.6.12",
"eslint-plugin-react-refresh": "^0.3.5",
"eslint-plugin-storybook": "^0.6.13",
"lodash": "^4.17.21",
"prop-types": "^15.8.1",
"storybook": "^7.0.24",
"typescript": "^5.0.2",
"vite": "^4.3.9"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.9",
"storybook": "^7.4.0",
"typescript": "^5.2.2",
"vite": "^4.4.9"
}
}
14 changes: 14 additions & 0 deletions packages/cxx-storybook/src/CxxFrontendClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import wasmBinaryUrl from "cxx-frontend/dist/cxx-js.wasm?url";
import { Parser } from "cxx-frontend";

export class CxxFrontendClient {
async load(signal?: AbortSignal) {
const response = await fetch(wasmBinaryUrl, { signal });
if (!response.ok) throw new Error("failed to load cxx-js.wasm");
if (signal?.aborted) throw new Error("aborted");
const data = await response.arrayBuffer();
const wasmBinary = new Uint8Array(data);
await Parser.init({ wasmBinary });
return true;
}
}
26 changes: 26 additions & 0 deletions packages/cxx-storybook/src/CxxFrontendProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import { CxxFrontendClient } from "./CxxFrontendClient";

interface CxxFrontendProviderProps {
client: CxxFrontendClient;
fallback?: React.ReactNode;
children: React.ReactNode;
}

export function CxxFrontendProvider({
client,
fallback,
children,
}: CxxFrontendProviderProps) {
const [isLoaded, setIsLoaded] = React.useState(false);

React.useEffect(() => {
if (isLoaded) return;
setIsLoaded(false);
const controller = new AbortController();
client.load(controller.signal).then(() => setIsLoaded(true));
return () => controller.abort();
}, [client, isLoaded]);

return isLoaded ? <>{children}</> : fallback;
}
3 changes: 0 additions & 3 deletions packages/cxx-storybook/src/Editor.css

This file was deleted.

169 changes: 28 additions & 141 deletions packages/cxx-storybook/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,162 +18,49 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import { FC, useEffect, useRef, useState } from "react";
import { basicSetup } from "codemirror";
import { cpp } from "@codemirror/lang-cpp";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { linter, Diagnostic } from "@codemirror/lint";
import * as cxx from "cxx-frontend";
import "./Editor.css";
import { ComponentProps } from "react";
import { useConfigureEditor } from "./useConfigureEditor";
import { Parser } from "cxx-frontend";

export interface EditorProps {
export interface EditorProps
extends Pick<ComponentProps<"div">, "className" | "style"> {
/**
* The initial text of the editor.
* The initial value of the editor.
* @default ""
*/
initialValue?: string;

/**
* Whether the Editor owns the syntax tree.
*
* Whether the editor is editable.
* @default true
*/
editorWillDisposeSyntaxTree?: boolean;
editable?: boolean;

/**
* Called when the cursor position changes.
*
* @param lineNumber 1-based line number
* @param column 1-based column number
* Whether to check the syntax of the document.
* @default true
*/
onCursorPositionChanged?: (lineNumber: number, column: number) => void;
checkSyntax?: boolean;

/**
* Called when the syntax is parsed.
*
* @param parser the Parser
* Called when the parser changes.
* @param parser The new parser or null if the parser was unset.
*/
onSyntaxChanged?: (parser: cxx.Parser) => void;
onParserChanged?: (parser: Parser | null) => void;
}

export const Editor: FC<EditorProps> = ({
export function Editor({
initialValue,
editorWillDisposeSyntaxTree = true,
onCursorPositionChanged,
onSyntaxChanged,
}) => {
const editorRef = useRef<HTMLDivElement>(null);

const [editor, setEditor] = useState<EditorView | null>(null);

const linterRef = useRef({
editorWillDisposeSyntaxTree,
onCursorPositionChanged,
onSyntaxChanged,
checkSyntax,
editable,
onParserChanged,
...props
}: EditorProps) {
const configureEditor = useConfigureEditor({
initialValue,
checkSyntax,
editable,
onParserChanged,
});

linterRef.current = {
editorWillDisposeSyntaxTree,
onCursorPositionChanged,
onSyntaxChanged,
};

useEffect(() => {
if (!editor) return;

editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: initialValue },
});
}, [editor, initialValue]);

useEffect(() => {
const domElement = editorRef.current;

if (!domElement) {
return;
}

const {
editorWillDisposeSyntaxTree,
onCursorPositionChanged,
onSyntaxChanged,
} = linterRef.current;

const syntaxChecker = (view: EditorView) => {
if (!cxx.Parser.isInitialized()) {
// The parser is not ready yet, we can't do anything
return [];
}

const source = view.state.doc.toString();

const parser = new cxx.Parser({
path: "main.cc",
source,
});

parser.parse();

const diagnostics: Diagnostic[] = [];

for (const diagnostic of parser.getDiagnostics()) {
const { startLine, startColumn, endLine, endColumn, message } =
diagnostic;

const from =
view.state.doc.line(startLine).from + Math.max(startColumn - 1, 0);

const to =
view.state.doc.line(endLine).from + Math.max(endColumn - 1, 0);

diagnostics.push({
severity: "error",
from,
to,
message,
});
}

onSyntaxChanged?.(parser);

if (editorWillDisposeSyntaxTree || !onSyntaxChanged) {
parser.dispose();
}

return diagnostics;
};

const needsRefresh = () => {
return !cxx.Parser.isInitialized();
};

const cppLinter = linter(syntaxChecker, { needsRefresh });

const updateListener = EditorView.updateListener.of((update) => {
if (update.selectionSet && onCursorPositionChanged) {
const sel = update.state.selection.main;
const line = update.state.doc.lineAt(sel.to);
const column = sel.from - line.from;
onCursorPositionChanged(line.number, column);
}
});

const startState = EditorState.create({
doc: "",
extensions: [basicSetup, cpp(), updateListener, cppLinter],
});

const editor = new EditorView({
state: startState,
parent: domElement,
});

setEditor(editor);

return () => {
editor.destroy();
};
}, [editorRef]);

return <div ref={editorRef} className="Editor" />;
};
return <div ref={configureEditor} {...props} />;
}
32 changes: 0 additions & 32 deletions packages/cxx-storybook/src/setupCxxFrontend.ts

This file was deleted.

Loading

0 comments on commit cf6cf4f

Please sign in to comment.