Skip to content

Commit

Permalink
Merge branch 'main' into post-init
Browse files Browse the repository at this point in the history
  • Loading branch information
meatballs committed Jul 8, 2023
2 parents 8be9827 + ff3deba commit 6d3bfab
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 34 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Magma is a NeoVim plugin for running code interactively with Jupyter.
- [`cairosvg`](https://cairosvg.org/) (for displaying SVG images)
- [`pnglatex`](https://pypi.org/project/pnglatex/) (for displaying TeX formulas)
- `plotly` and `kaleido` (for displaying Plotly figures)
- `pyperclip` if you want to use `magma_copy_output`
- For .NET (C#, F#)
- `dotnet tool install -g Microsoft.dotnet-interactive`
- `dotnet interactive jupyter install`

You can do a `:checkhealth` to see if you are ready to go.

Expand Down Expand Up @@ -59,6 +63,37 @@ let g:magma_image_provider = "ueberzug"

**Note:** The options that are altered here don't have these as their default values in order to provide a simpler (albeit perhaps a bit more inconvenient) UI for someone who just added the plugin without properly reading the README.

To make initialisation of kernels easier, you can add these commands:

```lua
function MagmaInitPython()
vim.cmd[[
:MagmaInit python3
:MagmaEvaluateArgument a=5
]]
end

function MagmaInitCSharp()
vim.cmd[[
:MagmaInit .net-csharp
:MagmaEvaluateArgument Microsoft.DotNet.Interactive.Formatting.Formatter.SetPreferredMimeTypesFor(typeof(System.Object),"text/plain");
]]
end

function MagmaInitFSharp()
vim.cmd[[
:MagmaInit .net-fsharp
:MagmaEvaluateArgument Microsoft.DotNet.Interactive.Formatting.Formatter.SetPreferredMimeTypesFor(typeof<System.Object>,"text/plain")
]]
end

vim.cmd[[
:command MagmaInitPython lua MagmaInitPython()
:command MagmaInitCSharp lua MagmaInitCSharp()
:command MagmaInitFSharp lua MagmaInitFSharp()
]]
```

## Usage

The plugin provides a bunch of commands to enable interaction. It is recommended to map most of them to keys, as explained in [Keybindings](#keybindings). However, this section will refer to the commands by their names (so as to not depend on some specific mappings).
Expand Down Expand Up @@ -141,6 +176,14 @@ nnoremap <expr> <LocalLeader>r nvim_exec('MagmaEvaluateOperator', v:true)

Upon using this mapping, you will enter operator mode, with which you will be able to select text you want to execute. You can, of course, hit ESC to cancel, as usual with operator mode.

#### MagmaEvaluateArgument

Evaluate the text following this command. Could be used for some automation (e. g. run something on initialization of a kernel).

```vim
:MagmaEvaluateArgument a=5;
```

#### MagmaReevaluateCell

Reevaluate the currently selected cell.
Expand Down Expand Up @@ -307,6 +350,12 @@ Where to save/load with [`:MagmaSave`](#magmasave) and [`:MagmaLoad`](#magmaload

The generated file is placed in this directory, with the filename itself being the buffer's name, with `%` replaced by `%%` and `/` replaced by `%`, and postfixed with the extension `.json`.

### `g:magma_copy_output`

Defaults to `v:false`.

To copy the evaluation output to clipboard automatically.

### [DEBUG] `g:magma_show_mimetype_debug`

Defaults to `v:false`.
Expand Down Expand Up @@ -340,6 +389,10 @@ Here is a list of the currently handled mimetypes:

This already provides quite a bit of basic functionality. As development continues, more mimetypes will be added.

### Correct progress bars and alike stuff

![](./caret.gif)

### Notifications

We use the `vim.notify` API. This means that you can use plugins such as [rcarriga/nvim-notify](https://github.com/rcarriga/nvim-notify) for pretty notifications.
Binary file added caret.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion rplugin/python3/magma/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def _initialize_buffer(self, kernel_name: str) -> MagmaBuffer:

return magma

@pynvim.command("MagmaInit", nargs="?", sync=True) # type: ignore
@pynvim.command("MagmaInit", nargs="?", sync=True, complete='file') # type: ignore
@nvimui # type: ignore
def command_init(self, args: List[str]) -> None:
self._initialize_if_necessary()
Expand Down Expand Up @@ -224,13 +224,31 @@ def _do_evaluate(

magma.run_code(code, span)

def _do_evaluate_expr(self, expr):
self._initialize_if_necessary()

magma = self._get_magma(True)
assert magma is not None
bufno = self.nvim.current.buffer.number
span = Span(
DynamicPosition(self.nvim, self.extmark_namespace, bufno, 0, 0),
DynamicPosition(self.nvim, self.extmark_namespace, bufno, 0, 0),
)
magma.run_code(expr, span)

@pynvim.command("MagmaEnterOutput", sync=True) # type: ignore
@nvimui # type: ignore
def command_enter_output_window(self) -> None:
magma = self._get_magma(True)
assert magma is not None
magma.enter_output()

@pynvim.command("MagmaEvaluateArgument", nargs=1, sync=True)
@nvimui
def commnand_magma_evaluate_argument(self, expr) -> None:
assert len(expr) == 1
self._do_evaluate_expr(expr[0])

@pynvim.command("MagmaEvaluateVisual", sync=True) # type: ignore
@nvimui # type: ignore
def command_evaluate_visual(self) -> None:
Expand Down
18 changes: 16 additions & 2 deletions rplugin/python3/magma/magmabuffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,13 @@ def restart(self, delete_outputs: bool = False) -> None:
self.runtime.restart()

def run_code(self, code: str, span: Span) -> None:
self._delete_all_cells_in_span(span)
self.runtime.run_code(code)

if span in self.outputs:
self.outputs[span].clear_interface()
del self.outputs[span]

self.outputs[span] = OutputBuffer(self.nvim, self.canvas, self.options)
self.queued_outputs.put(span)

Expand Down Expand Up @@ -168,15 +172,25 @@ def _get_selected_span(self) -> Optional[Span]:

return selected

def _delete_all_cells_in_span(self, span: Span) -> None:
for output_span in reversed(list(self.outputs.keys())):
if (
output_span.begin in span
or output_span.end in span
or span.begin in output_span
or span.end in output_span
):
self.outputs[output_span].clear_interface()
del self.outputs[output_span]

def delete_cell(self) -> None:
self.selected_cell = self._get_selected_span()
if self.selected_cell is None:
return

self.outputs[self.selected_cell].clear_interface()
del self.outputs[self.selected_cell]

self.update_interface()

def update_interface(self) -> None:
if self.buffer.number != self.nvim.current.buffer.number:
return
Expand Down
6 changes: 5 additions & 1 deletion rplugin/python3/magma/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ class MagmaOptions:
cell_highlight_group: str
save_path: str
image_provider: str
copy_output: bool

def __init__(self, nvim: Nvim):
self.automatically_open_output = nvim.vars.get(
"magma_automatically_open_output", True
)
self.wrap_output = nvim.vars.get("magma_wrap_output", True)
self.wrap_output = nvim.vars.get("magma_wrap_output", False)
self.output_window_borders = nvim.vars.get(
"magma_output_window_borders", True
)
Expand All @@ -31,3 +32,6 @@ def __init__(self, nvim: Nvim):
os.path.join(nvim.funcs.stdpath("data"), "magma"),
)
self.image_provider = nvim.vars.get("magma_image_provider", "none")
self.copy_output = nvim.vars.get(
"magma_copy_output", False
)
8 changes: 8 additions & 0 deletions rplugin/python3/magma/outputbuffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ def show(self, anchor: Position) -> None: # XXX .show_outputs(_, anchor)
lines_str += chunktext
lineno += chunktext.count("\n")
lines = lines_str.rstrip().split("\n")
actualLines = []
for line in lines:
parts = line.split('\r')
last = parts[-1]
if last != "":
actualLines.append(last)
lines = actualLines
lineno = len(lines)
else:
lines = [lines_str]
self.display_buffer[0] = self._get_header_text(self.output) # TODO
Expand Down
16 changes: 8 additions & 8 deletions rplugin/python3/magma/outputchunks.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,21 @@ def place(
) -> str:
pass

# Adapted from [https://stackoverflow.com/a/14693789/4803382]:
ANSI_CODE_REGEX = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
def clean_up_text(text: str) -> str:
text = ANSI_CODE_REGEX.sub("", text)
text = text.replace("\r\n", "\n")
return text

class TextOutputChunk(OutputChunk):
text: str

# Adapted from [https://stackoverflow.com/a/14693789/4803382]:
ANSI_CODE_REGEX = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")

def __init__(self, text: str):
self.text = text

def _cleanup_text(self, text: str) -> str:
# Adapted from [https://stackoverflow.com/a/14693789/4803382]:
text = self.ANSI_CODE_REGEX.sub("", text)
text = text.replace("\r\n", "\n")
return text
return clean_up_text(text)

def place(
self,
Expand Down Expand Up @@ -209,7 +209,7 @@ def __init__(self, execution_count: Optional[int]):
def to_outputchunk(
alloc_file: Callable[
[str, str],
AbstractContextManager[Tuple[str, IO[bytes]]],
"AbstractContextManager[Tuple[str, IO[bytes]]]",
],
data: Dict[str, Any],
metadata: Dict[str, Any],
Expand Down
81 changes: 59 additions & 22 deletions rplugin/python3/magma/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from queue import Empty as EmptyQueueException
import os
import tempfile
import json

import jupyter_client

Expand All @@ -15,6 +16,7 @@
TextOutputChunk,
OutputStatus,
to_outputchunk,
clean_up_text
)


Expand All @@ -39,20 +41,40 @@ def __init__(self, kernel_name: str, options: MagmaOptions):
self.state = RuntimeState.STARTING
self.kernel_name = kernel_name

self.kernel_manager = jupyter_client.manager.KernelManager(
kernel_name=kernel_name
)
self.kernel_manager.start_kernel()
self.kernel_client = self.kernel_manager.client()
assert isinstance(
self.kernel_client,
jupyter_client.blocking.client.BlockingKernelClient,
)
self.kernel_client.start_channels()
if ".json" not in self.kernel_name:

self.external_kernel = True
self.kernel_manager = jupyter_client.manager.KernelManager(
kernel_name=kernel_name
)
self.kernel_manager.start_kernel()
self.kernel_client = self.kernel_manager.client()
assert isinstance(
self.kernel_client,
jupyter_client.blocking.client.BlockingKernelClient,
)
self.kernel_client.start_channels()

self.allocated_files = []

self.allocated_files = []
self.options = options

self.options = options
else:
kernel_file = kernel_name
self.external_kernel = True
# Opening JSON file
kernel_json = json.load(open(kernel_file))
# we have a kernel json
self.kernel_manager = jupyter_client.manager.KernelManager(
kernel_name=kernel_json["kernel_name"]
)
self.kernel_client = self.kernel_manager.client()

self.kernel_client.load_connection_file(connection_file=kernel_file)

self.allocated_files = []

self.options = options

def is_ready(self) -> bool:
return self.state.value > RuntimeState.STARTING.value
Expand All @@ -62,7 +84,8 @@ def deinit(self) -> None:
if os.path.exists(path):
os.remove(path)

self.kernel_client.shutdown()
if self.external_kernel is False:
self.kernel_client.shutdown()

def interrupt(self) -> None:
self.kernel_manager.interrupt_kernel()
Expand Down Expand Up @@ -96,21 +119,31 @@ def _append_chunk(
def _tick_one(
self, output: Output, message_type: str, content: Dict[str, Any]
) -> bool:

def copy_on_demand(content_ctor):
if self.options.copy_output:
import pyperclip
if type(content_ctor) is str:
pyperclip.copy(content_ctor)
else:
pyperclip.copy(content_ctor())

if output._should_clear:
output.chunks.clear()
output._should_clear = False

if message_type == "execute_input":
output.execution_count = content["execution_count"]
assert output.status != OutputStatus.DONE
if output.status == OutputStatus.HOLD:
output.status = OutputStatus.RUNNING
elif output.status == OutputStatus.RUNNING:
output.status = OutputStatus.DONE
else:
raise ValueError(
"bad value for output.status: %r" % output.status
)
if self.external_kernel is False:
assert output.status != OutputStatus.DONE
if output.status == OutputStatus.HOLD:
output.status = OutputStatus.RUNNING
elif output.status == OutputStatus.RUNNING:
output.status = OutputStatus.DONE
else:
raise ValueError(
"bad value for output.status: %r" % output.status
)
return True
elif message_type == "status":
execution_state = content["execution_state"]
Expand All @@ -129,16 +162,20 @@ def _tick_one(
return False
elif message_type == "execute_result":
self._append_chunk(output, content["data"], content["metadata"])
if 'text/plain' in content['data']:
copy_on_demand(content["data"]['text/plain'])
return True
elif message_type == "error":
output.chunks.append(
ErrorOutputChunk(
content["ename"], content["evalue"], content["traceback"]
)
)
copy_on_demand(lambda: "\n\n".join(map(clean_up_text, content["traceback"])))
output.success = False
return True
elif message_type == "stream":
copy_on_demand(content["text"])
output.chunks.append(TextOutputChunk(content["text"]))
return True
elif message_type == "display_data":
Expand Down

0 comments on commit 6d3bfab

Please sign in to comment.