Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(codeeditor): support to extensions #286

Merged
merged 9 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions cypress/integration/CodeMirror.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,34 @@ describe("CodeMirror", () => {

cy.get(".cm-content").invoke("attr", "style", "tab-size: 4;").snapshot();
});

it("should load the autocomplete CodeMirror extension", () => {
cy.viewport(600, 1000).visit(
`/iframe.html?id=components-code-editor--extension-autocomplete`
);

// Remove all content from element editable
cy.get(".cm-content").type("{selectall}").type("{backspace}");

// Trigger autocomplete keybinding
cy.get(".cm-content").type("{ctrl} ");

// Wait for the autocomplete modal appears and select the first item
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(200).focused().type("{enter}");

/**
* Take a snapshot of the current editor state,
* which should contains a snippet code:
*
* ```
* class name {
* constructor(params) {
*
* }
* }
* ```
*/
cy.get(".cm-content").snapshot();
});
});
3 changes: 3 additions & 0 deletions cypress/snapshots.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions sandpack-react/src/components/CodeEditor/CodeEditor.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import type { Story } from "@storybook/react";
import * as React from "react";

import { Sandpack } from "../../";
import { SandpackProvider } from "../../contexts/sandpackContext";
import { SandpackThemeProvider } from "../../contexts/themeContext";
import { SandpackPreview } from "../Preview";
Expand Down Expand Up @@ -70,3 +72,15 @@ export const ClosableTabs: React.FC = () => (
</SandpackThemeProvider>
</SandpackProvider>
);

export const ExtensionAutocomplete: React.FC = () => (
<SandpackProvider template="react">
<SandpackThemeProvider>
<SandpackCodeEditor
extensions={[autocompletion()]}
extensionsKeymap={[completionKeymap]}
id="extensions"
/>
</SandpackThemeProvider>
</SandpackProvider>
);
26 changes: 16 additions & 10 deletions sandpack-react/src/components/CodeEditor/CodeMirror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { defaultHighlightStyle } from "@codemirror/highlight";
import { history, historyKeymap } from "@codemirror/history";
import { bracketMatching } from "@codemirror/matchbrackets";
import { EditorState } from "@codemirror/state";
import type { Annotation } from "@codemirror/state";
import type { Annotation, Extension } from "@codemirror/state";
import {
highlightSpecialChars,
highlightActiveLine,
Expand Down Expand Up @@ -70,6 +70,8 @@ interface CodeMirrorProps {
decorators?: Decorators;
initMode: SandpackInitMode;
id?: string;
extensions?: Extension[];
extensionsKeymap?: Array<readonly KeyBinding[]>;
}

export interface CodeMirrorRef {
Expand All @@ -94,6 +96,8 @@ export const CodeMirror = React.forwardRef<CodeMirrorRef, CodeMirrorProps>(
decorators,
initMode = "lazy",
id,
extensions = [],
extensionsKeymap = [],
},
ref
) => {
Expand Down Expand Up @@ -170,7 +174,7 @@ export const CodeMirror = React.forwardRef<CodeMirrorRef, CodeMirrorProps>(
},
];

const extensions = [
const extensionList = [
highlightSpecialChars(),
history(),
closeBrackets(),
Expand All @@ -181,41 +185,43 @@ export const CodeMirror = React.forwardRef<CodeMirrorRef, CodeMirrorProps>(
...historyKeymap,
...commentKeymap,
...customCommandsKeymap,
...extensionsKeymap,
] as KeyBinding[]),
langSupport,

defaultHighlightStyle.fallback,

getEditorTheme(theme),
getSyntaxHighlight(theme),
...extensions,
];

if (readOnly) {
extensions.push(EditorView.editable.of(false));
extensionList.push(EditorView.editable.of(false));
} else {
extensions.push(bracketMatching());
extensions.push(highlightActiveLine());
extensionList.push(bracketMatching());
extensionList.push(highlightActiveLine());
}

if (decorators) {
extensions.push(highlightDecorators(decorators));
extensionList.push(highlightDecorators(decorators));
}

if (wrapContent) {
extensions.push(EditorView.lineWrapping);
extensionList.push(EditorView.lineWrapping);
}

if (showLineNumbers) {
extensions.push(lineNumbers());
extensionList.push(lineNumbers());
}

if (showInlineErrors) {
extensions.push(highlightInlineError());
extensionList.push(highlightInlineError());
}

const startState = EditorState.create({
doc: code,
extensions,
extensions: extensionList,
});

const parentDiv = wrapper.current;
Expand Down
18 changes: 18 additions & 0 deletions sandpack-react/src/components/CodeEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useClasser } from "@code-hike/classer";
import type { Extension } from "@codemirror/state";
import type { KeyBinding } from "@codemirror/view";
import * as React from "react";

import { RunButton } from "../../common/RunButton";
Expand Down Expand Up @@ -27,6 +29,16 @@ export interface CodeEditorProps {
* a certain control of when to initialize them.
*/
initMode?: SandpackInitMode;
/**
* CodeMirror extensions for the editor state, which can
* provide extra features and functionalities to the editor component.
*/
extensions?: Extension[];
/**
* Property to register CodeMirror extension keymap.
*/
extensionsKeymap?: Array<readonly KeyBinding[]>;
id?: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓What's the id for? Should we add a comment explaining what it's for? I'm assuming it maps to the id of the CodeEditor so maybe that's obvious.

Copy link
Member Author

@danilowoz danilowoz Jan 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. This is a temp bug fix that I'll approach in another PR soon. It's related to a random id generated in every render, which leads to unmatch values in tests cases. Here's more detail #143

}

export { CodeMirror as CodeEditor };
Expand All @@ -48,6 +60,9 @@ export const SandpackCodeEditor = React.forwardRef<
wrapContent = false,
closableTabs = false,
initMode,
extensions,
extensionsKeymap,
id,
},
ref
) => {
Expand All @@ -72,7 +87,10 @@ export const SandpackCodeEditor = React.forwardRef<
ref={ref}
code={code}
editorState={editorState}
extensions={extensions}
extensionsKeymap={extensionsKeymap}
filePath={activePath}
id={id}
initMode={initMode || sandpack.initMode}
onCodeUpdate={handleCodeUpdate}
showInlineErrors={showInlineErrors}
Expand Down
4 changes: 4 additions & 0 deletions sandpack-react/src/presets/Sandpack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SandpackPreview } from "../components/Preview";
import type { SandpackProviderProps } from "../contexts/sandpackContext";
import { SandpackProvider } from "../contexts/sandpackContext";
import type {
SandpackCodeOptions,
FileResolver,
SandpackFiles,
SandpackInitMode,
Expand Down Expand Up @@ -55,6 +56,7 @@ export interface SandpackProps {
autorun?: boolean;
recompileMode?: "immediate" | "delayed";
recompileDelay?: number;
codeEditor?: SandpackCodeOptions;
};
}

Expand Down Expand Up @@ -84,6 +86,8 @@ export const Sandpack: React.FC<SandpackProps> = (props) => {
wrapContent: props.options?.wrapContent,
closableTabs: props.options?.closableTabs,
initMode: props.options?.initMode,
extensions: props.options?.codeEditor?.extensions,
extensionsKeymap: props.options?.codeEditor?.extensionsKeymap,
};

const providerOptions: SandpackProviderProps = {
Expand Down
18 changes: 18 additions & 0 deletions sandpack-react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
UnsubscribeFunction,
} from "@codesandbox/sandpack-client";

import type { CodeEditorProps } from ".";

export type SandpackClientDispatch = (
msg: SandpackMessage,
clientId?: string
Expand Down Expand Up @@ -231,6 +233,22 @@ export type SandpackThemeProp =
| SandpackPartialTheme
| "auto";

/**
* Custom properties to be used in the SandpackCodeEditor component,
* some of which are exclusive to customize the CodeMirror instance.
*/
export interface SandpackCodeOptions {
/**
* CodeMirror extensions for the editor state, which can
* provide extra features and functionalities to the editor component.
*/
extensions?: CodeEditorProps["extensions"];
/**
* Property to register CodeMirror extension keymap.
*/
extensionsKeymap?: CodeEditorProps["extensionsKeymap"];
}

/**
* @hidden
*/
Expand Down
25 changes: 25 additions & 0 deletions website/docs/docs/advanced-usage/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,31 @@ If you played with the `Sandpack` preset, you should be familiar already with th
</SandpackLayout>
</SandpackProvider>

### Extensions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! I was hoping you'd add docs too 🙌


Sandpack uses CodeMirror behind the hood to provide a nice editor. You can extend the editor with your own extensions, like [`@codemirror/autocomplete`](https://www.npmjs.com/package/@codemirror/autocomplete).
danilowoz marked this conversation as resolved.
Show resolved Hide resolved

```jsx
<SandpackProvider template="react">
<SandpackThemeProvider>
<SandpackCodeEditor
extensions={[autocompletion()]}
extensionsKeymap={[completionKeymap]}
/>
</SandpackThemeProvider>
</SandpackProvider>

<Sandpack
options={{
codeEditor: {
extensions: [autocompletion()],
extensionsKeymap: [completionKeymap],
},
}}
template="react"
/>
```

## Code Viewer

For situations when you strictly want to show some code and run it in the browser, you can use the `SandpackCodeViewer` component. It looks similar to the code editor, but it renders a read-only version of `codemirror`, so users will not be able to edit the code.
Expand Down
2 changes: 1 addition & 1 deletion website/docs/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ We can't currently support this in the Sandpack library, due to technical limita

#### How to highlight TypeScript errors in the editor?

Sandpack still doesn't officially support any kind of language server to provide a way to highlight errors in the `SandpackCodeEditor`. However, there is a discussion of how to make it work on [this topic](https://github.com/codesandbox/sandpack/discussions/237), with some examples and Codemirror documentation references of how to implement it.
Sandpack still doesn't officially support any kind of language server to provide a way to highlight errors in the `SandpackCodeEditor`. However, there is a discussion of how to make it work on [this topic](https://github.com/codesandbox/sandpack/discussions/237), with some examples and CodeMirror documentation references of how to implement it.

#### Why is the bundler hosted externally (iframe) and not a simple JavaScript module?

Expand Down