Skip to content

Commit

Permalink
Enable cell inputs & outputs to be collapsed
Browse files Browse the repository at this point in the history
  • Loading branch information
joouha committed Oct 15, 2022
1 parent 71659e8 commit a52d4b8
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Upcoming
Added
=====

- Enable cell inputs and outputs to be collapsed
- Make changing cursor shapes to showing editing mode configurable

Fixed
Expand Down
20 changes: 12 additions & 8 deletions euporie/core/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,11 @@ def on_shell_execute_reply(self, rsp: "dict[str, Any]") -> "None":
msg_id = rsp.get("parent_header", {}).get("msg_id")
content = rsp.get("content", {})

if callable(set_metadata := self.msg_id_callbacks[msg_id]["set_metadata"]):
if self.kernel_tab.app.config.record_cell_timing and callable(
set_metadata := self.msg_id_callbacks[msg_id]["set_metadata"]
):
set_metadata(
("execute", "shell", "execute_reply"),
("execution", "shell.execute_reply"),
rsp["header"]["date"].isoformat(),
)

Expand Down Expand Up @@ -486,29 +488,31 @@ def on_iopub_status(self, rsp: "dict[str, Any]") -> "None":
set_status(status)

if status == "idle":
if callable(
if self.kernel_tab.app.config.record_cell_timing and callable(
set_metadata := self.msg_id_callbacks[msg_id].get("set_metadata")
):
set_metadata(
("iopub", "status", "idle"),
("execution", "iopub.status.idle"),
rsp["header"]["date"].isoformat(),
)

elif status == "busy":
if callable(
if self.kernel_tab.app.config.record_cell_timing and callable(
set_metadata := self.msg_id_callbacks[msg_id].get("set_metadata")
):
set_metadata(
("iopub", "status", "busy"),
("execution", "iopub.status.busy"),
rsp["header"]["date"].isoformat(),
)

def on_iopub_execute_input(self, rsp: "dict[str, Any]") -> "None":
"""Call callbacks for an iopub execute input response."""
msg_id = rsp.get("parent_header", {}).get("msg_id")
if callable(set_metadata := self.msg_id_callbacks[msg_id]["set_metadata"]):
if self.kernel_tab.app.config.record_cell_timing and callable(
set_metadata := self.msg_id_callbacks[msg_id]["set_metadata"]
):
set_metadata(
("iopub", "execute_input"),
("execution", "iopub", "execute_input"),
rsp["header"]["date"].isoformat(),
)

Expand Down
2 changes: 2 additions & 0 deletions euporie/core/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ def build_style(
"cell.output": "fg:default bg:default",
"cell.input.prompt": "fg:blue",
"cell.output.prompt": "fg:red",
"cell show outputs": "bg:#888",
"cell show inputs": "bg:#888",
# Scrollbars
"scrollbar": f"fg:{cp.bg.more(15/20)} bg:{cp.bg.more(3/20)}",
"scrollbar.background": f"fg:{cp.bg.more(15/20)} bg:{cp.bg.more(3/20)}",
Expand Down
21 changes: 20 additions & 1 deletion euporie/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
from itertools import chain
from typing import TYPE_CHECKING, Sequence, TypeVar, overload

from prompt_toolkit.mouse_events import MouseButton, MouseEventType
from upath import UPath

if TYPE_CHECKING:
from os import PathLike
from typing import Iterable, Optional, Union
from typing import Callable, Iterable, Optional, Union

from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
from prompt_toolkit.layout.mouse_handlers import MouseHandler
from prompt_toolkit.mouse_events import MouseEvent

T = TypeVar("T")

Expand Down Expand Up @@ -57,3 +62,17 @@ def parse_path(path: "Optional[Union[str, PathLike]]") -> "Optional[UPath]":
except AttributeError:
pass
return upath


def on_click(func: "Callable") -> "MouseHandler":
"""Return a mouse handler which call a given function on click."""

def _mouse_handler(mouse_event: "MouseEvent") -> "NotImplementedOrNone":
if (
mouse_event.button == MouseButton.LEFT
and mouse_event.event_type == MouseEventType.MOUSE_UP
):
return func()
return NotImplemented

return _mouse_handler
124 changes: 112 additions & 12 deletions euporie/core/widgets/cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
import weakref
from functools import partial
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast

import nbformat
from prompt_toolkit.filters import Condition
Expand All @@ -27,13 +27,15 @@
from euporie.core.config import add_setting
from euporie.core.filters import multiple_cells_selected
from euporie.core.format import format_code
from euporie.core.utils import on_click
from euporie.core.widgets.cell_outputs import CellOutputArea
from euporie.core.widgets.inputs import KernelInput, StdInput

if TYPE_CHECKING:
from typing import Any, Callable, Literal, Optional

from prompt_toolkit.buffer import Buffer
from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
from prompt_toolkit.layout.containers import AnyContainer
from prompt_toolkit.layout.dimension import Dimension
Expand Down Expand Up @@ -355,14 +357,25 @@ def _send_input(buf: "Buffer") -> "bool":
],
height=1,
)

source_hidden = Condition(
lambda: self.json["metadata"].get("jupyter", {}).get("source_hidden", False)
)

input_row = ConditionalContainer(
VSplit(
[
fill(width=1, char=border_char("MID_LEFT")),
ConditionalContainer(
content=Window(
FormattedTextControl(
lambda: weak_self.prompt,
lambda: [
(
"",
weak_self.prompt,
on_click(self.toggle_input),
)
],
),
width=lambda: len(weak_self.prompt),
style="class:cell.input.prompt",
Expand All @@ -373,7 +386,24 @@ def _send_input(buf: "Buffer") -> "bool":
fill(width=1, char=border_char("MID_SPLIT")),
filter=show_prompt,
),
self.input_box,
ConditionalContainer(self.input_box, filter=~source_hidden),
ConditionalContainer(
Window(
FormattedTextControl(
[
cast(
"OneStyleAndTextTuple",
(
"class:cell,show,inputs",
" … ",
on_click(self.show_input),
),
)
]
)
),
filter=source_hidden,
),
fill(width=1, char=border_char("MID_RIGHT")),
],
),
Expand Down Expand Up @@ -403,14 +433,27 @@ def _send_input(buf: "Buffer") -> "bool":
),
filter=(show_input & show_output) | self.stdin_box.visible,
)

outputs_hidden = Condition(
lambda: self.json["metadata"]
.get("jupyter", {})
.get("outputs_hidden", False)
)

output_row = ConditionalContainer(
VSplit(
[
fill(width=1, char=border_char("MID_LEFT")),
ConditionalContainer(
content=Window(
FormattedTextControl(
lambda: weak_self.prompt,
lambda: [
(
"",
weak_self.prompt,
on_click(self.toggle_output),
)
],
),
width=lambda: len(weak_self.prompt),
style="class:cell.output.prompt",
Expand All @@ -427,7 +470,27 @@ def _send_input(buf: "Buffer") -> "bool":
),
HSplit(
[
self.output_area,
ConditionalContainer(
self.output_area,
filter=~outputs_hidden,
),
ConditionalContainer(
Window(
FormattedTextControl(
[
cast(
"OneStyleAndTextTuple",
(
"class:cell,show,outputs",
" … ",
on_click(self.show_output),
),
),
]
)
),
filter=outputs_hidden,
),
self.stdin_box,
]
),
Expand Down Expand Up @@ -488,6 +551,7 @@ def focus(self, position: "Optional[int]" = None, scroll: "bool" = False) -> "No
if self.stdin_box.visible():
to_focus = self.stdin_box.window
else:
self.show_input()
to_focus = self.input_box.window
self.rendered = False
if position is not None:
Expand Down Expand Up @@ -620,6 +684,8 @@ def remove_outputs(self) -> "None":
if "outputs" in self.json:
self.json["outputs"].clear()
self.output_area.reset()
# Ensure the cell output area is visible
self.show_output()

def set_cell_type(
self, cell_type: "Literal['markdown','code','raw']", clear: "bool" = False
Expand Down Expand Up @@ -724,6 +790,41 @@ def clear_output(self, wait: "bool" = False) -> "None":
else:
self.remove_outputs()

def show_input(self) -> "None":
"""Set the cell inputs to visible."""
self.set_metadata(("jupyter", "source_hidden"), False)

def hide_input(self) -> "None":
"""Set the cell inputs to visible."""
# Exit edit mode
self.kernel_tab.edit_mode = False
# Un-focus the cell input
self.focus()
# Set the input to hidden
self.set_metadata(("jupyter", "source_hidden"), True)

def toggle_input(self) -> "None":
"""Toggle the visibility of the cell input."""
if self.json["metadata"].get("jupyter", {}).get("source_hidden", False):
self.show_input()
else:
self.hide_input()

def show_output(self) -> "None":
"""Set the cell outputs to visible."""
self.set_metadata(("jupyter", "outputs_hidden"), False)

def hide_output(self) -> "None":
"""Set the cell outputs to visible."""
self.set_metadata(("jupyter", "outputs_hidden"), True)

def toggle_output(self) -> "None":
"""Toggle the visibility of the cell outputs."""
if self.json["metadata"].get("jupyter", {}).get("outputs_hidden", False):
self.show_output()
else:
self.hide_output()

def set_metadata(self, path: "tuple[str, ...]", data: "Any") -> "None":
"""Sets a value in the metadata at an arbitrary path.
Expand All @@ -732,13 +833,12 @@ def set_metadata(self, path: "tuple[str, ...]", data: "Any") -> "None":
data: The value to add
"""
if self.kernel_tab.app.config.record_cell_timing:
level = self.json["metadata"]
for i, key in enumerate(path):
if i == len(path) - 1:
level[key] = data
else:
level = level.setdefault(key, {})
level = self.json["metadata"]
for i, key in enumerate(path):
if i == len(path) - 1:
level[key] = data
else:
level = level.setdefault(key, {})

def set_status(self, status: "str") -> "None":
"""Set the execution status of the cell."""
Expand Down
70 changes: 70 additions & 0 deletions euporie/notebook/tabs/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,76 @@ def _cells_to_raw() -> "None":
for cell in nb.cells:
cell.set_cell_type("raw", clear=True)

@staticmethod
@add_cmd(
filter=cell_has_focus & ~buffer_has_focus,
title="Expand cell inputs",
)
def _show_cell_inputs() -> "None":
"""Expand the selected cells' inputs."""
nb = get_app().tab
if isinstance(nb, Notebook):
for cell in nb.cells:
cell.show_input()

@staticmethod
@add_cmd(
filter=cell_has_focus & ~buffer_has_focus,
title="Collapse cell inputs",
)
def _hide_cell_inputs() -> "None":
"""Collapse the selected cells' inputs."""
nb = get_app().tab
if isinstance(nb, Notebook):
for cell in nb.cells:
cell.hide_input()

@staticmethod
@add_cmd(
filter=cell_has_focus & ~buffer_has_focus,
)
def _toggle_cell_inputs() -> "None":
"""Toggle the visibility of the selected cells' inputs."""
nb = get_app().tab
if isinstance(nb, Notebook):
for cell in nb.cells:
cell.toggle_input()

@staticmethod
@add_cmd(
filter=cell_has_focus & ~buffer_has_focus,
title="Expand cell outputs",
)
def _show_cell_outputs() -> "None":
"""Expand the selected cells' outputs."""
nb = get_app().tab
if isinstance(nb, Notebook):
for cell in nb.cells:
cell.show_output()

@staticmethod
@add_cmd(
filter=cell_has_focus & ~buffer_has_focus,
title="Collapse cell outputs",
)
def _hide_cell_outputs() -> "None":
"""Collapse the selected cells' outputs."""
nb = get_app().tab
if isinstance(nb, Notebook):
for cell in nb.cells:
cell.hide_output()

@staticmethod
@add_cmd(
filter=cell_has_focus & ~buffer_has_focus,
)
def _toggle_cell_outputs() -> "None":
"""Toggle the visibility of the selected cells' outputs."""
nb = get_app().tab
if isinstance(nb, Notebook):
for cell in nb.cells:
cell.toggle_output()

@staticmethod
@add_cmd(
title="Reformat cells",
Expand Down

0 comments on commit a52d4b8

Please sign in to comment.