mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-17 22:30:03 +08:00
Add code formatting integration to Elixir cells (#416)
This commit is contained in:
parent
834b88905c
commit
86316e9460
4 changed files with 96 additions and 20 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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} ->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue