Skip to content

Commit

Permalink
Start the LSP via a request to the kernel positron.lsp comm (posit-de…
Browse files Browse the repository at this point in the history
  • Loading branch information
petetronic authored and wesm committed May 9, 2023
1 parent 566aa9a commit ba798e8
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 90 deletions.
61 changes: 61 additions & 0 deletions extensions/positron-python/pythonFiles/positron/lsp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#
# Copyright (C) 2023 Posit Software, PBC. All rights reserved.
#

import urllib.parse
from typing import Tuple

from .positron_jedilsp import POSITRON


class LSPService:
"""
LSPService manages the positron.lsp comm and cooridinates starting the LSP.
"""

def __init__(self, kernel): # noqa: F821
self.kernel = kernel
self.lsp_comm = None

def on_comm_open(self, comm, open_msg) -> None:
"""
Setup positron.lsp comm to receive messages.
"""
self.lsp_comm = comm
comm.on_msg(self.receive_message)
self.receive_open(open_msg)

def receive_open(self, msg) -> None:
"""
Start the LSP on the requested port.
"""
data = msg["content"]["data"]

client_address = data.get("client_address", None)
if client_address is not None:
host, port = self.split_address(client_address)
if host is not None and port is not None:
POSITRON.start(lsp_host=host, lsp_port=port, kernel=self.kernel)
return

raise ValueError("Invalid client_address in LSP open message")

def receive_message(self, msg) -> None:
"""
Handle messages received from the client via the positron.lsp comm.
"""
pass

def shutdown(self) -> None:
if self.lsp_comm is not None:
try:
self.lsp_comm.close()
except Exception:
pass

def split_address(self, client_address: str) -> Tuple[str | None, int | None]:
"""
Split an address of the form "host:port" into a tuple of (host, port).
"""
result = urllib.parse.urlsplit("//" + client_address)
return (result.hostname, result.port)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .dataviewer import DataViewerService
from .environment import EnvironmentService
from .inspectors import get_inspector
from .lsp import LSPService
from .plots import PositronDisplayPublisherHook

POSITRON_DATA_VIEWER_COMM = "positron.dataViewer"
Expand All @@ -24,6 +25,9 @@
POSITRON_ENVIRONMENT_COMM = "positron.environment"
"""The comm channel target_name for Positron's Environment View"""

POSITRON_LSP_COMM = "positron.lsp"
"""The comm channel target_name for Positron's LSP"""

POSITRON_PLOT_COMM = "positron.plot"
"""The comm channel target_name for Positron's Plots View"""

Expand Down Expand Up @@ -51,6 +55,10 @@ def __init__(self, **kwargs):
shell.events.register("post_execute", self.handle_post_execute)
self.get_user_ns_hidden().update(POSITON_NS_HIDDEN)

# Setup Positron's LSP service
self.lsp_service = LSPService(self)
self.comm_manager.register_target(POSITRON_LSP_COMM, self.lsp_service.on_comm_open)

# Setup Positron's environment service
self.env_service = EnvironmentService(self)
self.comm_manager.register_target(POSITRON_ENVIRONMENT_COMM, self.env_service.on_comm_open)
Expand All @@ -70,6 +78,7 @@ def do_shutdown(self, restart) -> dict:
result = super().do_shutdown(restart)
self.display_pub_hook.shutdown()
self.env_service.shutdown()
self.lsp_service.shutdown()
self.dataviewer_service.shutdown()
return result

Expand Down
30 changes: 10 additions & 20 deletions extensions/positron-python/pythonFiles/positron/positron_jedilsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from threading import Event
from typing import Any, Callable, List, Optional, Union

from ipykernel import kernelapp

from jedi.api import Interpreter
from jedi_language_server import jedi_utils, pygls_utils
from jedi_language_server.server import (
Expand Down Expand Up @@ -83,8 +83,6 @@
from pygls.capabilities import get_capability
from pygls.feature_manager import has_ls_param_or_annotation

from .positron_ipkernel import PositronIPyKernel


class PositronJediLanguageServer(JediLanguageServer):
"""Positron extenstion to the Jedi language server."""
Expand All @@ -111,38 +109,30 @@ def decorator(f):

return decorator

def start(self, lsp_host, lsp_port) -> None:
def start(self, lsp_host: str, lsp_port: int, kernel) -> None:
"""
Starts IPyKernel and the Jedi LSP in parallel. This arrangement allows
us to share the active namespaces of the IPyKernel's interpreter with
the Jedi LSP for enhanced completions.
Start the LSP with a reference to Positron's IPyKernel to enhance
completions with awareness of live variables from user's namespace.
"""
global KERNEL
KERNEL = kernel

loop = asyncio.get_event_loop()
try:
asyncio.ensure_future(self.start_ipykernel())
asyncio.ensure_future(self.start_jedi(lsp_host, lsp_port))
asyncio.ensure_future(self._start_jedi(lsp_host, lsp_port))
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
loop.close()

async def start_jedi(self, lsp_host, lsp_port):
async def _start_jedi(self, lsp_host, lsp_port):
"""Starts Jedi LSP as a TCP server using existing asyncio loop."""
self._stop_event = Event()
loop = asyncio.get_event_loop()
self._server = await loop.create_server(self.lsp, lsp_host, lsp_port) # type: ignore
await self._server.serve_forever()

async def start_ipykernel(self) -> None:
"""Starts Positron's IPyKernel as the interpreter for our console."""
app = kernelapp.IPKernelApp.instance(kernel_class=PositronIPyKernel)
app.initialize()
# Register the kernel for enhanced LSP completions
global KERNEL
KERNEL = app.kernel
app.kernel.start()


POSITRON = PositronJediLanguageServer(
name="jedi-language-server",
Expand All @@ -151,7 +141,7 @@ async def start_ipykernel(self) -> None:
)


KERNEL: Optional[PositronIPyKernel] = None
KERNEL = None

# Server Features
# Unfortunately we need to re-register these as Pygls Feature Management does
Expand Down
67 changes: 33 additions & 34 deletions extensions/positron-python/pythonFiles/positron_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,27 @@
Server and IPyKernel in the same environment.
"""

import asyncio
import argparse
import logging
import os
import sys
import traceback
from typing import Tuple

from ipykernel import kernelapp

# Add the lib path to our sys path so jedi_language_server can find its references
EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "jedilsp"))
sys.path.insert(1, os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python"))

from positron.positron_jedilsp import POSITRON
from positron.positron_ipkernel import PositronIPyKernel


def initialize() -> Tuple[str, int]:
def initialize_config() -> None:
"""
Initialize the configuration for the Positron Python Language Server
and REPL Kernel.
Returns:
(str, int): TCP host and port of the LSP server
"""

# Given we're using TCP, support a subset of the Jedi LSP configuration
Expand All @@ -34,24 +33,12 @@ def initialize() -> Tuple[str, int]:
description="Positron Jedi language server: an LSP wrapper for jedi.",
)

parser.add_argument(
"--host",
help="host for web server (default 127.0.0.1)",
type=str,
default="127.0.0.1",
)
parser.add_argument(
"--debugport",
help="port for debugpy debugger",
type=int,
default=None,
)
parser.add_argument(
"--port",
help="port for web server (default 2087)",
type=int,
default=2087,
)
parser.add_argument(
"--logfile",
help="redirect logs to file specified",
Expand All @@ -73,7 +60,7 @@ def initialize() -> Tuple[str, int]:

log_level = {0: logging.WARN, 1: logging.INFO, 2: logging.DEBUG}.get(
args.verbose,
logging.INFO,
logging.DEBUG,
)

if args.logfile:
Expand All @@ -82,41 +69,53 @@ def initialize() -> Tuple[str, int]:
filemode="w",
level=log_level,
)
pass
else:
logging.basicConfig(stream=sys.stderr, level=log_level)

# Start the debugpy debugger if a port was specified
if args.debugport is not None:
import debugpy

debugpy.listen(args.debugport)
try:
import debugpy

return args.host, args.port
debugpy.listen(args.debugport)
except Exception as error:
logging.warning(f"Unable to start debugpy: {error}", exc_info=True)


def start(lsp_host: str, lsp_port: int):
"""
Starts Positron Python (based on the Jedi Language Server) to
suport both LSP and REPL functionality.
"""
exitStatus = POSITRON.start(lsp_host, lsp_port)
return exitStatus
async def start_ipykernel() -> None:
"""Starts Positron's IPyKernel as the interpreter for our console."""
app = kernelapp.IPKernelApp.instance(kernel_class=PositronIPyKernel)
app.initialize()
app.kernel.start()


if __name__ == "__main__":
exitStatus = 0

try:
lsp_host, lsp_port = initialize()
exitStatus = start(lsp_host, lsp_port)
# Init the configuration args
initialize_config()

# Start Positron's IPyKernel as the interpreter for our console.
loop = asyncio.get_event_loop()
try:
asyncio.ensure_future(start_ipykernel())
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
loop.close()

except SystemExit as error:
# TODO: Remove this workaround once we can improve Jedi
# disconnection logic
tb = "".join(traceback.format_tb(error.__traceback__))
if tb.find("connection_lost") > 0:
logging.warning("Positron LSP client disconnected, exiting.")
logging.warning("Positron Language Server client disconnected, exiting.")
exitStatus = 0
else:
logging.error("Error in Positron Jedi LSP: %s", error)
logging.error("Error in Positron Language Server: %s", error)
exitStatus = 1

sys.exit(exitStatus)
Loading

0 comments on commit ba798e8

Please sign in to comment.