mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 04:54:29 +08:00
Add Elixir language support to the editor (#13)
This commit is contained in:
parent
b8df31e1ae
commit
8e4b4af60c
3 changed files with 171 additions and 1 deletions
37
assets/js/editor/elixir/language_configuration.js
Normal file
37
assets/js/editor/elixir/language_configuration.js
Normal file
|
@ -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;
|
118
assets/js/editor/elixir/on_type_formatting_edit_provider.js
Normal file
118
assets/js/editor/elixir/on_type_formatting_edit_provider.js
Normal file
|
@ -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;
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue