Skip to content

feat: add completion and hover language-feature providers#5

Merged
jourdain merged 3 commits into
masterfrom
language-feature-providers
Jun 16, 2026
Merged

feat: add completion and hover language-feature providers#5
jourdain merged 3 commits into
masterfrom
language-feature-providers

Conversation

@jlee-kitware

Copy link
Copy Markdown
Collaborator

Add completion and hover props to the Editor widget. Each names a trame @trigger that receives (code, line, column) and returns results; the component registers Monaco completion/hover providers that call the trigger over the existing websocket and map the normalized results onto Monaco. The consumer writes only Python: no client-side JavaScript and no access to the Monaco instance are required.

  • completion items: {label, kind, detail, documentation, insertText}
  • hover: a markdown string, a list of strings, or {contents: [...]}
  • positions: line is 1-based, column is 0-based
  • requests honor Monaco's CancellationToken; providers are disposed on unmount and re-registered when the language changes
  • adds a jedi-backed Python example under example/language-features

this is not an LSP as what is proposed in the language-servers branch. as such we can register other complete handlers like a live-state contextual complete as demonstrated in the example. This capability is complementary to the LSP capability.

@jlee-kitware jlee-kitware force-pushed the language-feature-providers branch from 6fea1ee to 171adb0 Compare June 12, 2026 23:01
Add `completion` and `hover` props to the Editor widget. Each names a
trame @trigger that receives (code, line, column) and returns results;
the component registers Monaco completion/hover providers that call the
trigger over the existing websocket and map the normalized results onto
Monaco. The consumer writes only Python: no client-side JavaScript and
no access to the Monaco instance are required.

- completion items: {label, kind, detail, documentation, insertText}
- hover: a markdown string, a list of strings, or {contents: [...]}
- positions: line is 1-based, column is 0-based
- requests honor Monaco's CancellationToken; providers are disposed on
  unmount and re-registered when the language changes
- adds a jedi-backed Python example under example/language-features
@jlee-kitware jlee-kitware force-pushed the language-feature-providers branch from 171adb0 to a6ece94 Compare June 12, 2026 23:45
@jlee-kitware jlee-kitware requested a review from jourdain June 13, 2026 00:31
@jlee-kitware jlee-kitware self-assigned this Jun 13, 2026
Comment thread vue-components/src/components/Editor.js
Comment thread vue-components/src/components/Editor.js
Comment thread vue-components/src/components/Editor.js
@@ -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()

Comment thread trame_code/widgets/code.py Outdated
:param theme:
:param language:
:param textmate:
:param completion: name of a server ``@trigger`` that returns completion

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.

should be function/method pointer directly. Let's compute their trigger internally.

Comment thread trame_code/widgets/code.py Outdated

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.

def __init__(self, completion=None, hover=None, **kwargs):
    super().__init__(
        "vs-editor",
        **kwargs,
    )
    self._attr_names += [
            "options",
            "value",
            ("model_value", "modelValue"),
            "theme",
            "language",
            "textmate",
            ("completion_trigger_characters", "completionTriggerCharacters"),
    ]
    self._event_names += [
        "input",
    ]

    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)}"'

@jourdain

Copy link
Copy Markdown
Collaborator

Looks great, but lets make it easier on the user.

Address review: pass a function/method to completion/hover instead of a
trigger-name string. The widget registers it via ctrl.trigger_name and hands
the client the generated name, keeping the trigger internal while still
returning results to Monaco's providers. Convert both examples to the callable
form and update the test to the callable contract.
@jlee-kitware

Copy link
Copy Markdown
Collaborator Author

Done in the follow-up commit, switched completion/hover to take the callable directly and register the trigger internally via ctrl.trigger_name, as you laid out. Both examples now pass the function.

@jlee-kitware jlee-kitware requested a review from jourdain June 15, 2026 22:36
Comment thread example/live-state/app.py Outdated

import re

from trame.app import get_server

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.

Can you make use of TrameApp instead of get_server?

@jourdain

Copy link
Copy Markdown
Collaborator

Happy to merge as-is, but ideally I would like the examples to lineup with the latest best practice.

Match the language-features example: wrap the live-state demo in a TrameApp
subclass with the completion callback as a method, instead of a bare
get_server plus module-level layout.
@jlee-kitware

Copy link
Copy Markdown
Collaborator Author

this looks alot better, thanks for the guidance.

@jlee-kitware jlee-kitware requested a review from jourdain June 16, 2026 13:00
@jourdain jourdain merged commit 522f513 into master Jun 16, 2026
6 checks passed
@jourdain jourdain deleted the language-feature-providers branch June 16, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants