From e614dcbac123239c5bb2e4c61085427dd7cf56b0 Mon Sep 17 00:00:00 2001 From: Jose Vargas Date: Wed, 24 May 2023 06:56:13 -0600 Subject: [PATCH] Add doctest decorations to monaco editor per result (#1911) --- assets/css/editor.css | 34 ++++++++++++++++ assets/js/hooks/cell.js | 22 +++++++++++ assets/js/hooks/cell_editor/live_editor.js | 46 ++++++++++++++++++++++ lib/livebook/runtime/evaluator/doctests.ex | 25 +++++++++++- lib/livebook/session/data.ex | 13 ++++++ lib/livebook_web/live/session_live.ex | 18 +++++++++ 6 files changed, 157 insertions(+), 1 deletion(-) diff --git a/assets/css/editor.css b/assets/css/editor.css index 389650f07..8024a34ea 100644 --- a/assets/css/editor.css +++ b/assets/css/editor.css @@ -158,3 +158,37 @@ Also some spacing adjustments. margin-left: 2px; transform: none; } + +/* To style circles for doctest results */ +.line-circle-red { + background-color: red; + box-sizing: border-box; + border-radius: 100%; + border-style: solid; + border-width: 2px; + max-width: 15px; + height: 15px !important; + margin: 3px; +} + +.line-circle-green { + background-color: green; + box-sizing: border-box; + border-radius: 100%; + border-style: solid; + border-width: 2px; + max-width: 15px; + height: 15px !important; + margin: 3px; +} + +.line-circle-grey { + background-color: grey; + box-sizing: border-box; + border-radius: 100%; + border-style: solid; + border-width: 2px; + max-width: 15px; + height: 15px !important; + margin: 3px; +} diff --git a/assets/js/hooks/cell.js b/assets/js/hooks/cell.js index 3766a2c1e..d73f7c5db 100644 --- a/assets/js/hooks/cell.js +++ b/assets/js/hooks/cell.js @@ -242,6 +242,28 @@ const Cell = { liveEditor.setCodeErrorMarker(code_error); } ); + + this.handleEvent(`start_evaluation:${this.props.cellId}`, () => { + liveEditor.clearDoctestDecorations(); + }); + + this.handleEvent( + `doctest_result:${this.props.cellId}`, + ({ state, line }) => { + console.log({ state, line }); + switch (state) { + case "evaluating": + liveEditor.addEvaluatingDoctestDecoration(line); + break; + case "success": + liveEditor.addSuccessDoctestDecoration(line); + break; + case "failed": + liveEditor.addFailedDoctestDecoration(line); + break; + } + } + ); } } }, diff --git a/assets/js/hooks/cell_editor/live_editor.js b/assets/js/hooks/cell_editor/live_editor.js index 9725695a4..f8d4d0cb7 100644 --- a/assets/js/hooks/cell_editor/live_editor.js +++ b/assets/js/hooks/cell_editor/live_editor.js @@ -35,6 +35,15 @@ class LiveEditor { this._onBlur = []; this._onCursorSelectionChange = []; this._remoteUserByClientId = {}; + /* For doctest decorations we store the params to create the + * decorations and also the result of creating the decorations. + * The params are IModelDeltaDecoration from https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IModelDeltaDecoration.html + * and the result is IEditorDecorationsCollection from https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorDecorationsCollection.html + */ + this._doctestDecorations = { + deltaDecorations: {}, + decorationCollection: null, + }; const serverAdapter = new HookServerAdapter(hook, cellId, tag); this.editorClient = new EditorClient(serverAdapter, revision); @@ -270,6 +279,9 @@ class LiveEditor { : "off", }); + this._doctestDecorations.decorationCollection = + this.editor.createDecorationsCollection([]); + this.editor.addAction({ contextMenuGroupId: "word-wrapping", id: "enable-word-wrapping", @@ -566,6 +578,40 @@ class LiveEditor { ); }); } + + clearDoctestDecorations() { + this._doctestDecorations.decorationCollection.clear(); + this._doctestDecorations.deltaDecorations = {}; + } + + _createDoctestDecoration(lineNumber, className) { + return { + range: new monaco.Range(lineNumber, 1, lineNumber, 1), + options: { + isWholeLine: true, + linesDecorationsClassName: className, + }, + }; + } + + _addDoctestDecoration(line, className) { + const newDecoration = this._createDoctestDecoration(line, className); + this._doctestDecorations.deltaDecorations[line] = newDecoration; + const decos = Object.values(this._doctestDecorations.deltaDecorations); + this._doctestDecorations.decorationCollection.set(decos); + } + + addSuccessDoctestDecoration(line) { + this._addDoctestDecoration(line, "line-circle-green"); + } + + addFailedDoctestDecoration(line) { + this._addDoctestDecoration(line, "line-circle-red"); + } + + addEvaluatingDoctestDecoration(line) { + this._addDoctestDecoration(line, "line-circle-grey"); + } } function completionItemsToSuggestions(items, settings) { diff --git a/lib/livebook/runtime/evaluator/doctests.ex b/lib/livebook/runtime/evaluator/doctests.ex index 3810525ca..af9ae63b0 100644 --- a/lib/livebook/runtime/evaluator/doctests.ex +++ b/lib/livebook/runtime/evaluator/doctests.ex @@ -20,7 +20,12 @@ defmodule Livebook.Runtime.Evaluator.Doctests do tests = test_module.tests |> Enum.sort_by(& &1.tags.doctest_line) - |> Enum.map(&run_test/1) + |> Enum.map(fn test -> + report_doctest_state(:evaluating, test) + test = run_test(test) + report_doctest_state(:success_or_failed, test) + test + end) formatted = format_results(tests) put_output({:text, formatted}) @@ -35,6 +40,24 @@ defmodule Livebook.Runtime.Evaluator.Doctests do :ok end + defp report_doctest_state(:evaluating, test) do + result = %{ + doctest_line: test.tags.doctest_line, + state: :evaluating + } + + put_output({:doctest_result, result}) + end + + defp report_doctest_state(:success_or_failed, test) do + result = %{ + doctest_line: test.tags.doctest_line, + state: get_in(test, [Access.key(:state), Access.elem(0)]) || :success + } + + put_output({:doctest_result, result}) + end + defp define_test_module(modules) do id = modules diff --git a/lib/livebook/session/data.ex b/lib/livebook/session/data.ex index 84998b8a2..f99557e24 100644 --- a/lib/livebook/session/data.ex +++ b/lib/livebook/session/data.ex @@ -536,6 +536,19 @@ defmodule Livebook.Session.Data do end end + def apply_operation( + data, + {:add_cell_evaluation_output, _client_id, id, {:doctest_result, _result}} + ) do + with {:ok, _cell, _} <- Notebook.fetch_cell_and_section(data.notebook, id) do + data + |> with_actions() + |> wrap_ok() + else + _ -> :error + end + end + def apply_operation(data, {:add_cell_evaluation_output, _client_id, id, output}) do with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, id) do data diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 767e75756..86181d2b4 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -1760,6 +1760,17 @@ defmodule LivebookWeb.SessionLive do end end + defp after_operation( + socket, + _prev_socket, + {:add_cell_evaluation_output, _client_id, cell_id, {:doctest_result, result}} + ) do + push_event(socket, "doctest_result:#{cell_id}", %{ + state: result.state, + line: result.doctest_line + }) + end + defp after_operation( socket, _prev_socket, @@ -1821,6 +1832,10 @@ defmodule LivebookWeb.SessionLive do end end + defp handle_action(socket, {:start_evaluation, cell, _section}) do + push_event(socket, "start_evaluation:#{cell.id}", %{}) + end + defp handle_action(socket, _action), do: socket defp client_info(id, user) do @@ -2229,6 +2244,9 @@ defmodule LivebookWeb.SessionLive do data_view + {:add_cell_evaluation_output, _client_id, _cell_id, {:doctest_result, _result}} -> + data_view + {:add_cell_evaluation_output, _client_id, cell_id, {:stdout, text}} -> # Lookup in previous data to see if the output is already there case Notebook.fetch_cell_and_section(prev_data.notebook, cell_id) do