Showing inline errors in codemirror

2024-06-08

Problem

Our website has hundreds of different pages, and many of them have a split-page layout:

  • Left side is a code editor (we use CodeMirror for that)
  • Right side is some execution result (e.g., SQL table output, Graphviz/Mermaid graph, regex/JQ/JSONPath result, etc.) split page layout

When an issue occurs on the right side, it's critical to point the user to the exact location in the code editor so they can fix it. For example, this is bad experience - we just show an error that underlying engine throws. bad error display

Solution

There are 2 possible solutions:

CodeMirror linter with matching rules to the execution engine

For example, this works well for us on the Graphviz Playground, where we have a Graphviz linter + Graphviz rendering. Independent execution usually matches errors, so it feels like when render fails, we show the correct error.

Here is how it's done in the Graphviz code editor (CodeMirror):

const syntaxLinter = linter((view) => {
  let diagnostics: Diagnostic[] = [];
  let graphtype: string | null = null;
  ...
  return diagnostics;
});

let state = EditorState.create({
    doc: defaultValue,
    extensions: [
        basicSetup,
        ...
        syntaxLinter,
        theme,
    ]
});

graphviz error display

Manually show execution engine errors in CodeMirror

We just started this approach with our DDL to DBML/grpahviz conversion page.

We are using DBML-core library to parse DDL, it throws an error with line number and column number when parsing failed. We are catching this error and converting that to a codemirror diagnostics object.

Construct correct codemirror offset for diagnostics error

First we convert the error to our internal type.

try {
    databaseObj = Parser.parse(source, 'mysql');
} catch (e) {
    if (e instanceof CompilerError) {
        const compilerError = e as CompilerError;
        const diag: CompilerDiagnostic = compilerError.diags[0];
        const editorError: EditorError = {
            name: diag.type || 'Error',
            message: diag.message.replace(/\\n/g, '\n'),
            location: {
              start: diag.location.start,
              end: diag.location.end
            }
          }
        setEditorError(editorError);
    }
}   

then we convert that into CodeMirror diagnostics object

let diagnostics = [
    {
        from: startPosition,
        to: endPosition,
        severity: "error" as "error",
        message: error.message
    }
];

 editorRef.current.view?.dispatch(
    setDiagnostics(editorRef.current.state!, diagnostics)
);

The result looks like this result error inline

Now we plan to apply the same idea to other pages like SQL playground, MermaidJS playground, Regex tester, JQ playground etc.