Skip to content

Commit

Permalink
Merge pull request #42 from davidbrochart/editor
Browse files Browse the repository at this point in the history
Add text input
  • Loading branch information
davidbrochart authored Jun 28, 2023
2 parents 5130ef2 + bb19450 commit 4f1b123
Show file tree
Hide file tree
Showing 18 changed files with 495 additions and 79 deletions.
84 changes: 55 additions & 29 deletions plugins/cell/txl_cell/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,60 @@
import pkg_resources
import y_py as Y
from asphalt.core import Component, Context
from rich import box
from rich.console import RenderableType
from rich.markdown import Markdown
from rich.panel import Panel
from rich.syntax import Syntax
from rich.text import Text
from textual.containers import Container
from textual.widgets import Static

from txl.base import Cell, CellFactory, Kernel, Widgets
from txl.text_input import TextInput

YDOCS = {
ep.name: ep.load() for ep in pkg_resources.iter_entry_points(group="ypywidgets")
}


class Source(Static):
def __init__(self, cell, *args, **kwargs):
super().__init__(*args, **kwargs)
self.cell = cell
class Source(TextInput):
def __init__(self, ycell, *args, **kwargs):
super().__init__(ytext=ycell["source"], *args, **kwargs)
self.ycell = ycell
self._selected = False

@property
def selected(self) -> bool:
return self._selected

async def on_click(self):
if self.cell.kernel:
await self.cell.kernel.execute(self.cell.ydoc, self.cell.ycell)
@selected.setter
def selected(self, value: bool) -> None:
self._selected = value
self.update()

def render(self) -> RenderableType:
if self.ycell["cell_type"] == "code":
code = super().render()
border_style = "yellow"
box_style = box.DOUBLE if self.selected else box.ROUNDED
renderable = Panel(
code,
height=len(self.lines) + 2,
border_style=border_style,
box=box_style,
)
elif self.ycell["cell_type"] == "markdown":
renderable = Markdown(str(self.ycell["source"]), code_theme="ansi_dark")
else:
renderable = Text(str(self.ycell["source"]))
return renderable


class CellMeta(type(Cell), type(Container)):
pass


class _Cell(Cell, Container, metaclass=CellMeta):
class _Cell(Cell, Container, metaclass=CellMeta, can_focus=True):
def __init__(
self,
ycell: Y.YMap,
Expand Down Expand Up @@ -83,24 +108,6 @@ def get_execution_count(self, value):
execution_count = str(value).removesuffix(".0")
return f"[green]In [[#66ff00]{execution_count}[/#66ff00]]:[/green]"

def get_source(self, cell):
source = "".join(cell["source"])
theme = "ansi_dark"
if cell["cell_type"] == "code":
renderable = Panel(
Syntax(
source,
self.language,
theme=theme,
),
border_style="yellow",
)
elif cell["cell_type"] == "markdown":
renderable = Markdown(source, code_theme=theme)
else:
renderable = Text(source)
return renderable

def update(self):
cell = json.loads(self.ycell.to_json())
execution_count = (
Expand All @@ -110,8 +117,9 @@ def update(self):
)
self.execution_count = Static(execution_count)
self.mount(self.execution_count)
source = self.get_source(cell)
self.source = Source(self, source)
self.source = Source(
ydoc=self.ydoc, ycell=self.ycell, parent_widget=self, lexer=self.language
)
self.mount(self.source)

for output in cell.get("outputs", []):
Expand Down Expand Up @@ -159,6 +167,24 @@ def get_output_widget(self, output):
output_widget = widget
return output_widget

def select(self) -> None:
self.source.selected = True

def unselect(self) -> None:
self.source.selected = False

@property
def selected(self) -> bool:
return self.source.selected

@property
def clicked(self) -> bool:
return self.source.clicked

@clicked.setter
def clicked(self, value: bool):
self.source.clicked = value


class CellComponent(Component):
async def start(
Expand Down
1 change: 1 addition & 0 deletions plugins/image_viewer/txl_image_viewer/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async def open(self, path: str) -> None:
def update_viewer(self):
if not self.data:
return

f = tempfile.NamedTemporaryFile(delete=False)
try:
f.write(self.data)
Expand Down
10 changes: 8 additions & 2 deletions plugins/local_kernels/txl_local_kernels/components.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import y_py as Y
from asphalt.core import Component, Context
from asphalt.core import Component, Context, context_teardown

from txl.base import Kernels

from .driver import KernelDriver
from .driver import KernelDriver, kernel_drivers


class LocalKernels(Kernels):
Expand All @@ -18,8 +18,14 @@ async def execute(self, ydoc: Y.YDoc, ycell: Y.YMap):


class LocalKernelsComponent(Component):
@context_teardown
async def start(
self,
ctx: Context,
) -> None:
ctx.add_resource(LocalKernels, types=Kernels)

yield

for kernel_driver in kernel_drivers:
await kernel_driver.stop()
3 changes: 3 additions & 0 deletions plugins/local_kernels/txl_local_kernels/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from .kernelspec import find_kernelspec
from .message import deserialize, feed_identities, serialize

kernel_drivers = []


class KernelDriver(KernelMixin):
def __init__(
Expand Down Expand Up @@ -42,6 +44,7 @@ def __init__(
self.channel_tasks: List[asyncio.Task] = []
self.comm_handlers = comm_handlers
asyncio.create_task(self.start())
kernel_drivers.append(self)

async def restart(self, startup_timeout: float = float("inf")) -> None:
for task in self.channel_tasks:
Expand Down
66 changes: 57 additions & 9 deletions plugins/notebook_editor/txl_notebook_editor/components.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from functools import partial

from asphalt.core import Component, Context
from textual.containers import Container
from textual.containers import VerticalScroll
from textual.events import Event
from textual.keys import Keys

from txl.base import CellFactory, Contents, Editor, Editors, FileOpenEvent, Kernels


class NotebookEditorMeta(type(Editor), type(Container)):
class NotebookEditorMeta(type(Editor), type(VerticalScroll)):
pass


class NotebookEditor(Editor, Container, metaclass=NotebookEditorMeta):
class NotebookEditor(Editor, VerticalScroll, metaclass=NotebookEditorMeta):
def __init__(
self,
contents: Contents,
Expand All @@ -22,27 +24,73 @@ def __init__(
self.kernels = kernels
self.cell_factory = cell_factory
self.kernel = None
self.cells = []
self.cell_i = 0

async def on_open(self, event: FileOpenEvent) -> None:
await self.open(event.path)

async def open(self, path: str) -> None:
self.ynb = await self.contents.get(path, type="notebook")
self.update()
self.ynb.observe(self.on_change)

def update(self):
ipynb = self.ynb.source
self.language = (
ipynb.get("metadata", {}).get("kernelspec", {}).get("language", "")
)
kernel_name = ipynb.get("metadata", {}).get("kernelspec", {}).get("name")
if kernel_name:
self.kernel = self.kernels(kernel_name)
self.update()

def update(self):
if self.kernel is None:
kernel_name = ipynb.get("metadata", {}).get("kernelspec", {}).get("name")
if kernel_name:
self.kernel = self.kernels(kernel_name)
for i_cell in range(self.ynb.cell_number):
cell = self.cell_factory(
self.ynb._ycells[i_cell], self.ynb.ydoc, self.language, self.kernel
)
self.mount(cell)
self.cells.append(cell)

def on_change(self, target, event):
if target == "cells":
for e in event:
e.path()[0]

async def on_key(self, event: Event) -> None:
if event.key == Keys.Up:
event.stop()
if self.cell_i > 0:
self.current_cell.unselect()
self.cell_i -= 1
self.current_cell.select()
self.scroll_to_widget(self.current_cell)
elif event.key == Keys.Down:
event.stop()
if self.cell_i < len(self.cells) - 1:
self.current_cell.unselect()
self.cell_i += 1
self.current_cell.select()
self.scroll_to_widget(self.current_cell)
elif event.key == Keys.Return or event.key == Keys.Enter:
event.stop()
self.current_cell.source.focus()
elif event.key == Keys.Space:
event.stop()
if self.kernel:
await self.kernel.execute(self.ynb.ydoc, self.ynb._ycells[self.cell_i])

def on_click(self) -> None:
for cell_i, cell in enumerate(self.cells):
if cell.clicked:
cell.clicked = False
cell.select()
self.cell_i = cell_i
elif cell.selected:
cell.unselect()

@property
def current_cell(self):
return self.cells[self.cell_i]


class NotebookEditorComponent(Component):
Expand Down
6 changes: 0 additions & 6 deletions plugins/notebook_viewer/txl_notebook_viewer/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ def update_viewer(self):
else:
continue

with open("debug.txt", "at") as f:
f.write(f"{widget=}\n")
if widget is None:
num_lines = len(text.splitlines())
self.add_row(execution_count, renderable, height=num_lines)
Expand All @@ -173,11 +171,7 @@ def on_change(self, target, event):
async def key_e(self) -> None:
if self.kernel:
ycell = self.ynb._ycells[self._selected_cell_idx]
with open("debug.txt", "at") as f:
f.write(f"executing {ycell=}\n")
await self.kernel.execute(self.ynb.ydoc, ycell)
with open("debug.txt", "at") as f:
f.write("executing done\n")


class NotebookViewerComponent(Component):
Expand Down
22 changes: 13 additions & 9 deletions plugins/remote_contents/txl_remote_contents/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,27 @@ async def get(
doc_format = {"blob": "base64"}.get(type, "text")
doc_type = type # if type == "notebook" else "file"
async with httpx.AsyncClient() as client:
r = await client.put(
f"{self.base_url}/api/yjs/roomid/{path}",
response = await client.put(
f"{self.base_url}/api/collaboration/session/{path}",
json={"format": doc_format, "type": doc_type},
params={**self.query_params},
cookies=self.cookies,
)
self.cookies.update(r.cookies)
roomid = r.text
self.cookies.update(response.cookies)
r = response.json()
room_id = f"{r['format']}:{r['type']}:{r['fileId']}"
session_id = r["sessionId"]
ydoc = Y.YDoc()
jupyter_ydoc = ydocs[type](ydoc)
asyncio.create_task(self._websocket_provider(roomid, ydoc))
asyncio.create_task(self._websocket_provider(room_id, session_id, ydoc))
return jupyter_ydoc

async def _websocket_provider(self, roomid, ydoc):
ws_url = f"{self.ws_url}/api/yjs/{roomid}"
async with aconnect_ws(ws_url, cookies=self.cookies) as websocket:
WebsocketProvider(ydoc, Websocket(websocket, roomid))
async def _websocket_provider(self, room_id, session_id, ydoc):
ws_url = f"{self.ws_url}/api/collaboration/room/{room_id}"
async with aconnect_ws(
ws_url, cookies=self.cookies, params={"sessionId": session_id}
) as websocket:
WebsocketProvider(ydoc, Websocket(websocket, room_id))
await asyncio.Future()


Expand Down
30 changes: 16 additions & 14 deletions plugins/remote_kernels/txl_remote_kernels/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(
self.shell_channel = "shell"
self.control_channel = "control"
self.iopub_channel = "iopub"
self.send_lock = asyncio.Lock()

async def start(self):
i = str(uuid.uuid4())
Expand Down Expand Up @@ -103,18 +104,19 @@ async def send_message(
channel,
change_date_to_str: bool = False,
):
_date_to_str = date_to_str if change_date_to_str else lambda x: x
msg["header"] = _date_to_str(msg["header"])
msg["parent_header"] = _date_to_str(msg["parent_header"])
msg["metadata"] = _date_to_str(msg["metadata"])
msg["content"] = _date_to_str(msg.get("content", {}))
msg["channel"] = channel
if self.websocket.subprotocol == "v1.kernel.websocket.jupyter.org":
bmsg = serialize_msg_to_ws_v1(msg)
await self.websocket.send_bytes(bmsg)
else:
bmsg = to_binary(msg)
if bmsg is None:
await self.websocket.send_json(msg)
else:
async with self.send_lock:
_date_to_str = date_to_str if change_date_to_str else lambda x: x
msg["header"] = _date_to_str(msg["header"])
msg["parent_header"] = _date_to_str(msg["parent_header"])
msg["metadata"] = _date_to_str(msg["metadata"])
msg["content"] = _date_to_str(msg.get("content", {}))
msg["channel"] = channel
if self.websocket.subprotocol == "v1.kernel.websocket.jupyter.org":
bmsg = serialize_msg_to_ws_v1(msg)
await self.websocket.send_bytes(bmsg)
else:
bmsg = to_binary(msg)
if bmsg is None:
await self.websocket.send_json(msg)
else:
await self.websocket.send_bytes(bmsg)
Loading

0 comments on commit 4f1b123

Please sign in to comment.