From 5770539a9e1e3c451c86cd7fc9043fcbec84cd26 Mon Sep 17 00:00:00 2001 From: xavArtley Date: Wed, 3 Apr 2019 16:32:34 +0200 Subject: [PATCH] Ace code editor (#359) --- examples/reference/panes/Ace.ipynb | 175 +++++++++++++++++++++++++++++ panel/config.py | 3 +- panel/models/ace.py | 41 +++++++ panel/models/ace.ts | 129 +++++++++++++++++++++ panel/pane/__init__.py | 1 + panel/pane/ace.py | 62 ++++++++++ panel/tests/pane/test_ace.py | 35 ++++++ 7 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 examples/reference/panes/Ace.ipynb create mode 100644 panel/models/ace.py create mode 100644 panel/models/ace.ts create mode 100644 panel/pane/ace.py create mode 100644 panel/tests/pane/test_ace.py diff --git a/examples/reference/panes/Ace.ipynb b/examples/reference/panes/Ace.ipynb new file mode 100644 index 0000000000..d82732f940 --- /dev/null +++ b/examples/reference/panes/Ace.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "pn.extension('ace')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``Ace`` pane allows to embed a code editor based on [Ace](https://ace.c9.io/).\n", + "Only a small subset of functionnalities are enabled :\n", + " - **syntax highligting** for several languages\n", + " - **themes**\n", + " - basic **completion** fonctionnalities `ctrl+space` (only static analysis of the code)\n", + " - **annotations**\n", + " - **readonly**\n", + "\n", + "#### Parameters:\n", + "\n", + "For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n", + "\n", + "* **``code``** (str): A string with code to set in the editor\n", + "* **``language``** (str): A string wich define the code syntax highlighter (default: 'python')\n", + "* **``theme``** (str): theme of the editor (defaut: 'chrome')\n", + "* **``annotations``** (list): list of annotations. An annotation is a dict with the following keys:\n", + " - `'row'`: row in the editor of the annotation\n", + " - `'column'`: column of the annotation\n", + " - `'text'`: 'text displayed when hover the annotation'\n", + " - `'type'`: define the type of annotation and the icon displayed {`warning` | `error`}\n", + "* **``readonly``** (boolean): A boolean to set the editor in read only mode\n", + "___" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To construct an `Ace` panel we must define it explicitly using `pn.pane.Ace`, we can add some text as initial code.\n", + "Code inserted in the editor is automatically reflected in the `code` parameter and can be linked to an other `panel`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "py_code = \"\"\"import sys\n", + "\"\"\"\n", + "editor = pn.pane.Ace(py_code, sizing_mode='stretch_both', height=300)\n", + "html_pane = pn.pane.HTML(sizing_mode='stretch_both', height=300)\n", + "editor.link(html_pane,code=\"object\")\n", + "pn.Row(editor, html_pane)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "we can add some code in it" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "editor.code += \"\"\"import Math\n", + "\n", + "x = Math.cos(x)**2 + Math.cos(y)**2\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can change language and theme of the editor" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "html_code = r\"\"\"\n", + "\n", + " \n", + " \n", + " `substitute(Filename('', 'Page Title'), '^.', '\\u&', '')`\n", + " \n", + " \n", + "

Title1

\n", + "

Title2

\n", + "

Paragraph

\n", + " \n", + "\n", + "\"\"\"\n", + "editor.language = \"html\"\n", + "editor.theme = \"monokai\"\n", + "editor.code = html_code\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can add some annotations to the editor" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "editor.annotations= [dict(row=1, column=0, text='an error', type='error'),\n", + " dict(row=2, column=0, text='a warning', type='warning')]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we want just to display editor content but not edit it we can set the `readonly` property to `True`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "editor.readonly = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/panel/config.py b/panel/config.py index 88e8758b6f..c70ce6d05f 100644 --- a/panel/config.py +++ b/panel/config.py @@ -152,7 +152,8 @@ class panel_extension(_pyviz_extension): 'mathjax': 'panel.models.mathjax', 'plotly': 'panel.models.plotly', 'vega': 'panel.models.vega', - 'vtk': 'panel.models.vtk'} + 'vtk': 'panel.models.vtk', + 'ace': 'panel.models.ace'} def __call__(self, *args, **params): # Abort if IPython not found diff --git a/panel/models/ace.py b/panel/models/ace.py new file mode 100644 index 0000000000..6b59064506 --- /dev/null +++ b/panel/models/ace.py @@ -0,0 +1,41 @@ +""" +Defines custom AcePlot bokeh model to render Ace editor. +""" +import os + +from bokeh.core.properties import String, Override, Dict, Any, List, Bool +from bokeh.models import HTMLBox + +from ..compiler import CUSTOM_MODELS + +class AcePlot(HTMLBox): + """ + A Bokeh model that wraps around a Ace editor and renders it inside + a Bokeh plot. + """ + + __javascript__ = ['https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.3/ace.js', + 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.3/ext-language_tools.js'] + + __js_require__ = {'paths': {'ace': 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.3/ace'}, + 'shim': {'ace': 'ace'}} + + __implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'ace.ts') + + code = String() + + theme = String(default='chrome') + + language = String(default='python') + + annotations = List(Dict(String, Any), default=[]) + + readonly = Bool(default=False) + + height = Override(default=300) + + width = Override(default=300) + + + +CUSTOM_MODELS['panel.models.ace.AcePlot'] = AcePlot diff --git a/panel/models/ace.ts b/panel/models/ace.ts new file mode 100644 index 0000000000..87890c8033 --- /dev/null +++ b/panel/models/ace.ts @@ -0,0 +1,129 @@ +import * as p from "core/properties" +import {HTMLBox, HTMLBoxView} from "models/layouts/html_box" +import { div } from 'core/dom'; + +function ID() { + // Math.random should be unique because of its seeding algorithm. + // Convert it to base 36 (numbers + letters), and grab the first 9 characters + // after the decimal. + return '_' + Math.random().toString(36).substr(2, 9); +} + + +export class AcePlotView extends HTMLBoxView { + model: AcePlot + protected _ace: any + protected _editor: any + protected _langTools: any + protected _container: HTMLDivElement + + initialize(): void { + super.initialize() + this._ace = (window as any).ace + this._container = div({ + id: ID(), + style: { + width: "100%", + height: "100%" + } + }) + } + + connect_signals(): void { + super.connect_signals() + this.connect(this.model.properties.code.change, () => this._update_code_from_model()) + this.connect(this.model.properties.theme.change, () => this._update_theme()) + this.connect(this.model.properties.language.change, () => this._update_language()) + this.connect(this.model.properties.annotations.change, () => this._add_annotations()) + this.connect(this.model.properties.readonly.change, () => { + this._editor.setReadOnly(this.model.readonly) + }) + } + + render(): void { + super.render() + if (!(this._container === this.el.childNodes[0])) + this.el.appendChild(this._container) + this._container.textContent = this.model.code + this._editor = this._ace.edit(this._container.id) + this._editor.setTheme("ace/theme/" + `${this.model.theme}`) + this._editor.session.setMode("ace/mode/" + `${this.model.language}`) + this._editor.setReadOnly(this.model.readonly) + this._langTools = this._ace.require('ace/ext/language_tools') + this._editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true, + fontFamily: "monospace", //hack for cursor position + }); + this._editor.on('change', () => this._update_code_from_editor()) + } + + _update_code_from_model(): void { + if (this._editor && this._editor.getValue() != this.model.code) + this._editor.setValue(this.model.code) + } + + _update_code_from_editor(): void { + if(this._editor.getValue() != this.model.code){ + this.model.code = this._editor.getValue() + } + } + + _update_theme(): void{ + this._editor.setTheme("ace/theme/" + `${this.model.theme}`) + } + + _update_language(): void{ + this._editor.session.setMode("ace/mode/" + `${this.model.language}`) + } + + _add_annotations(): void{ + this._editor.session.setAnnotations(this.model.annotations) + } + + after_layout(): void{ + super.after_layout() + this._editor.resize() + } + +} + +export namespace AcePlot { + export type Attrs = p.AttrsOf + export type Props = HTMLBox.Props & { + code: p.Property + language: p.Property + theme: p.Property + annotations: p.Property + readonly: p.Property + } +} + +export interface AcePlot extends AcePlot.Attrs {} + +export class AcePlot extends HTMLBox { + properties: AcePlot.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static initClass(): void { + this.prototype.type = "AcePlot" + this.prototype.default_view = AcePlotView + + this.define({ + code: [ p.String ], + language: [ p.String, 'python' ], + theme: [ p.String, 'chrome' ], + annotations: [ p.Array, [] ], + readonly: [ p.Boolean, false ] + }) + + this.override({ + height: 300, + width: 300 + }) + } +} +AcePlot.initClass() diff --git a/panel/pane/__init__.py b/panel/pane/__init__.py index cfae9d3d68..e9a793bb2d 100644 --- a/panel/pane/__init__.py +++ b/panel/pane/__init__.py @@ -17,6 +17,7 @@ from .plot import Bokeh, Matplotlib, RGGPlot, YT # noqa from .vega import Vega # noqa from .vtk import VTK # noqa +from .ace import Ace # noqa def panel(obj, **kwargs): diff --git a/panel/pane/ace.py b/panel/pane/ace.py new file mode 100644 index 0000000000..c70d55411c --- /dev/null +++ b/panel/pane/ace.py @@ -0,0 +1,62 @@ +import sys + +import param + +from six import string_types +from pyviz_comms import JupyterComm + +from .base import PaneBase + + +class Ace(PaneBase): + """ + Ace panes allow rendering Ace editor. + """ + + priority = 0 + + code = param.String(doc="State of the current code in the editor") + + theme = param.String(default='chrome', doc="Theme of the editor") + + language = param.String(default='python', doc="Language of the editor") + + annotations = param.List(default=[], doc="List of annotations to add to the editor") + + readonly = param.Boolean(default=False, doc="Define if editor content can be modified") + + _updates = True + + @classmethod + def applies(cls, obj): + if isinstance(obj, string_types): + return None + else: + return False + + def _get_model(self, doc, root=None, parent=None, comm=None): + """ + Should return the bokeh model to be rendered. + """ + if 'panel.models.ace' not in sys.modules: + if isinstance(comm, JupyterComm): + self.param.warning('AcePlot was not imported on instantiation ' + 'and may not render in a notebook. Restart ' + 'the notebook kernel and ensure you load ' + 'it as part of the extension using:' + '\n\npn.extension(\'ace\')\n') + from ..models.ace import AcePlot + else: + AcePlot = getattr(sys.modules['panel.models.ace'], 'AcePlot') + + self.code = self.object if self.object else '' + props = self._process_param_change(self._init_properties()) + model = AcePlot(**props) + if root is None: + root = model + self._link_props(model, ['code', 'language', 'theme', 'annotations', 'readonly'], doc, root, comm) + self._models[root.ref['id']] = (model, parent) + return model + + def _update(self, model): + model.code = self.object if self.object else '' diff --git a/panel/tests/pane/test_ace.py b/panel/tests/pane/test_ace.py new file mode 100644 index 0000000000..a9b6c35403 --- /dev/null +++ b/panel/tests/pane/test_ace.py @@ -0,0 +1,35 @@ +from panel.pane import Ace + +code = """ +import math +x = 10 +math.sin(x)**2 + math.cos(x)**2 == 1 +""" + + +def test_ace_pane(document, comm): + pane = Ace(code) + + # Create pane + model = pane.get_root(document, comm=comm) + assert pane._models[model.ref['id']][0] is model + assert model.code == code + + # Replace Pane.object + pane.object = None + assert pane._models[model.ref['id']][0] is model + assert model.code == '' + + # Replace params + pane.code = "test" + pane.language = 'xml' + pane.theme = 'monokai' + + assert pane._models[model.ref['id']][0] is model + assert model.code == "test" + assert model.language == "xml" + assert model.theme == "monokai" + + # Cleanup + pane._cleanup(model) + assert pane._models == {}