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
2 changes: 1 addition & 1 deletion .github/workflows/lsp-devtools-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
hatch build

- name: 'Upload Artifact'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: 'dist'
path: lib/lsp-devtools/dist
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pytest-lsp-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
hatch build

- name: 'Upload Artifact'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: 'dist'
path: lib/pytest-lsp/dist
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.7
rev: v0.14.10
hooks:
- id: ruff
name: ruff (lsp-devtools)
Expand All @@ -33,7 +33,7 @@ repos:
files: 'lib/pytest-lsp/.*\.py'

- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.19.0'
rev: 'v1.19.1'
hooks:
- id: mypy
name: mypy (pytest-lsp)
Expand Down
1 change: 1 addition & 0 deletions lib/lsp-devtools/changes/229.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix issue where `lsp-devtools inspect` would not exit cleanly
1 change: 1 addition & 0 deletions lib/lsp-devtools/changes/231.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix crash caused by trying to dismiss the message filter dialog twice.
1 change: 1 addition & 0 deletions lib/lsp-devtools/changes/233.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bump textual to `>=8.1.0`
3 changes: 3 additions & 0 deletions lib/lsp-devtools/changes/234.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The server command used with the `lsp-devtools client` command can now be configured at runtime.

Server process lifecycle information and errors are now also logged to the `Server` panel.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
import importlib.metadata
import logging
import os
import typing

Expand All @@ -10,28 +11,28 @@
from textual import events
from textual import on
from textual.app import App
from textual.containers import Vertical
from textual.message import Message
from textual.widgets import Button
from textual.widgets import Footer

from lsp_devtools.cli.utils import LiveSqlHandler
from lsp_devtools.client import LanguageClient
from lsp_devtools.editor import Explorer
from lsp_devtools.editor import OutputWindow
from lsp_devtools.editor import Panel
from lsp_devtools.editor import TextEditorView
from lsp_devtools.inspector.message_browser import MessageBrowser
from lsp_devtools.inspector import MessageBrowser

from .client import ClientState
from .client import LanguageClient
from .config import AppConfig
from .config import ConfigurationScreen

if typing.TYPE_CHECKING:
from textual.app import ComposeResult
from textual.widgets import DirectoryTree


class StderrReceived(Message):
def __init__(self, data: bytes):
super().__init__()
self.data = data


@typing.final
class LSPClient(App[None]):
"""A simple LSP client."""
Expand All @@ -41,11 +42,16 @@ class LSPClient(App[None]):
display: none;
}

Explorer {
#left-sidebar {
dock: left;
width: 15%;
}

#open-settings-btn {
margin: 1;
width: 100%;
}

MessageBrowser {
dock: right;
width: 30%;
Expand All @@ -64,14 +70,15 @@ class LSPClient(App[None]):
BINDINGS = [
("ctrl+c", "quit"),
("f2", "toggle_explorer", "Explorer"),
("f5", "run_server", "Run Server"),
("f8", "toggle_panel", "Panel"),
("f12", "toggle_devtools", "Devtools"),
]

def __init__(self, *args, server_command: list[str], **kwargs):
def __init__(self, *args, config: AppConfig, **kwargs):
super().__init__(*args, **kwargs)

self.server_command = server_command
self.config: AppConfig = config
self.client: LanguageClient | None = None

self.db = LiveSqlHandler()
Expand All @@ -84,78 +91,119 @@ def compose(self) -> ComposeResult:

# Sidebars
yield MessageBrowser()
yield Explorer()

with Vertical(id="left-sidebar"):
yield Explorer()
yield Button("Settings", id="open-settings-btn")

# Footer
yield Footer()

def action_open_settings(self):
def maybe_update_config(new_config: AppConfig | None):
if new_config is not None:
# TODO: Restart server as required.
self.config = new_config

_ = self.push_screen(
ConfigurationScreen(config=self.config), maybe_update_config
)

def action_toggle_explorer(self) -> None:
explorer = self.query_one(Explorer)
explorer = self.query_one("#left-sidebar")
is_visible = not explorer.has_class("hidden")

if is_visible:
explorer.add_class("hidden")
_ = explorer.add_class("hidden")

else:
explorer.remove_class("hidden")
_ = explorer.remove_class("hidden")
self.screen.set_focus(explorer)

def action_toggle_devtools(self) -> None:
devtools = self.query_one(MessageBrowser)
is_visible = not devtools.has_class("hidden")

if is_visible:
devtools.add_class("hidden")
_ = devtools.add_class("hidden")

else:
devtools.remove_class("hidden")
_ = devtools.remove_class("hidden")
self.screen.set_focus(devtools)

def action_toggle_panel(self) -> None:
panel = self.query_one(Panel)
is_visible = not panel.has_class("hidden")

if is_visible:
panel.add_class("hidden")
_ = panel.add_class("hidden")

else:
panel.remove_class("hidden")
_ = panel.remove_class("hidden")
self.screen.set_focus(panel)

def on_ready(self, event: events.Ready):
self.run_worker(self.start_server(), name="lsp-connection")
async def action_run_server(self):
_ = self.run_worker(self.run_server())

async def on_ready(self, event: events.Ready):
# Auto start server if possible.
if len(self.config.server.command) > 0:
await self.action_run_server()

@on(LiveSqlHandler.MessageReceived)
def on_message_received(self, event: LiveSqlHandler.MessageReceived):
browser = self.query_one(MessageBrowser)
browser.reload(follow=True)

@on(StderrReceived)
def on_stderr_received(self, event: StderrReceived):
panel = self.query_one(Panel)
log = panel.query_one("#stderr-window", OutputWindow)
log.write(event.data)
def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "open-settings-btn":
self.action_open_settings()

def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected):
"""Handle file-open."""
editor = self.query_one(TextEditorView)
editor.open_text_document(event.path)

async def start_server(self):
async def run_server(self):
"""Start, or restart the server."""
if self.client is None:
self.client = await self.start_server()
return

# Don't interfere with a client that is starting up.
if self.client.state in {ClientState.Starting}:
return

if self.client.state in {ClientState.Running}:
await self.stop_server()

self.client = await self.start_server()

async def start_server(self) -> LanguageClient | None:
"""Start the server and connect to it."""

def stderr_handler(data: bytes):
self.app.post_message(StderrReceived(data))
server_config = self.config.server
if len(server_config.command) == 0:
# TODO: Prompt user to set a command.
return

self.client = LanguageClient(
output_window = self.query_one("#stderr-window", OutputWindow)
output_window.clear()

server_logger = logging.getLogger("server")
server_logger.setLevel(logging.DEBUG)
server_logger.addHandler(output_window.log_handler)

client = LanguageClient(
self.db,
stderr_handler=stderr_handler,
logger=server_logger,
name="lsp-devtools",
version=importlib.metadata.version("lsp-devtools"),
)
await self.client.start_io(*self.server_command)

result = await self.client.initialize_async(
await client.start_io(*server_config.command)

result = await client.initialize_async(
types.InitializeParams(
capabilities=types.ClientCapabilities(),
process_id=os.getpid(),
Expand All @@ -167,16 +215,28 @@ def stderr_handler(data: bytes):
],
)
)
self.client.initialized(types.InitializedParams())
client.initialized(types.InitializedParams())
return client

async def stop_server(self):
if self.client is None or self.client.state not in {ClientState.Running}:
return

await self.client.shutdown_async(None)
self.client.exit(None)

await self.client.stop()


def client(args, extra: list[str]):
if len(extra) == 0:
raise ValueError(
"Missing server command. (e.g. lsp-devtools client -- server-cmd --stdio)"
)
# TODO: Read configs from file.
config = AppConfig()

# Allow for a server command to be passed on the cli.
if len(extra) > 0:
config.server.command = extra

app = LSPClient(server_command=extra)
app = LSPClient(config=config)
app.run()


Expand Down
Loading