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

Add linters to code editor #218

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- basic Storybook example for `<Application* />` components
- `$eccgui-selector-text-spot-highlight` config variable to specify selector that is used to create shortly highlighted spots
- it is highlighted when the selector is also active local anchor target or if it has the `.eccgui-typography--spothighlight` class attached to it
- `<CodeEditor />`
- implemented support for linting which is enabled via `useLinting` prop
- `turtle` and `javascript` are currently supported languages for linting
- editor is focused on load if `autoFocus` prop is set to `true`
- implemented support for `disabled` state in code editor
- implemented support for `intent` states in code editor

### Changed

Expand Down
16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,17 @@
"@blueprintjs/select": "^5.2.2",
"@carbon/icons": "^11.47.1",
"@carbon/react": "^1.64.1",
"@mavrin/remark-typograf": "^2.2.0",
"codemirror": "^6.0.1",
"@codemirror/legacy-modes": "^6.4.1",
"@codemirror/lang-markdown": "^6.2.5",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.2.5",
"@codemirror/lang-xml": "^6.1.0",
"xml-formatter": "^3.6.3",
"@codemirror/legacy-modes": "^6.4.1",
"@mavrin/remark-typograf": "^2.2.0",
"codemirror": "^6.0.1",
"color": "^4.2.3",
"compute-scroll-into-view": "^3.1.0",
"jshint": "^2.13.6",
"lodash": "^4.17.21",
"n3": "^1.23.0",
"re-resizable": "6.9.9",
"react": "^16.13.1",
"react-dom": "^16.13.1",
Expand All @@ -94,7 +95,8 @@
"remark-parse": "^10.0.2",
"reset-css": "^5.0.2",
"unified": "^11.0.5",
"wicg-inert": "^3.1.3"
"wicg-inert": "^3.1.3",
"xml-formatter": "^3.6.3"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand All @@ -120,7 +122,9 @@
"@types/codemirror": "^5.60.15",
"@types/color": "^3.0.6",
"@types/jest": "^29.5.12",
"@types/jshint": "^2.12.4",
"@types/lodash": "^4.17.7",
"@types/n3": "^1.21.1",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
Expand Down
13 changes: 12 additions & 1 deletion src/cmem/react-flow/StickyNoteModal/StickyNoteModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";

import { IntentTypes } from "../../../common/Intent";
import getColorConfiguration from "../../../common/utils/getColorConfiguration";
import { CodeEditor } from "../../../extensions";
import { ReactFlowHotkeyContext } from "../extensions/ReactFlowHotkeyContext";
Expand Down Expand Up @@ -32,10 +33,18 @@ export interface StickyNoteModalProps {
* Forward other properties to the `SimpleModal` element that is used for this dialog.
*/
simpleDialogProps?: Omit<SimpleDialogProps, "size" | "title" | "hasBorder" | "isOpen" | "onClose" | "actions">;
/**
* Disables the code editor
*/
disabledCodeEditor?: boolean;
/**
*Code editor intent
*/
codeEditorIntent?: IntentTypes | "edited" | "removed";
}

export const StickyNoteModal: React.FC<StickyNoteModalProps> = React.memo(
({ metaData, onClose, onSubmit, translate, simpleDialogProps }) => {
({ metaData, onClose, onSubmit, translate, simpleDialogProps, disabledCodeEditor, codeEditorIntent }) => {
const refNote = React.useRef<string>(metaData?.note ?? "");
const [color, setSelectedColor] = React.useState<string>(metaData?.color ?? "");
const noteColors: [string, string][] = Object.entries(getColorConfiguration("stickynotes")).map(
Expand Down Expand Up @@ -123,6 +132,8 @@ export const StickyNoteModal: React.FC<StickyNoteModalProps> = React.memo(
refNote.current = value;
}}
defaultValue={refNote.current}
disabled={disabledCodeEditor}
intent={codeEditorIntent}
/>
</FieldItem>
<FieldItem
Expand Down
13 changes: 13 additions & 0 deletions src/components/AutoSuggestion/ExtendedCodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Classes as BlueprintClassNames } from "@blueprintjs/core";
import { EditorState } from "@codemirror/state";
import { EditorView, lineNumbers, Rect } from "@codemirror/view";

import { IntentTypes } from "../../common/Intent";
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
import { CodeEditor } from "../../extensions/codemirror/CodeMirror";
//hooks
Expand Down Expand Up @@ -40,6 +41,14 @@ export interface ExtendedCodeEditorProps {
showScrollBar?: boolean;
/** allow multiline entries when new line characters are entered */
multiline?: boolean;
/**
* Disables the code editor
*/
disabled?: boolean;
/**
*Code editor intent
*/
intent?: IntentTypes | "edited" | "removed";
}

export type IEditorProps = ExtendedCodeEditorProps;
Expand All @@ -58,6 +67,8 @@ export const ExtendedCodeEditor = ({
placeholder,
onCursorChange,
onSelection,
disabled,
intent,
}: ExtendedCodeEditorProps) => {
const initialContent = React.useRef(multiline ? initialValue : initialValue.replace(/[\r\n]/g, " "));
const multilineExtensions = multiline
Expand Down Expand Up @@ -88,6 +99,8 @@ export const ExtendedCodeEditor = ({
multiline ? "codeeditor" : `singlelinecodeeditor ${BlueprintClassNames.INPUT}`
}`,
}}
disabled={disabled}
intent={intent}
/>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/components/TextField/stories/TextField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Default.args = {
fullWidth: false,
placeholder: "placeholder text",
readOnly: false,
disabled: true,
};

/** Text field with default value that contains a zero width/invisible character.
Expand All @@ -46,7 +47,7 @@ const invisibleCharacterWarningProps: TextFieldProps = {
const codePointsString = [...Array.from(codePoints)]
.map((n) => {
const info = characters.invisibleZeroWidthCharacters.codePointMap.get(n);
return info.fullLabel;
return info?.fullLabel;
})
.join(", ");
alert("Invisible character detected in input string. Code points: " + codePointsString);
Expand Down
1 change: 1 addition & 0 deletions src/configuration/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ $eccgui-color-applicationheader-text: #222 !default;
$eccgui-color-applicationheader-background: #ddd !default;
$eccgui-color-workspace-text: #444 !default;
$eccgui-color-workspace-background: #f5f5f5 !default;
$ecgui-color-background-disabled: #c6c6c6 !default;
$eccgui-color-application-text: $eccgui-color-workspace-text !default; // deprecated
$eccgui-color-application-background: $eccgui-color-workspace-background !default; // deprecated

Expand Down
30 changes: 30 additions & 0 deletions src/extensions/codemirror/CodeMirror.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import { Meta, StoryFn } from "@storybook/react";

import { helpersArgTypes } from "../../../.storybook/helpers";

import { CodeEditor } from "./CodeMirror";

export default {
Expand All @@ -11,6 +13,9 @@ export default {
onChange: {
action: "value changed",
},
intent: {
...helpersArgTypes.exampleIntent,
},
},
} as Meta<typeof CodeEditor>;

Expand All @@ -22,3 +27,28 @@ BasicExample.args = {
mode: "markdown",
defaultValue: "**test me**",
};

export const LinterExample = TemplateFull.bind({});
LinterExample.args = {
name: "codeinput",
defaultValue: "**test me**",
mode: "javascript",
useLinting: true,
autoFocus: true,
};

export const DisabledExample = TemplateFull.bind({});
DisabledExample.args = {
name: "codeinput",
defaultValue: "**test me**",
mode: "javascript",
disabled: true,
};

export const IntentExample = TemplateFull.bind({});
IntentExample.args = {
name: "codeinput",
defaultValue: "**test me**",
mode: "javascript",
intent: "warning",
};
73 changes: 72 additions & 1 deletion src/extensions/codemirror/CodeMirror.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useRef } from "react";
import React, { useMemo, useRef } from "react";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
import { foldKeymap } from "@codemirror/language";
import { lintGutter } from "@codemirror/lint";
import { EditorState, Extension } from "@codemirror/state";
import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } from "@codemirror/view";
import { minimalSetup } from "codemirror";

import { IntentTypes } from "../../common/Intent";
import { markField } from "../../components/AutoSuggestion/extensions/markText";
import { CLASSPREFIX as eccgui } from "../../configuration/constants";

Expand All @@ -14,6 +16,8 @@ import {
supportedCodeEditorModes,
useCodeMirrorModeExtension,
} from "./hooks/useCodemirrorModeExtension.hooks";
import { jsLinter } from "./linters/jsLinter";
import { turtleLinter } from "./linters/turtleLinter";
//adaptations
import {
adaptedCodeFolding,
Expand All @@ -25,6 +29,7 @@ import {
adaptedLineNumbers,
adaptedPlaceholder,
} from "./tests/codemirrorTestHelper";
import { EditorMode, ExtensionCreator } from "./types";

export interface CodeEditorProps {
// Is called with the editor instance that allows access via the CodeMirror API
Expand Down Expand Up @@ -133,13 +138,38 @@ export interface CodeEditorProps {
* If the <Tab> key is enabled as normal input, i.e. it won't have the behavior of changing to the next input element, expected in a web app.
*/
enableTab?: boolean;
/**
* Enables linting feature in the editor.
*/
useLinting?: boolean;
/**
* Id used for testing
*/
dataTestId?: string;
/**
* Autofocus the editor when it is rendered
*/
autoFocus?: boolean;
/**
* Intent state of the code editor.
*/
intent?: IntentTypes | "edited" | "removed";
/**
* Disables the editor.
*/
disabled?: boolean;
}

const addExtensionsFor = (flag: boolean, ...extensions: Extension[]) => (flag ? [...extensions] : []);
const addToKeyMapConfigFor = (flag: boolean, ...keys: any) => (flag ? [...keys] : []);
const addHandlersFor = (flag: boolean, handlerName: string, handler: any) =>
flag ? ({ [handlerName]: handler } as DOMEventHandlers<any>) : {};

const ModeLinterMap: ReadonlyMap<EditorMode, ReadonlyArray<ExtensionCreator>> = new Map([
[EditorMode.Turtle, [turtleLinter]],
[EditorMode.JavaScript, [jsLinter]],
]);

/**
* Includes a code editor, currently we use CodeMirror library as base.
*/
Expand Down Expand Up @@ -170,9 +200,29 @@ export const CodeEditor = ({
tabForceSpaceForModes = ["python", "yaml"],
enableTab = false,
height,
useLinting = false,
dataTestId,
autoFocus = false,
disabled = false,
intent,
}: CodeEditorProps) => {
const parent = useRef<any>(undefined);

const linters = useMemo(() => {
if (!mode) {
return [];
}

const values = [lintGutter()];

const linters = ModeLinterMap.get(mode as EditorMode);
if (linters) {
values.push(...linters.map((linter) => linter()));
}

return values;
}, [mode]);

const onKeyDownHandler = (event: KeyboardEvent, view: EditorView) => {
if (onKeyDown && !onKeyDown(event)) {
if (event.key === "Enter") {
Expand Down Expand Up @@ -219,13 +269,20 @@ export const CodeEditor = ({
keymap?.of(keyMapConfigs),
EditorState?.tabSize.of(tabIntentSize),
EditorState?.readOnly.of(readOnly),
EditorView?.editable.of(!disabled),
AdaptedEditorViewDomEventHandlers(domEventHandlers) as Extension,
EditorView?.updateListener.of((v: ViewUpdate) => {
if (disabled) return;

onChange && onChange(v.state.doc.toString());

if (onSelection)
onSelection(v.state.selection.ranges.filter((r) => !r.empty).map(({ from, to }) => ({ from, to })));

if (onFocusChange) {
v.view.dom.className += ` ${eccgui}-intent--${intent}`;
}

if (onCursorChange) {
const cursorPosition = v.state.selection.main.head ?? 0;
const editorRect = v.view.dom.getBoundingClientRect();
Expand All @@ -250,6 +307,7 @@ export const CodeEditor = ({
addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine()),
addExtensionsFor(wrapLines, EditorView?.lineWrapping),
addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding()),
addExtensionsFor(useLinting, ...linters),
additionalExtensions,
];

Expand All @@ -265,6 +323,18 @@ export const CodeEditor = ({
view.dom.style.height = typeof height === "string" ? height : `${height}px`;
}

if (disabled) {
view.dom.className += ` ${eccgui}-disabled`;
}

if (intent) {
view.dom.className += ` ${eccgui}-intent--${intent}`;
}

if (autoFocus) {
view.focus();
}

setEditorView && setEditorView(view);

return () => {
Expand All @@ -280,6 +350,7 @@ export const CodeEditor = ({
id={id ? id : name ? `codemirror-${name}` : undefined}
ref={parent}
data-test-id="codemirror-wrapper"
data-testid={dataTestId ? `${dataTestId}_codemirror}` : undefined}
className={
`${eccgui}-codeeditor ${eccgui}-codeeditor--mode-${mode}` +
(outerDivAttributes?.className ? ` ${outerDivAttributes?.className}` : "")
Expand Down
Loading