From 8e4b4af60cf43fb3b288c6c8843c20897b61f860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Fri, 22 Jan 2021 23:27:25 +0100 Subject: [PATCH] Add Elixir language support to the editor (#13) --- .../editor/elixir/language_configuration.js | 37 ++++++ .../on_type_formatting_edit_provider.js | 118 ++++++++++++++++++ assets/js/editor/monaco.js | 17 ++- 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 assets/js/editor/elixir/language_configuration.js create mode 100644 assets/js/editor/elixir/on_type_formatting_edit_provider.js diff --git a/assets/js/editor/elixir/language_configuration.js b/assets/js/editor/elixir/language_configuration.js new file mode 100644 index 000000000..d3dc42bf8 --- /dev/null +++ b/assets/js/editor/elixir/language_configuration.js @@ -0,0 +1,37 @@ +/** + * Defines Elixir traits to enable various editor features, + * like automatic bracket insertion and indentation. + */ +const ElixirLanguageConfiguration = { + comments: { + lineComment: "#", + }, + brackets: [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ], + surroundingPairs: [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["'", "'"], + ['"', '"'], + ], + autoClosingPairs: [ + { open: "'", close: "'", notIn: ["string", "comment"] }, + { open: '"', close: '"', notIn: ["comment"] }, + { open: '"""', close: '"""' }, + { open: "`", close: "`", notIn: ["string", "comment"] }, + { open: "(", close: ")" }, + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "<<", close: ">>" }, + ], + indentationRules: { + increaseIndentPattern: /^\s*(after|else|catch|rescue|fn|[^#]*(do|<\-|\->|\{|\[|\=))\s*$/, + decreaseIndentPattern: /^\s*((\}|\])\s*$|(after|else|catch|rescue|end)\b)/, + }, +}; + +export default ElixirLanguageConfiguration; diff --git a/assets/js/editor/elixir/on_type_formatting_edit_provider.js b/assets/js/editor/elixir/on_type_formatting_edit_provider.js new file mode 100644 index 000000000..b14c86c20 --- /dev/null +++ b/assets/js/editor/elixir/on_type_formatting_edit_provider.js @@ -0,0 +1,118 @@ +/** + * Defines custom auto-formatting behavior for Elixir. + * + * The provider is triggered when the user makes edits + * and it may instruct the editor to apply some additional changes. + */ +const ElixirOnTypeFormattingEditProvider = { + autoFormatTriggerCharacters: ["\n"], + provideOnTypeFormattingEdits(model, position, char, options, token) { + if (char === "\n") { + return closingEndTextEdits(model, position); + } + + return []; + }, +}; + +function closingEndTextEdits(model, position) { + const lines = model.getLinesContent(); + const lineIndex = position.lineNumber - 1; + const line = lines[lineIndex]; + const prevLine = lines[lineIndex - 1]; + const prevIndentation = indentation(prevLine); + + if (shouldInsertClosingEnd(lines, lineIndex)) { + // If this is the last line or the line is not empty, + // we have to insert a newline at the current position. + // Otherwise we prefer to explicitly insert the closing end + // in the next line, as it preserves current cursor position. + const shouldInsertInNextLine = + position.lineNumber < lines.length && isBlank(line); + + const textEdit = insertClosingEndTextEdit( + position, + prevIndentation, + shouldInsertInNextLine + ); + + return [textEdit]; + } + + return []; +} + +function shouldInsertClosingEnd(lines, lineIndex) { + const prevLine = lines[lineIndex - 1]; + const prevIndentation = indentation(prevLine); + const prevTokens = tokens(prevLine); + + if ( + last(prevTokens) === "do" || + (prevTokens.includes("fn") && last(prevTokens) === "->") + ) { + const nextLineWithSameIndentation = lines + .slice(lineIndex + 1) + .filter(line => !isBlank(line)) + .find((line) => indentation(line) === prevIndentation); + + if (nextLineWithSameIndentation) { + const [firstToken] = tokens(nextLineWithSameIndentation); + + if (["after", "else", "catch", "rescue", "end"].includes(firstToken)) { + return false; + } + } + + return true; + } + + return false; +} + +function insertClosingEndTextEdit( + position, + indentation, + shouldInsertInNextLine +) { + if (shouldInsertInNextLine) { + return { + range: new monaco.Range( + position.lineNumber + 1, + 1, + position.lineNumber + 1, + 1 + ), + text: `${indentation}end\n`, + }; + } else { + return { + range: new monaco.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + ), + text: `\n${indentation}end`, + }; + } +} + +function indentation(line) { + const [indentation] = line.match(/^\s*/); + return indentation; +} + +function tokens(line) { + return line.replace(/#.*/, "").match(/->|[\w:]+/g) || []; +} + +function last(list) { + return list[list.length - 1]; +} + +function isBlank(string) { + return string.trim() === ""; +} + +export default ElixirOnTypeFormattingEditProvider; diff --git a/assets/js/editor/monaco.js b/assets/js/editor/monaco.js index ad5cdb4ca..85e528996 100644 --- a/assets/js/editor/monaco.js +++ b/assets/js/editor/monaco.js @@ -1,5 +1,20 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import ElixirLanguageConfiguration from "./elixir/language_configuration"; +import ElixirOnTypeFormattingEditProvider from "./elixir/on_type_formatting_edit_provider"; -// TODO: add Elixir language definition +// Register the Elixir language and add relevant configuration +monaco.languages.register({ id: "elixir" }); + +monaco.languages.setLanguageConfiguration( + "elixir", + ElixirLanguageConfiguration +); + +monaco.languages.registerOnTypeFormattingEditProvider( + "elixir", + ElixirOnTypeFormattingEditProvider +); + +// TODO: add Monarch tokenizer for syntax highlighting export default monaco;