Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions example/language-features/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Editor language features: completion + hover backed by a Python callback.

@jourdain jourdain Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file could looks like

"""Editor language features: completion + hover backed by a Python callback.

The ``completion`` and ``hover`` props on ``code.Editor`` each name a trame
``@trigger`` that receives ``(code, line, column)`` and returns results. Here
both are backed by jedi, giving live Python completion and docstring-on-hover
entirely in-process, with no client-side JavaScript.

The contract:

* completion trigger returns a list of items, each a dict with keys
  ``label`` (required), ``kind``, ``detail``, ``documentation``, ``insertText``.
* hover trigger returns a markdown string, a list of markdown strings, or
  ``{"contents": [...]}`` (or ``None`` for no hover).
* positions are passed as ``line`` (1-based) and ``column`` (0-based), matching
  jedi's API directly.

Run with::

    pip install trame trame-vuetify trame-code jedi
    python app.py
"""

import jedi
from trame.app import TrameApp
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import code
from trame.widgets import vuetify3 as v3


INITIAL_CODE = '''import math


def circle_area(radius):
    """Return the area of a circle with the given radius."""
    return math.pi * radius**2


# Type "math." below, or hover a name, to see completion and docstrings.
math.
'''

class PyCodeEditor(TrameApp):
    def __init__(self, server=None):
        super().__init__(server)
        self._build_ui()

    def _build_ui(self):
        self.state.trame__title = "PyEditor"
        with SinglePageLayout(server) as self.ui:
            self.ui.title.set_text("Editor language features (jedi)")
            with self.ui.content:
                with v3.VContainer(fluid=True, classes="fill-height pa-0"):
                    code.Editor(
                        value=INITIAL_CODE,
                        language="python",
                        theme="vs",
                        style="width: 100%; height: 100%;",

                        completion=self.on_completion,
                        hover=self.on_hover,

                    )

    def on_completion(self, code_text, line, column):
        """Completion items for (code, line, column). line 1-based, column 0-based."""
        try:
            completions = jedi.Script(code=code_text).complete(line, column)
        except Exception:
            return []
        return [
            {
                "label": c.name,
                "kind": c.type,
                "detail": (c.description or "")[:80],
            }
            for c in completions[:200]
        ]

    def on_hover(self, code_text, line, column):
        """Hover markdown (signature + docstring) for the symbol at the cursor."""
        try:
            definitions = jedi.Script(code=code_text).help(line, column)
        except Exception:
            return None
        if not definitions:
            return None
        definition = definitions[0]
        contents = []
        signatures = [s.to_string() for s in definition.get_signatures()]
        if signatures:
            contents.append("```python\n" + "\n".join(signatures) + "\n```")
        doc = definition.docstring(raw=True) or ""
        if doc:
            contents.append(doc)
        return {"contents": contents} if contents else None






if __name__ == "__main__":
    app = PyCodeEditor()
    app.server.start()


The ``completion`` and ``hover`` props on ``code.Editor`` each take a callable
that receives ``(code, line, column)`` and returns results. Here both are backed
by jedi, giving live Python completion and docstring-on-hover entirely
in-process, with no client-side JavaScript.

The contract:

* completion returns a list of items, each a dict with keys ``label`` (required),
``kind``, ``detail``, ``documentation``, ``insertText``.
* hover returns a markdown string, a list of markdown strings, or
``{"contents": [...]}`` (or ``None`` for no hover).
* positions are passed as ``line`` (1-based) and ``column`` (0-based), matching
jedi's API directly.

Run with::

pip install trame trame-vuetify trame-code jedi
python app.py
"""

import jedi
from trame.app import TrameApp
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import code
from trame.widgets import vuetify3 as v3

INITIAL_CODE = '''import math


def circle_area(radius):
"""Return the area of a circle with the given radius."""
return math.pi * radius**2


# Type "math." below, or hover a name, to see completion and docstrings.
math.
'''


class PyCodeEditor(TrameApp):
def __init__(self, server=None):
super().__init__(server)
self._build_ui()

def _build_ui(self):
self.state.trame__title = "PyEditor"
with SinglePageLayout(self.server) as self.ui:
self.ui.title.set_text("Editor language features (jedi)")
with self.ui.content:
with v3.VContainer(fluid=True, classes="fill-height pa-0"):
code.Editor(
v_model=("editor_code", INITIAL_CODE),
language="python",
theme="vs",
style="width: 100%; height: 100%;",
completion=self.on_completion,
hover=self.on_hover,
)

def on_completion(self, code_text, line, column):
"""Completion items for (code, line, column): line 1-based, column 0-based."""
try:
completions = jedi.Script(code=code_text).complete(line, column)
except Exception:
return []
return [
{
"label": c.name,
"kind": c.type,
"detail": (c.description or "")[:80],
}
for c in completions[:200]
]

def on_hover(self, code_text, line, column):
"""Hover markdown (signature + docstring) for the symbol at the cursor."""
try:
definitions = jedi.Script(code=code_text).help(line, column)
except Exception:
return None
if not definitions:
return None
definition = definitions[0]
contents = []
signatures = [s.to_string() for s in definition.get_signatures()]
if signatures:
contents.append("```python\n" + "\n".join(signatures) + "\n```")
doc = definition.docstring(raw=True) or ""
if doc:
contents.append(doc)
return {"contents": contents} if contents else None


if __name__ == "__main__":
app = PyCodeEditor()
app.server.start()
4 changes: 4 additions & 0 deletions example/language-features/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
trame
trame-vuetify
trame-code
jedi
128 changes: 128 additions & 0 deletions example/live-state/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Editor language features: completion from live in-process state.

This example shows the capability that distinguishes a callback-backed provider
from a language server: the suggestions come from a live Python object in the
running process, not from source text or type stubs. A language server cannot
offer these names, because they exist only at runtime.

Here the editor completes the keys of an in-memory ``DATASET`` when the cursor
is inside a ``dataset["..."]`` subscript, annotating each with the live value's
type and length. Swap ``DATASET`` for a loaded data file, a database schema, or
any live object and the same handler surfaces those names into the editor.

The contract (shared with the language-features example):

* completion returns a list of items, each a dict with key ``label`` (required)
and optional ``kind``, ``detail``, ``documentation``, ``insertText``.
* positions are passed as ``line`` (1-based) and ``column`` (0-based).

Run with::

pip install trame trame-vuetify trame-code
python app.py
"""

import re

from trame.app import TrameApp
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import code
from trame.widgets import vuetify3 as vuetify

# Stand-in for state that exists only at runtime (a loaded dataset, a live
# object graph, a fetched schema...). None of these names appear in the source.
DATASET = {
"pressure": [0.0] * 1000,
"density": [0.0] * 1000,
"temperature": [0.0] * 1000,
"velocity": [(0.0, 0.0, 0.0)] * 1000,
"time_steps": list(range(50)),
}

# Match an unclosed string subscript at the cursor: dataset["pre
_SUBSCRIPT_RE = re.compile(r"""\[\s*["']([^"']*)$""")

INITIAL_CODE = """# Live-state completion demo
#
# 1. Put the cursor between the empty quotes on the last line: dataset[""]
# 2. Type a letter (p, d, t, or v). Suggestions appear automatically.
# 3. The keys are read live from the in-memory DATASET object
# (pressure, density, temperature, velocity, time_steps),
# each shown with its Python type and length.
#
# A language server cannot offer these: they exist only at runtime.

field = dataset[""]
"""


def _describe(value):
"""A short, live description of a value: type plus length when available."""
type_name = type(value).__name__
try:
return f"{type_name}, len {len(value)}"
except TypeError:
return type_name


class LiveStateEditor(TrameApp):
def __init__(self, server=None):
super().__init__(server)
self._build_ui()

def _build_ui(self):
self.state.trame__title = "Live-state completion"
with SinglePageLayout(self.server) as self.ui:
self.ui.title.set_text("Live-state completion")
with self.ui.content:
with vuetify.VContainer(fluid=True, classes="fill-height pa-0"):
code.Editor(
v_model=("live_code", INITIAL_CODE),
language="python",
theme="vs",
completion=self.live_complete,
# open the list as soon as the key string is entered
completion_trigger_characters=(
"completion_triggers",
['"', "'", "["],
),
# dict-key completion happens inside a string literal,
# where Monaco suppresses suggestions by default; enable
# them there, and drop document-word noise.
options=(
"live_editor_options",
{
"quickSuggestions": {
"other": True,
"comments": False,
"strings": True,
},
"wordBasedSuggestions": False,
"minimap": {"enabled": False},
},
),
style="width: 100%; height: 100%;",
)

def live_complete(self, code_text, line, column):
"""Complete DATASET keys when the cursor is inside a string subscript."""
lines = code_text.split("\n")
if line < 1 or line > len(lines):
return []
prefix = lines[line - 1][:column]

match = _SUBSCRIPT_RE.search(prefix)
if not match:
return [] # not inside a key subscript -> defer to others

stub = match.group(1)
return [
{"label": key, "kind": "field", "detail": _describe(value)}
for key, value in DATASET.items()
if key.startswith(stub)
]


if __name__ == "__main__":
app = LiveStateEditor()
app.server.start()
3 changes: 3 additions & 0 deletions example/live-state/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
trame
trame-vuetify
trame-code
1 change: 1 addition & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pytest
trame
29 changes: 29 additions & 0 deletions tests/test_language_features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
def test_completion_and_hover_register_as_triggers():
"""A callable passed to completion/hover is registered as a trigger and
surfaces on the element as a trigger-name attribute."""
from trame.app import get_server
from trame.ui.html import DivLayout
from trame.widgets import code

def on_complete(code_text, line, column):
return []

def on_hover(code_text, line, column):
return None

server = get_server("test_language_features")
with DivLayout(server):
editor = code.Editor(
language="python",
completion=on_complete,
hover=on_hover,
)

html = editor.html
assert 'completion="' in html
assert 'hover="' in html

# the internal triggers resolve back to the original callables
ctrl = server.controller
assert ctrl.trigger_fn(server.trigger_name(on_complete)) is on_complete
assert ctrl.trigger_fn(server.trigger_name(on_hover)) is on_hover
26 changes: 25 additions & 1 deletion trame_code/widgets/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,24 @@ class Editor(HtmlElement):
:param theme:
:param language:
:param textmate:
:param completion: a callable ``fn(code, line, column)`` returning a list of
completion items, each a dict with keys ``label`` (required), ``kind``,
``detail``, ``documentation``, ``insertText``. ``line`` is 1-based,
``column`` 0-based. Registered as a trigger internally so the client can
invoke it and receive the returned items (Monaco needs the result back).
:param hover: a callable ``fn(code, line, column)`` returning hover content:
a markdown string, a list of markdown strings, ``{contents: [...]}``, or
``None``. Registered as a trigger internally like ``completion``.
:param completion_trigger_characters: list of characters that open the
completion list (defaults to ``["."]`` in the component).

Events:

:param input:

"""

def __init__(self, **kwargs):
def __init__(self, completion=None, hover=None, **kwargs):
super().__init__(
"vs-editor",
**kwargs,
Expand All @@ -48,7 +58,21 @@ def __init__(self, **kwargs):
"theme",
"language",
"textmate",
("completion_trigger_characters", "completionTriggerCharacters"),
]
self._event_names += [
"input",
]

# `completion` / `hover` take a callable receiving (code, line, column).
# Monaco pulls results from the provider and needs them returned, so the
# callback is registered as a trigger internally and the client invokes
# it by name and awaits the returned value.
if completion is not None:
self._attributes[
"completion_trigger"
] = f'completion="{self.ctrl.trigger_name(completion)}"'
if hover is not None:
self._attributes[
"hover_trigger"
] = f'hover="{self.ctrl.trigger_name(hover)}"'
Loading
Loading