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 additional, user-supplied language modes #570

Merged
merged 6 commits into from
Sep 15, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sandpack-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@codemirror/legacy-modes": "^0.19.1",
"@codemirror/stream-parser": "^0.19.9",
"@codesandbox/sandpack-themes": "^1.6.0",
"@storybook/addon-actions": "^6.1.9",
"@storybook/addon-essentials": "^6.1.9",
Expand Down
44 changes: 44 additions & 0 deletions sandpack-react/src/components/CodeEditor/CodeMirror.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { LanguageSupport } from "@codemirror/language";
import { python } from "@codemirror/legacy-modes/mode/python";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { StreamLanguage } from "@codemirror/stream-parser";
import * as React from "react";

import { SandpackProvider } from "../../contexts/sandpackContext";
Expand Down Expand Up @@ -95,6 +99,46 @@ export const Markdown: React.FC = () => (
</SandpackProvider>
);

export const CustomLanguageShell: React.FC = () => (
<SandpackProvider
additionalLanguages={[
{
name: "shell",
extensions: ["sh"],
language: new LanguageSupport(StreamLanguage.define(shell)),
},
]}
>
<CodeEditor
code={mocks.shell}
filePath="example.sh"
id="shell"
initMode="immediate"
showLineNumbers={false}
/>
</SandpackProvider>
);

export const CustomLanguagePython: React.FC = () => (
<SandpackProvider
additionalLanguages={[
jodyheavener marked this conversation as resolved.
Show resolved Hide resolved
{
name: "python",
extensions: ["py"],
language: new LanguageSupport(StreamLanguage.define(python)),
},
]}
>
<CodeEditor
code={mocks.python}
fileType="python"
id="python"
initMode="immediate"
showLineNumbers={false}
/>
</SandpackProvider>
);

export const ShowLineNumber: React.FC = () => (
<SandpackProvider>
<CodeEditor
Expand Down
28 changes: 14 additions & 14 deletions sandpack-react/src/components/CodeEditor/CodeMirror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,7 @@ export type Decorators = Array<{
interface CodeMirrorProps {
code: string;
filePath?: string;
fileType?:
| "js"
| "jsx"
| "ts"
| "tsx"
| "css"
| "scss"
| "less"
| "html"
| "vue"
| "markdown";
fileType?: string;
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if we can extend this key using generics and still keep type-safe, let me know if you need any help

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've been wracking my brain trying to make this work, but I'm not sure I'm fully understanding how we can use generics in this case. Would love some guidance if you have a chance!

onCodeUpdate?: (newCode: string) => void;
showLineNumbers?: boolean;
showInlineErrors?: boolean;
Expand Down Expand Up @@ -144,7 +134,10 @@ export const CodeMirror = React.forwardRef<CodeMirrorRef, CodeMirrorProps>(
);

const c = useClasser(THEME_PREFIX);
const { listen } = useSandpack();
const {
listen,
sandpack: { additionalLanguages },
jodyheavener marked this conversation as resolved.
Show resolved Hide resolved
} = useSandpack();
const ariaId = useGeneratedId(id);

const prevExtension = React.useRef<Extension[]>([]);
Expand All @@ -167,8 +160,15 @@ export const CodeMirror = React.forwardRef<CodeMirrorRef, CodeMirrorProps>(
}
}, [initMode, isIntersecting]);

const languageExtension = getLanguageFromFile(filePath, fileType);
const langSupport = getCodeMirrorLanguage(languageExtension);
const languageExtension = getLanguageFromFile(
filePath,
fileType,
additionalLanguages
);
const langSupport = getCodeMirrorLanguage(
languageExtension,
additionalLanguages
);
const highlightTheme = getSyntaxHighlight(theme);

const syntaxHighlightRender = useSyntaxHighlight({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1456,6 +1456,21 @@ Array [

exports[`useSyntaxHighlight renders a markdown block 1`] = `null`;

exports[`useSyntaxHighlight renders a python block 1`] = `
Array [
"import os

api_token = os.environ",
<span
className="ͼo"
>
['API_TOKEN']
</span>,
]
`;

exports[`useSyntaxHighlight renders a shell block 1`] = `null`;
Copy link
Member

Choose a reason for hiding this comment

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

Interesting why the output is null, could you take a look at this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is odd - it looks like it's stumbling on the provided code sample? If I change the mocked "shell" code to the Python code it works fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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


exports[`useSyntaxHighlight renders a vue block 1`] = `
Array [
<span
Expand Down
16 changes: 16 additions & 0 deletions sandpack-react/src/components/CodeEditor/languages-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,19 @@ export const markdown = `## Title
- List item
- List item
- List item`;

export const shell = `#!/bin/sh

EXAMPLE="drawn joyed"

# Prints the EXAMPLE variable
function show-example() {
echo $EXAMPLE
}`;

export const python = `import os

api_token = os.environ['API_TOKEN']

# Prints the api_token variable
print("API token:", api_token)`;
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const highlightTheme = getSyntaxHighlight(defaultLight);
describe(useSyntaxHighlight, () => {
Object.entries(mocks).forEach(([fileType, code]) => {
it(`renders a ${fileType} block`, () => {
const languageExtension = getLanguageFromFile("", fileType);
const langSupport = getCodeMirrorLanguage(languageExtension);
const languageExtension = getLanguageFromFile("", fileType, []);
const langSupport = getCodeMirrorLanguage(languageExtension, []);

const reactElements = useSyntaxHighlight({
code,
Expand Down
29 changes: 23 additions & 6 deletions sandpack-react/src/components/CodeEditor/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EditorView } from "@codemirror/view";
import * as React from "react";

import { THEME_PREFIX } from "../../styles";
import type { SandpackTheme } from "../../types";
import type { CustomLanguage, SandpackTheme } from "../../types";

export const getCodeMirrorPosition = (
doc: Text,
Expand Down Expand Up @@ -160,9 +160,10 @@ type SandpackLanguageSupport =
| "markdown";

export const getLanguageFromFile = (
filePath?: string,
fileType?: string
): SandpackLanguageSupport => {
filePath: string | undefined,
fileType: string | undefined,
additionalLanguages: CustomLanguage[]
): string => {
if (!filePath && !fileType) return "markdown";

let extension = fileType;
Expand All @@ -171,6 +172,15 @@ export const getLanguageFromFile = (
extension = filePath.slice(extensionDotIndex + 1);
}

for (const additionalLanguage of additionalLanguages) {
if (
extension === additionalLanguage.name ||
additionalLanguage.extensions.includes(extension || "")
) {
return additionalLanguage.name;
}
}

switch (extension) {
case "js":
case "jsx":
Expand All @@ -195,7 +205,8 @@ export const getLanguageFromFile = (
};

export const getCodeMirrorLanguage = (
extension: SandpackLanguageSupport
extension: string,
additionalLanguages: CustomLanguage[]
): LanguageSupport => {
const options: Record<SandpackLanguageSupport, LanguageSupport> = {
javascript: javascript({ jsx: true, typescript: false }),
Expand All @@ -205,7 +216,13 @@ export const getCodeMirrorLanguage = (
markdown: markdown(),
};

return options[extension];
for (const additionalLanguage of additionalLanguages) {
if (extension === additionalLanguage.name) {
return additionalLanguage.language;
}
}

return options[extension as keyof typeof options];
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
3 changes: 3 additions & 0 deletions sandpack-react/src/contexts/sandpackContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export class SandpackProviderClass extends React.PureComponent<
editorState: "pristine",
initMode: this.props.options?.initMode || "lazy",
reactDevTools: undefined,
additionalLanguages: this.props.additionalLanguages || [],
};

/**
Expand Down Expand Up @@ -625,6 +626,7 @@ export class SandpackProviderClass extends React.PureComponent<
sandpackStatus,
environment,
initMode,
additionalLanguages,
} = this.state;

return {
Expand All @@ -639,6 +641,7 @@ export class SandpackProviderClass extends React.PureComponent<
status: sandpackStatus,
editorState,
initMode,
additionalLanguages,
clients: this.clients,

dispatch: this.dispatchMessage,
Expand Down
40 changes: 40 additions & 0 deletions sandpack-react/src/presets/Sandpack.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { LanguageSupport } from "@codemirror/language";
import { python } from "@codemirror/legacy-modes/mode/python";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { StreamLanguage } from "@codemirror/stream-parser";
import React from "react";

import { Sandpack } from "../";
Expand Down Expand Up @@ -286,3 +290,39 @@ export const ShowConsoleButton: React.FC = () => (
/>
</div>
);

export const CustomLanguages: React.FC = () => (
<Sandpack
additionalLanguages={[
jodyheavener marked this conversation as resolved.
Show resolved Hide resolved
{
name: "python",
extensions: ["py"],
language: new LanguageSupport(StreamLanguage.define(python)),
},
{
name: "shell",
extensions: ["sh"],
jodyheavener marked this conversation as resolved.
Show resolved Hide resolved
language: new LanguageSupport(StreamLanguage.define(shell)),
},
]}
customSetup={{
entry: "/example.py",
}}
files={{
"/example.py": `import os

api_token = os.environ['API_TOKEN']

# Prints the api_token variable
print("API token:", api_token)`,
"/example.sh": `#!/bin/sh

EXAMPLE="drawn joyed"

# Prints the EXAMPLE variable
function show-example() {
echo $EXAMPLE
}`,
}}
/>
);
1 change: 1 addition & 0 deletions sandpack-react/src/presets/Sandpack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const Sandpack: SandpackInternal = (props) => {

return (
<SandpackProvider
additionalLanguages={props.additionalLanguages}
customSetup={props.customSetup}
files={props.files as TemplateFiles<SandpackPredefinedTemplate>}
options={providerOptions}
Expand Down
13 changes: 13 additions & 0 deletions sandpack-react/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import type { LanguageSupport } from "@codemirror/language";
import type {
BundlerState,
ListenerFunction,
Expand Down Expand Up @@ -270,6 +271,15 @@ export interface SandpackFile {
*/
export type SandpackInitMode = "immediate" | "lazy" | "user-visible";

/**
* @category Setup
*/
export interface CustomLanguage {
name: string;
extensions: string[];
language: LanguageSupport;
}

/**
* @category Theme
*/
Expand Down Expand Up @@ -414,6 +424,7 @@ interface SandpackRootProps<
template?: TemplateName;
customSetup?: SandpackSetup;
theme?: SandpackThemeProp;
additionalLanguages?: CustomLanguage[];
}

/**
Expand Down Expand Up @@ -542,6 +553,7 @@ export interface SandpackState {
editorState: EditorState;
error: SandpackError | null;
files: SandpackBundlerFiles;
additionalLanguages: CustomLanguage[];
environment?: SandboxEnvironment;
status: SandpackStatus;
initMode: SandpackInitMode;
Expand Down Expand Up @@ -660,4 +672,5 @@ export interface SandpackProviderState {
sandpackStatus: SandpackStatus;
editorState: EditorState;
reactDevTools?: ReactDevToolsMode;
additionalLanguages: CustomLanguage[];
}
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,13 @@
"@lezer/common" "^0.15.5"
"@lezer/lr" "^0.15.0"

"@codemirror/legacy-modes@^0.19.1":
version "0.19.1"
resolved "https://registry.yarnpkg.com/@codemirror/legacy-modes/-/legacy-modes-0.19.1.tgz#7dc3b5df1842060648f75764ab6919fcfce3ea1a"
integrity sha512-vYPLsD/ON+3SXhlGj9Qb3fpFNNU3Ya/AtDiv/g3OyqVzhh5vs5rAnOvk8xopGWRwppdhlNPD9VyXjiOmZUQtmQ==
dependencies:
"@codemirror/stream-parser" "^0.19.0"

"@codemirror/lint@^0.19.0":
version "0.19.6"
resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-0.19.6.tgz#0379688da3e16739db4a6304c73db857ca85d7ec"
Expand Down Expand Up @@ -1514,6 +1521,18 @@
dependencies:
"@codemirror/text" "^0.19.0"

"@codemirror/stream-parser@^0.19.0", "@codemirror/stream-parser@^0.19.9":
version "0.19.9"
resolved "https://registry.yarnpkg.com/@codemirror/stream-parser/-/stream-parser-0.19.9.tgz#34955ea91a8047cf72abebd5ce28f0d332aeca48"
integrity sha512-WTmkEFSRCetpk8xIOvV2yyXdZs3DgYckM0IP7eFi4ewlxWnJO/H4BeJZLs4wQaydWsAqTQoDyIwNH1BCzK5LUQ==
dependencies:
"@codemirror/highlight" "^0.19.0"
"@codemirror/language" "^0.19.0"
"@codemirror/state" "^0.19.0"
"@codemirror/text" "^0.19.0"
"@lezer/common" "^0.15.0"
"@lezer/lr" "^0.15.0"

"@codemirror/text@^0.19.0", "@codemirror/text@^0.19.2", "@codemirror/text@^0.19.6":
version "0.19.6"
resolved "https://registry.yarnpkg.com/@codemirror/text/-/text-0.19.6.tgz#9adcbd8137f69b75518eacd30ddb16fd67bbac45"
Expand Down