Skip to content

Commit

Permalink
feat: Allow custom languages for code editor (#537)
Browse files Browse the repository at this point in the history
  • Loading branch information
avinashbot authored Dec 1, 2022
1 parent abd801c commit 707ed43
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 17 deletions.
10 changes: 9 additions & 1 deletion src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3821,7 +3821,9 @@ The object should contain, among others:
"type": "string",
},
Object {
"description": "Specifies the programming language. You can use any of the programming languages supported by the \`ace\` object that you provide.",
"description": "Specifies the programming language. You can use any of the programming languages supported by the \`ace\` object that you provide.
Alternatively, this can be used to set a language that is not supported by the default \`language\` list. Make sure you've added the highlighting support for this language to the Ace instance.
For more info on custom languages, see the [Code editor API](/components/code-editor?tabId=api) page.",
"inlineType": Object {
"name": "CodeEditorProps.Language",
"properties": Array [],
Expand All @@ -3831,6 +3833,12 @@ The object should contain, among others:
"optional": false,
"type": "CodeEditorProps.Language",
},
Object {
"description": "Specifies a custom label language. If set, it overrides the default language label.",
"name": "languageLabel",
"optional": true,
"type": "string",
},
Object {
"description": "Renders the code editor in a loading state.",
"name": "loading",
Expand Down
40 changes: 40 additions & 0 deletions src/code-editor/__tests__/code-editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,46 @@ describe('Code editor component', () => {
expect(wrapper.findStatusBar()!.getElement()).toHaveTextContent('JavaScript');
});

it('uses custom language label over the default label', () => {
const { wrapper } = renderCodeEditor({ languageLabel: 'PartiQL' });
expect(wrapper.findStatusBar()!.getElement()).toHaveTextContent('PartiQL');
});

it('allows providing a custom language', () => {
renderCodeEditor({ language: 'partiql' });
expect(editorMock.session.setMode).toHaveBeenCalledWith('ace/mode/partiql');
});

it('uses custom language with custom label', () => {
const { wrapper } = renderCodeEditor({ languageLabel: 'PartiQL', language: 'partiql' });
expect(editorMock.session.setMode).toHaveBeenCalledWith('ace/mode/partiql');
expect(wrapper.findStatusBar()!.getElement()).toHaveTextContent('PartiQL');
});

it('falls back to language name if a custom language is used without languageLabel', () => {
const { wrapper } = renderCodeEditor({ language: 'partiql' });
expect(editorMock.session.setMode).not.toHaveBeenCalledWith('ace/mode/javascript');
expect(editorMock.session.setMode).toHaveBeenCalledWith('ace/mode/partiql');
expect(wrapper.findStatusBar()!.getElement()).toHaveTextContent('partiql');
});

it("uses custom label even if a language isn't provided", () => {
const { wrapper } = renderCodeEditor({ language: undefined, languageLabel: 'PartiQL' });
expect(editorMock.session.setMode).toHaveBeenCalledWith('ace/mode/undefined');
expect(wrapper.findStatusBar()!.getElement()).toHaveTextContent('PartiQL');
});

/**
* Undefined language should run the component anyway even when bypassing language requirements,
* When that happens, Ace handles the missing language error
* renderCodeEditor uses Partial<CodeEditorProps>, no casting needed here
*/
it('allows unidentified language without breaking', () => {
const { wrapper } = renderCodeEditor({ language: undefined });
expect(editorMock.session.setMode).toHaveBeenCalledWith('ace/mode/undefined');
expect(wrapper.findStatusBar()!.getElement()).not.toHaveTextContent('undefined');
});

it('changes value', () => {
const { rerender } = renderCodeEditor({ value: 'value-initial' });

Expand Down
33 changes: 19 additions & 14 deletions src/code-editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import PreferencesModal from './preferences-modal';
import LoadingScreen from './loading-screen';
import ErrorScreen from './error-screen';

import styles from './styles.css.js';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { useContainerQuery } from '../internal/hooks/container-queries/use-container-query';
import useBaseComponent from '../internal/hooks/use-base-component';
Expand All @@ -38,12 +37,24 @@ import { useFormFieldContext } from '../internal/context/form-field-context';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { useControllable } from '../internal/hooks/use-controllable';
import LiveRegion from '../internal/components/live-region';

import styles from './styles.css.js';

export { CodeEditorProps };

export default function CodeEditor(props: CodeEditorProps) {
const { __internalRootRef } = useBaseComponent('CodeEditor');
const { controlId, ariaLabelledby, ariaDescribedby } = useFormFieldContext(props);
const { ace, value, language, i18nStrings, editorContentHeight, onEditorContentResize, ...rest } = props;
const {
ace,
value,
language,
i18nStrings,
editorContentHeight,
onEditorContentResize,
languageLabel: customLanguageLabel,
...rest
} = props;
const [editorHeight = 480, setEditorHeight] = useControllable(editorContentHeight, onEditorContentResize, 480, {
componentName: 'code-editor',
changeHandler: 'onEditorContentResize',
Expand All @@ -70,7 +81,7 @@ export default function CodeEditor(props: CodeEditorProps) {
);
},
[ace]
); // loads as soon as ace lib is available
);

useEffect(() => {
if (!editor) {
Expand All @@ -90,7 +101,6 @@ export default function CodeEditor(props: CodeEditorProps) {
const [paneStatus, setPaneStatus] = useState<PaneStatus>('hidden');
const [annotations, setAnnotations] = useState<Ace.Annotation[]>([]);
const [highlightedAnnotation, setHighlightedAnnotation] = useState<Ace.Annotation>();
const [languageLabel, setLanguageLabel] = useState<string>('');
const [cursorPosition, setCursorPosition] = useState<Ace.Point>({ row: 0, column: 0 });
const [isTabFocused, setTabFocused] = useState<boolean>(false);

Expand All @@ -117,7 +127,7 @@ export default function CodeEditor(props: CodeEditorProps) {

return () => {
editor?.destroy();
}; // TODO profile/monitor this
};
}, [ace, editor, __internalRootRef]);

useEffect(() => {
Expand All @@ -134,12 +144,7 @@ export default function CodeEditor(props: CodeEditorProps) {
}, [editor, value]);

useEffect(() => {
if (!editor) {
return;
}
editor.session.setMode(`ace/mode/${language}`);

setLanguageLabel(getLanguageLabel(language));
editor?.session.setMode(`ace/mode/${language}`);
}, [editor, language]);

useEffect(() => {
Expand All @@ -148,15 +153,13 @@ export default function CodeEditor(props: CodeEditorProps) {
}

const theme: CodeEditorProps.Theme = props.preferences?.theme ?? defaultTheme;

editor.setTheme(getAceTheme(theme));

editor.session.setUseWrapMode(props.preferences?.wrapLines ?? true);
}, [editor, defaultTheme, props.preferences]);

// listeners
// Change listeners
useChangeEffect(editor, props.onChange, props.onDelayedChange);
// TODO implement other listeners

// Hide error panel when there are no errors to show.
useEffect(() => {
Expand All @@ -169,6 +172,8 @@ export default function CodeEditor(props: CodeEditorProps) {
}
}, [annotations, props.onValidate]);

const languageLabel = customLanguageLabel ?? getLanguageLabel(language);

const errorCount = annotations.filter(a => a.type === 'error').length;
const warningCount = annotations.filter(a => a.type === 'warning').length;
const currentAnnotations = useMemo(() => annotations.filter(a => a.type === paneStatus), [annotations, paneStatus]);
Expand Down
16 changes: 15 additions & 1 deletion src/code-editor/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ export interface CodeEditorProps extends BaseComponentProps, FormFieldControlPro

/**
* Specifies the programming language. You can use any of the programming languages supported by the `ace` object that you provide.
* Alternatively, this can be used to set a language that is not supported by the default `language` list. Make sure you've added the highlighting support for this language to the Ace instance.
* For more info on custom languages, see the [Code editor API](/components/code-editor?tabId=api) page.
*/
language: CodeEditorProps.Language;

/**
* Specifies a custom label language. If set, it overrides the default language label.
*/
languageLabel?: string;

/**
* An event handler called when the value changes.
* The event `detail` contains the current value of the code editor content.
Expand Down Expand Up @@ -105,8 +112,15 @@ export interface CodeEditorProps extends BaseComponentProps, FormFieldControlPro
onEditorContentResize?: NonCancelableEventHandler<CodeEditorProps.ResizeDetail>;
}

// Prevents typescript from collapsing a string union type into a string type while still allowing any string.
// This leads to more helpful editor suggestions for known values.
// See: https://github.com/microsoft/TypeScript/issues/29729
type LiteralUnion<LiteralType, BaseType extends string> = LiteralType | (BaseType & { _?: never });

type BuiltInLanguage = typeof AceModes[number]['value'];

export namespace CodeEditorProps {
export type Language = typeof AceModes[number]['value'];
export type Language = LiteralUnion<BuiltInLanguage, string>;
export type Theme = typeof LightThemes[number]['value'] | typeof DarkThemes[number]['value'];

export interface AvailableThemes {
Expand Down
2 changes: 1 addition & 1 deletion src/code-editor/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ export function getAceTheme(theme: CodeEditorProps.Theme) {
}

export function getLanguageLabel(language: CodeEditorProps.Language): string {
return AceModes.filter((mode: { value: string }) => mode.value === language)[0]?.label || '';
return AceModes.filter((mode: { value: string }) => mode.value === language)[0]?.label || language;
}

0 comments on commit 707ed43

Please sign in to comment.