Add code formatting integration to Elixir cells (#416)

This commit is contained in:
Jonatan Kłosko 2021-07-01 11:50:04 +02:00 committed by GitHub
parent 834b88905c
commit 86316e9460
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 20 deletions

View file

@ -23,6 +23,7 @@ class LiveEditor {
if (type === "elixir") {
this.__setupCompletion();
this.__setupFormatting();
}
const serverAdapter = new HookServerAdapter(hook, cellId);
@ -247,6 +248,58 @@ class LiveEditor {
}
);
}
__setupFormatting() {
/**
* Similarly to completion, formatting is delegated to the function
* defined below, where we simply communicate with LV to get
* a formatted version of the current editor content.
*/
this.editor.getModel().__getDocumentFormattingEdits = (model) => {
const content = model.getValue();
return new Promise((resolve, reject) => {
this.hook.pushEvent(
"format_code",
{ code: content },
({ code: formatted }) => {
/**
* We use a single edit replacing the whole editor content,
* but the editor itself optimises this into a list of edits
* that produce minimal diff using the Myers string difference.
*
* References:
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L324
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/common/services/editorSimpleWorker.ts#L489
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/base/common/diff/diff.ts#L227-L231
*
* Eventually the editor will received the optimised list of edits,
* which we then convert to Delta and send to the server.
* Consequently, the Delta carries only the minimal formatting diff.
*
* Also, if edits are applied to the editor, either by typing
* or receiving remote changes, the formatting is cancelled.
* In other words the formatting changes are actually applied
* only if the editor stays intact.
*
* References:
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L313
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/browser/core/editorState.ts#L137
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L326
*/
const replaceEdit = {
range: model.getFullModelRange(),
text: formatted,
};
resolve([replaceEdit]);
}
);
});
};
}
}
function completionItemsToSuggestions(items) {

View file

@ -40,6 +40,19 @@ monaco.languages.registerCompletionItemProvider("elixir", {
},
});
// Define custom code formatting provider.
// Formatting is cell agnostic, but we still delegate
// to a cell specific implementation to communicate with LV.
monaco.languages.registerDocumentFormattingEditProvider("elixir", {
provideDocumentFormattingEdits: function (model, options, token) {
if (model.__getDocumentFormattingEdits) {
return model.__getDocumentFormattingEdits(model);
} else {
return [];
}
},
});
export default monaco;
/**

View file

@ -612,6 +612,19 @@ defmodule LivebookWeb.SessionLive do
{:noreply, socket}
end
def handle_event("format_code", %{"code" => code}, socket) do
formatted =
try do
code
|> Code.format_string!()
|> IO.iodata_to_binary()
rescue
_ -> code
end
{:reply, %{code: formatted}, socket}
end
defp create_session(socket, opts) do
case SessionSupervisor.create_session(opts) do
{:ok, id} ->

View file

@ -4,13 +4,24 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
@shortcuts %{
insert_mode: [
%{seq: ["esc"], desc: "Switch back to navigation mode"},
%{seq: ["ctrl", ""], press_all: true, desc: "Evaluate cell and stay in insert mode"},
%{
seq: ["ctrl", ""],
seq_mac: ["", ""],
press_all: true,
desc: "Evaluate cell and stay in insert mode"
},
%{seq: ["tab"], desc: "Autocomplete expression when applicable"},
%{
seq: ["ctrl", ""],
press_all: true,
transform_for_mac: false,
desc: "Show completion list, use twice for details"
},
%{
seq: ["ctrl", "shift", "i"],
seq_mac: ["", "", "f"],
seq_windows: ["shift", "alt", "f"],
press_all: true,
desc: "Format Elixir code"
}
],
navigation_mode: [
@ -36,7 +47,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
%{seq: ["s", "b"], desc: "Show bin"}
],
universal: [
%{seq: ["ctrl", "s"], press_all: true, desc: "Save notebook"}
%{seq: ["ctrl", "s"], seq_mac: ["", "s"], press_all: true, desc: "Save notebook"}
]
}
@ -74,7 +85,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
<h3 class="text-lg font-medium text-gray-900 pt-4">
<%= @title %>
</h3>
<div class="mt-2 flex sm:flex-row flex-col">
<div class="mt-2 flex flex-col sm:flex-row sm:space-x-2">
<div class="flex-grow">
<%= render_shortcuts_section_table(@left, @platform) %>
</div>
@ -107,18 +118,12 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
end
defp render_shortcut_seq(shortcut, platform) do
seq =
if platform == :mac and Map.get(shortcut, :transform_for_mac, true) do
seq_for_mac(shortcut.seq)
else
shortcut.seq
end
seq = shortcut[:"seq_#{platform}"] || shortcut.seq
press_all = Map.get(shortcut, :press_all, false)
joiner =
if press_all do
remix_icon("add-line", class: "text-xl text-gray-600")
remix_icon("add-line", class: "text-lg text-gray-600")
end
elements = Enum.map_intersperse(seq, joiner, &content_tag("kbd", &1))
@ -133,14 +138,6 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
"""
end
defp seq_for_mac(seq) do
Enum.map(seq, fn
"ctrl" -> ""
"alt" -> ""
key -> key
end)
end
defp split_in_half(list) do
half_idx = list |> length() |> Kernel.+(1) |> div(2)
Enum.split(list, half_idx)