diff --git a/examples/code_editor.ipynb b/examples/code_editor.ipynb index e4c89f4..45bd191 100644 --- a/examples/code_editor.ipynb +++ b/examples/code_editor.ipynb @@ -15,13 +15,15 @@ "\n", "Code completion is provided for the Python mime types `text/x-python` and `text/x-ipython`. The default invocation for code completion is `Tab`, the same as is used in Jupyterlab.\n", "\n", - "### Tooltips\n", + "### Tooltips (Inspect)\n", "\n", "Documentation `Tooltips` can be invoked with `Shift Tab`.\n", "\n", "### `namespace_id`\n", "\n", - "An alternate namespaces can be specified that corresponds to a namespace registry maintained by the `App`. The default namespace \"\" also includes the IPython Shell `user_ns." + "An alternate namespaces can be specified that corresponds to a namespace registry maintained by the `App`. The default namespace \"\" also includes the IPython Shell `user_ns.\n", + "\n", + "## Example" ] }, { @@ -31,6 +33,8 @@ "metadata": {}, "outputs": [], "source": [ + "import asyncio\n", + "\n", "import ipylab\n", "from ipylab.code_editor import CodeEditorOptions" ] @@ -45,10 +49,13 @@ "# Default syntax is Python\n", "ce = ipylab.CodeEditor(\n", " mime_type=\"text/x-python\",\n", - " description=\"Code editor\",\n", + " description=\"Code editor\",\n", " tooltip=\"This is a code editor. Code completion is provided for Python\",\n", " value=\"def test():\\n ipylab.app.notification.notify('CodeEditor evaluation')\\n\\n# Place the cursor in the CodeEditor and press `Shift Enter`\\ntest()\",\n", + " layout={\"height\": \"120px\", \"overflow\": \"hidden\"},\n", + " description_allow_html=True,\n", ")\n", + "asyncio.get_event_loop().call_later(0.5, ce.focus)\n", "ce" ] }, @@ -97,7 +104,12 @@ "metadata": {}, "outputs": [], "source": [ - "ce.editor_options = {\"lineNumbers\": False, \"codeFolding\": True}" + "ce.editor_options = {\n", + " \"autoClosingBrackets\": True,\n", + " \"matchBrackets\": True,\n", + " \"highlightTrailingWhitespace\": True,\n", + " \"highlightWhitespace\": True,\n", + "}" ] }, { @@ -106,6 +118,66 @@ "id": "8", "metadata": {}, "outputs": [], + "source": [ + "values = [\"short\", \"long \" * 20, \"multi line\\n\" * 10]\n", + "\n", + "\n", + "async def test():\n", + " import asyncio\n", + " import random\n", + "\n", + " for _ in range(20):\n", + " ce.value = random.choice(values) # noqa: S311\n", + " await asyncio.sleep(random.randint(10, 300) / 1e3) # noqa: S311" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "ce" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "t = ce.to_task(test())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "t.cancel()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "# Place the label above\n", + "ce.layout.flex_flow = \"column\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], "source": [ "# Add the same editor to the shell.\n", "ipylab.app.shell.add(ce)" @@ -113,7 +185,7 @@ }, { "cell_type": "markdown", - "id": "9", + "id": "14", "metadata": {}, "source": [ "### Other mime_types\n", @@ -124,7 +196,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -134,7 +206,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "16", "metadata": {}, "outputs": [], "source": [ diff --git a/ipylab/code_editor.py b/ipylab/code_editor.py index 335b3d4..2ca2ec4 100644 --- a/ipylab/code_editor.py +++ b/ipylab/code_editor.py @@ -5,16 +5,17 @@ import asyncio import inspect +import typing from asyncio import Task from typing import TYPE_CHECKING, Any, NotRequired, TypedDict from IPython.core import completer as IPC # noqa: N812 from IPython.utils.tokenutil import token_at_cursor -from ipywidgets import register, widget_serialization +from ipywidgets import Layout, register, widget_serialization from ipywidgets.widgets.trait_types import InstanceDict from ipywidgets.widgets.widget_description import DescriptionStyle from ipywidgets.widgets.widget_string import _String -from traitlets import Callable, Dict, Instance, Int, Unicode, default, observe +from traitlets import Callable, Container, Dict, Instance, Int, Unicode, default, observe import ipylab from ipylab._compat.typing import override @@ -185,7 +186,7 @@ class CodeEditor(Ipylab, _String): The completer is invoked with `Tab` by default. Use completer_invoke_keys to change. - `evaluate` and `do_complete` can be overloaded as required. + `evaluate` and `load_value` can be overloaded as required. Adjust `completer.disable_matchers` as required. """ @@ -197,6 +198,8 @@ class CodeEditor(Ipylab, _String): editor_options: Instance[CodeEditorOptions] = Dict().tag(sync=True) # type: ignore update_throttle_ms = Int(100, help="The limit at which changes are synchronised").tag(sync=True) _sync = Int(0).tag(sync=True) + + layout = InstanceDict(Layout, kw={"overflow": "hidden"}).tag(sync=True, **widget_serialization) placeholder = None # Presently not available value = Unicode() @@ -211,7 +214,8 @@ class CodeEditor(Ipylab, _String): ) namespace_id = Unicode("") - evaluate = Callable() + evaluate: Container[typing.Callable[[str], typing.Coroutine]] = Callable() # type: ignore + load_value: Container[typing.Callable[[str], None]] = Callable() # type: ignore @default("key_bindings") def _default_key_bindings(self): @@ -227,6 +231,10 @@ def _default_key_bindings(self): def _default_evaluate(self): return self.completer.evaluate + @default("load_value") + def _default_load_value(self): + return lambda value: self.set_trait("value", value) + @observe("value") def _observe_value(self, _): if not self._setting_value and not self._update_task: @@ -235,9 +243,9 @@ def _observe_value(self, _): async def send_value(): try: while True: - self._sync = self._sync + 1 value = self.value - await self.operation("setValue", {"sync": self._sync, "value": value}) + await self.operation("setValue", {"value": value}) + self._sync = self._sync + 1 await asyncio.sleep(self.update_throttle_ms / 1e3) if self.value == value: return @@ -264,7 +272,7 @@ async def _do_operation_for_frontend(self, operation: str, payload: dict, buffer if payload["sync"] == self._sync: self._setting_value = True try: - self.value = payload["value"] + self.load_value(payload["value"]) finally: self._setting_value = False return self.value == payload["value"] diff --git a/src/widgets/code_editor.ts b/src/widgets/code_editor.ts index 46a9024..e5f5f3b 100644 --- a/src/widgets/code_editor.ts +++ b/src/widgets/code_editor.ts @@ -100,8 +100,6 @@ export class CodeEditorModel extends IpylabModel { return this.editorModel.sharedModel.clearUndoHistory(); case 'setValue': this._syncRequired = true; - // Clear first for better reliability with plugins. - this.editorModel.sharedModel.setSource(''); this.editorModel.sharedModel.setSource(payload.value); this._syncRequired = false; return true; @@ -151,13 +149,13 @@ export class CodeEditorView extends StringView { model: this.model.editorModel }); this.editorWidget.id = this.editorWidget.id || UUID.uuid4(); - this.editorWidget.addClass(this.className); this.model.on('change:mimeType', this.updateCompleter, this); this.model.on('change:completer_invoke_keys', this.updateCompleter, this); this.model.on('change:editor_options', this.updateEditorOptions, this); this.updateCompleter(); this.updateEditorOptions(); this.editorWidget.addClass('ipylab-CodeEditor'); + this.editorWidget.addClass(this.className); this.el.appendChild(this.editorWidget.node); // Initialize the command registry with the bindings. diff --git a/style/widget.css b/style/widget.css index 758b5d9..0083216 100644 --- a/style/widget.css +++ b/style/widget.css @@ -25,10 +25,14 @@ .ipylab-MainArea { height: 100%; width: 100%; - display: block; + display: flex; } .ipylab-CodeEditor { + width: 100%; height: 100%; - overflow-y: auto; + overflow: auto; + + /* overflow-x: auto; + overflow-y: auto; */ }