Problem -> auto close «»

Hello and good day to all friends,
I have an issue in the editor I’ve built, and despite my efforts, I haven’t been able to solve it.
I am currently developing an editor for a new language, and in it, I need the following characters to be automatically closed, similar to how brackets {} are closed:
«»

How can I achieve this?

open character => “«”
closed character => “»”

And these characters are in Persian language

()
[]
{}

And the above characters work and are closed automatically

const EditorService = (autoCompleteData, callback) => {
  const CM = window?.CM;
  if (!CM) {
    console.error("Codemirror is not loaded");
    return;
  }
  const { basicSetup } = CM["codemirror"];
  const { keymap, EditorView } = CM["@codemirror/view"];
  const { closeBrackets, completeFromList, acceptCompletion, autocompletion } =
    CM["@codemirror/autocomplete"];
  const { LRParser } = CM["@lezer/lr"];
  const { LRLanguage, HighlightStyle } = CM["@codemirror/language"];
  const { indentWithTab, defaultKeymap, indentMore, indentLess } =
    CM["@codemirror/commands"];
  const { LanguageSupport } = CM["@codemirror/language"];
  const { foldNodeProp, foldInside, indentNodeProp } =
    CM["@codemirror/language"];
  const { styleTags, tags } = CM["@lezer/highlight"];

  const parser = LRParser.deserialize({
    version: 14,
    states:
      "#SQYQPOOO#wQQO'#C_OOQO'#Ce'#CeQYQPOOOOQO'#Ca'#CaO$OQQO'#CfO%`QQO'#C`O%gQPO,58yOOQO-E6c-E6cO%lQPO'#CgO%tQQO,59QOOQO-E6d-E6dOOQO1G.e1G.eOOQO'#Cb'#CbOOQO,59R,59ROOQO-E6e-E6e",
    stateData:
      "'U~O^OSPOS~O_POtPOuPOvPOwPOxPOyPOzPO{PO|PO}PO!OPO!PPO!QPO!RPO!SPO!TPO!UPO!VPO!WPO!XPO~O`SOaSObSOcSOdSOeSOfSOgSOhSOiSOjSOkSOlSOmSOnSOoSOpSOqSO~OsSP~P!mOrXO`YXaYXbYXcYXdYXeYXfYXgYXhYXiYXjYXkYXlYXmYXnYXoYXpYXqYXsYX~OsSX~P!mOs[O~OV]OW]O~OrXO`YaaYabYacYadYaeYafYagYahYaiYajYakYalYamYanYaoYapYaqYasYa~O",
    goto: "}[PPP]adhPPkqwTQORRVPTTPUR^XQRORWRQUPRZUQYTR_Y",
    nodeNames:
      "⚠ LineComment Program Command Body Property Value String Identifier",
    maxTerm: 55,
    skippedNodes: [0, 1],
    repeatNodeCount: 3,
    tokenData: `@top Program { Layout }

@skip { Space | Comment }

TagContent {
  Tag |
  Attribute
}

Finish {
  "تمام"
}

Layout {
  "صفحه" Start Body Finish
}

Tag {
  Key Start Body Finish
}

Body {
  TagContent*
}

Attribute {
  Key Equal Value
}

Value {
  String |
  Number
}

@tokens {
  String { '"' (!["\\\\] | "\\\\" _)* '"' | "«" (!["\\\\»] | "\\\\" _)* "»" }
  Comment {
    "//" ![\\n]* |
    "/*" ( (!"*/" any)* ) "*/"
  }
  Number { ($[0-9\u0660-\u0669\u06F0-\u06F9])+ ("." ($[0-9\u0660-\u0669\u06F0-\u06F9])+)? }
  Start { ":" }
  Equal { "=" }

  Key { $[a-zA-Z_\\-0-9\u0600-\u06FF]+ (" " $[a-zA-Z_\\-0-9\u0600-\u06FF]+)* }
  Space { $[ \\t\\n\\r]+ }
}`,
    tokenizers: [0, 1],
    topRules: { Program: [0, 2] },
    tokenPrec: 0,
  });

  let before_space = 0;

  function getSpacesBeforeCursor(state) {
    const cursorPos = state.selection.main.head;
    const line = state.doc.lineAt(cursorPos);

    let spacesCount = 0;
    for (let i = line.from; i < cursorPos; i++) {
      if (line.text.charAt(i - line.from) === " ") {
        spacesCount++;
      }
    }

    return spacesCount;
  }

  const SALAMLanguage = LRLanguage.define({
    parser: parser.configure({
      props: [
        indentNodeProp.add({
          Application: (context) =>
            context.column(context.node.from) + context.unit,
        }),
        foldNodeProp.add({
          Application: foldInside,
        }),
        styleTags({
          Identifier: tags.variableName,
          Boolean: tags.bool,
          String: tags.string,
          LineComment: tags.lineComment,
          "( )": tags.paren,
        }),
      ],
    }),
    languageData: {
      commentTokens: {
        line: "//",
        block: { open: "/*", close: "*/" },
      },
    },
  });

  const exampleCompletion = SALAMLanguage.data.of({
    autocomplete: completeFromList(autoCompleteData()),
  });

  function SALAM() {
    return new LanguageSupport(SALAMLanguage, [exampleCompletion]);
  }

  const elm_editor = document.querySelector("#editor");

  const tab = (view) => {
    const result = acceptCompletion(view);
    if (result) {
      const cursorPos = view.state.selection.main.head;
      const line = view.state.doc.lineAt(cursorPos);
      const targetLine = parseInt(line.number) - 1;
      const targetPos = view.state.doc.line(targetLine).from;
      view.dispatch({
        selection: { anchor: targetPos, head: targetPos },
      });
    }
    return result;
  };

  console.log(closeBrackets);

  const editor_options = {
    parent: elm_editor,
    styleActiveLine: true,
    lineNumbers: true,
    matchBrackets: true,
    viewportMargin: Infinity,
    autoCloseBrackets: true,
    extensions: [
      basicSetup,
      closeBrackets({ brackets: "()[]{}«»" }),
      SALAM(),
      autocompletion(),
      keymap.of([
        {
          key: "Tab",
          run: (view) => tab(view),
        },
        indentWithTab,
        ...defaultKeymap,
      ]),
      EditorView.updateListener.of((update) => {
        if (update.changes) {
          console.log(before_space);
          const newText = update.state.doc.toString();
          callback(newText);
          localStorage.setItem("code", newText);
        }
      }),
    ],
    doc: localStorage.getItem("code"),
  };

  let editor = new EditorView(editor_options).focus();
};

export default EditorService;

github:

1 Like

Your editor_options is full of options that aren’t actually recognized by new EditorView, suggesting you’ve either adapted a CodeMirror 5 example to version 6 without looking too closely, or you’re just trying stuff and seeing what sticks. I’d recommend looking closely at the docs and/or TypeScript types, so that the things you are doing actually match what the library expects.

Specifically regarding your question, the closeBrackets function takes no arguments, so the object you are passing in there cannot do anything. Bracket closing is configured per-language, so something like this (used as an extension) might work:

SALAMLanguage.data.of({closeBrackets: {brackets: ["(", "[", "{", "'", '"', "«"]}})

(Similar to what you’re doing for autocompletion.)

But in testing that out, I found that @codemirror/autocomplete has a bug where it closes « with a ¬ character, so you’ll have to upgrade that to 6.18.6 to get working autoclosing for that character.

2 Likes

Hey Marijn,
Many thanks for your help and support—it’s a pleasure to be in touch with you.

I was reviewing your recent commits and came across this one:

I noticed this line:

const definedClosing = "()[]{}<>«»»«[]{}"

Could you explain a bit more about this string? I see that after «», the sequence repeats in reverse order as »«. I’m curious about the reasoning behind this pattern.

Thanks in advance!

Best

I checked the CodeMirror CDN services, such as jsdelivr (codemirror CDN by jsDelivr - A CDN for npm and GitHub), and it seems that the latest version is not available there. Since we are using a bundled version of CodeMirror that includes all of its dependencies and libraries, I’m unsure how to find or generate a single JavaScript file that includes everything.

At the moment, we are using the following files:

Could you advise on how we can generate or find a complete bundled version of CodeMirror with all its dependencies? (Version 6.18.6 as you mentioned earlier)

I can see a new release in @codemirror/autocomplete: Release Version 6.18.6 · codemirror/autocomplete · GitHub

It lists the bracketing characters where the closing character is not the Unicode code point directly after the opening one (for example, and follow each other). Some languages use guillemets like «this» and others like »this«, so I included both in there.

1 Like

That is some 5.x version of CodeMirror. The solution I gave won’t work for that. 6.x is released as a bunch of separately versioned @codemirror/… packages.

1 Like