Skip to content

Conversation

@austin3dickey
Copy link
Contributor

@austin3dickey austin3dickey commented Dec 3, 2025

Release Notes

New Features

  • N/A

Bug Fixes

  • N/A

QA Notes

@:console @:help @:notebooks @:positron-notebooks @:outline @:problems @:references @:web @:win

@github-actions
Copy link

github-actions bot commented Dec 3, 2025

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:console @:help @:notebooks @:positron-notebooks @:outline @:problems @:references @:web @:win

readme  valid tags

@austin3dickey austin3dickey requested a review from Copilot December 4, 2025 05:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the Python language server implementation by replacing the jedi-language-server dependency with a custom lightweight LSP server implementation. The new server maintains Positron-specific features (namespace-aware completions, DataFrame/Series column completions, magic commands, help topics, syntax diagnostics) while delegating static analysis features to third-party extensions like Pylance.

Key changes:

  • Removed jedi-language-server, jedi, and parso dependencies and vendored code
  • Created new positron_lsp.py implementing a minimal LSP server using pygls directly
  • Removed complex Jedi patches and integration code
  • Updated dependency versions (pygls 2.0.0, lsprotocol 2025.0.0)

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/patches/*.patch Removed patch files for jedi, parso, and jedi-language-server
python_files/run-jedi-language-server.py Added type ignore comment for import
python_files/positron_requirements/requirements.txt Updated dependencies, removed jedi-language-server, jedi, parso
python_files/positron_requirements/requirements.in Removed jedi-language-server, upgraded pygls to 2.0.0
python_files/posit/positron_language_server.py Updated import from positron_jedilsp to positron_lsp
python_files/posit/positron/tests/test_positron_lsp.py New test file for the refactored LSP server
python_files/posit/positron/tests/test_positron_jedilsp.py Removed old test file
python_files/posit/positron/positron_lsp.py New LSP server implementation
python_files/posit/positron/positron_jedilsp.py Removed old LSP server implementation
python_files/posit/positron/lsp.py Updated import from positron_jedilsp to positron_lsp
python_files/posit/positron/jedi.py Removed Jedi patches and integration code

Comment on lines +781 to +782
elif inspect.ismethod(obj) or inspect.isfunction(obj):
return types.CompletionItemKind.Function
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

Lines 782 and 784 return the same value. The elif branch checking for methods/functions is redundant since the else branch will also return CompletionItemKind.Function. Consider simplifying by removing the elif or add a comment explaining why this distinction exists in the code structure.

Suggested change
elif inspect.ismethod(obj) or inspect.isfunction(obj):
return types.CompletionItemKind.Function

Copilot uses AI. Check for mistakes.
# Fall back to the expression itself
topic = expr

logger.info(f"Help topic found: {topic}")
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

Using f-string formatting in logging calls can cause performance issues since the string is always formatted even if the logging level would skip it. Consider using lazy formatting: logger.info('Help topic found: %s', topic)

Suggested change
logger.info(f"Help topic found: {topic}")
logger.info("Help topic found: %s", topic)

Copilot uses AI. Check for mistakes.
try:
_publish_diagnostics(server, uri)
except Exception:
logger.exception(f"Failed to publish diagnostics for {uri}", exc_info=True)
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

logger.exception() already includes exc_info=True by default, so passing it explicitly is redundant. Consider removing the exc_info=True parameter.

Suggested change
logger.exception(f"Failed to publish diagnostics for {uri}", exc_info=True)
logger.exception(f"Failed to publish diagnostics for {uri}")

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@seeM seeM left a comment

Choose a reason for hiding this comment

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

I took a look at the code and it looks like a great start!



from jedi_language_server.cli import cli # noqa: E402
from jedi_language_server.cli import cli # type: ignore # noqa: E402
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this an upstream file? If so can we ignore in the pyproject.toml instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I could add this to our pyproject.toml!

self._messages_to_handle: list[Any] = []

@lru_cache # noqa: B019
def get_message_type(self, method: str) -> type | None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this pygls' recommended way to add methods? I wonder if there's something neater now that we're not depending on JediLanguageServerProtocol

init_opts = PositronInitializationOptions()

# Store the working directory (using params.root_path since workspace may not be initialized yet)
server._working_directory = init_opts.working_directory or params.root_path # noqa: SLF001
Copy link
Contributor

Choose a reason for hiding this comment

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

I also wonder if there's another pygls recommended way

# Yield to parent implementation which handles workspace setup
return (yield from super().lsp_initialize(params))

def _data_received(self, data: bytes) -> None: # type: ignore[override]
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need this anymore, and may want to try to make our server async

# Cache for magic completions
self._magic_completions: dict[str, tuple] = {}

def start_tcp(self, host: str) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

It's possible we don't need all of this in the latest pygls. A lot of this was because pygls didn't let you use your own loop, which we needed to do

elif trimmed_line.startswith((_LINE_MAGIC_PREFIX, _CELL_MAGIC_PREFIX)):
# Magic command completion only
pass # Will add magics below
else:
Copy link
Contributor

Choose a reason for hiding this comment

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

I haven't checked, are we providing the right completions (if any) inside string literals? Do we provide path completions?


# Try to evaluate the expression
try:
obj = eval(expr, server.shell.user_ns)
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels like a really dangerous thing to do!

Comment on lines +565 to +578
# Get keys based on the type of object
if isinstance(obj, dict):
keys = [str(k) for k in obj if isinstance(k, str)]
elif _is_environ_like(obj):
# os.environ or similar
keys = list(os.environ.keys())
elif _is_dataframe_like(obj):
# pandas/polars DataFrame
with contextlib.suppress(Exception):
keys = list(obj.columns)
elif _is_series_like(obj):
# pandas Series with string index
with contextlib.suppress(Exception):
keys = [str(k) for k in obj.index if isinstance(k, str)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure yet if it's a good idea, but we could consider using the existing inspectors code to abstract this sort of thing


# Try to evaluate the expression in the namespace
try:
obj = eval(expr, server.shell.user_ns)
Copy link
Contributor

Choose a reason for hiding this comment

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

How do other libs handle safe evaluation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like parso doesn't eval code at all. I'll see if I can adopt a similar strategy (or just use parso)

logger.exception(f"Failed to publish diagnostics for {uri}", exc_info=True)


def _publish_diagnostics(server: PositronLanguageServer, uri: str) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this only run in console?

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.

3 participants