22
33import argparse
44import importlib .metadata
5+ import logging
56import os
67import typing
78
1011from textual import events
1112from textual import on
1213from textual .app import App
14+ from textual .containers import Vertical
1315from textual .message import Message
16+ from textual .widgets import Button
1417from textual .widgets import Footer
1518
1619from lsp_devtools .cli .utils import LiveSqlHandler
17- from lsp_devtools .client import LanguageClient
1820from lsp_devtools .editor import Explorer
1921from lsp_devtools .editor import OutputWindow
2022from lsp_devtools .editor import Panel
2123from lsp_devtools .editor import TextEditorView
22- from lsp_devtools .inspector .message_browser import MessageBrowser
24+ from lsp_devtools .inspector import MessageBrowser
25+
26+ from .client import ClientState
27+ from .client import LanguageClient
28+ from .config import AppConfig
29+ from .config import ConfigurationScreen
2330
2431if typing .TYPE_CHECKING :
2532 from textual .app import ComposeResult
2633 from textual .widgets import DirectoryTree
2734
2835
29- class StderrReceived (Message ):
30- def __init__ (self , data : bytes ):
31- super ().__init__ ()
32- self .data = data
33-
34-
3536@typing .final
3637class LSPClient (App [None ]):
3738 """A simple LSP client."""
@@ -41,11 +42,16 @@ class LSPClient(App[None]):
4142 display: none;
4243 }
4344
44- Explorer {
45+ #left-sidebar {
4546 dock: left;
4647 width: 15%;
4748 }
4849
50+ #open-settings-btn {
51+ margin: 1;
52+ width: 100%;
53+ }
54+
4955 MessageBrowser {
5056 dock: right;
5157 width: 30%;
@@ -64,14 +70,15 @@ class LSPClient(App[None]):
6470 BINDINGS = [
6571 ("ctrl+c" , "quit" ),
6672 ("f2" , "toggle_explorer" , "Explorer" ),
73+ ("f5" , "run_server" , "Run Server" ),
6774 ("f8" , "toggle_panel" , "Panel" ),
6875 ("f12" , "toggle_devtools" , "Devtools" ),
6976 ]
7077
71- def __init__ (self , * args , server_command : list [ str ] , ** kwargs ):
78+ def __init__ (self , * args , config : AppConfig , ** kwargs ):
7279 super ().__init__ (* args , ** kwargs )
7380
74- self .server_command = server_command
81+ self .config : AppConfig = config
7582 self .client : LanguageClient | None = None
7683
7784 self .db = LiveSqlHandler ()
@@ -84,78 +91,119 @@ def compose(self) -> ComposeResult:
8491
8592 # Sidebars
8693 yield MessageBrowser ()
87- yield Explorer ()
94+
95+ with Vertical (id = "left-sidebar" ):
96+ yield Explorer ()
97+ yield Button ("Settings" , id = "open-settings-btn" )
8898
8999 # Footer
90100 yield Footer ()
91101
102+ def action_open_settings (self ):
103+ def maybe_update_config (new_config : AppConfig | None ):
104+ if new_config is not None :
105+ # TODO: Restart server as required.
106+ self .config = new_config
107+
108+ _ = self .push_screen (
109+ ConfigurationScreen (config = self .config ), maybe_update_config
110+ )
111+
92112 def action_toggle_explorer (self ) -> None :
93- explorer = self .query_one (Explorer )
113+ explorer = self .query_one ("#left-sidebar" )
94114 is_visible = not explorer .has_class ("hidden" )
95115
96116 if is_visible :
97- explorer .add_class ("hidden" )
117+ _ = explorer .add_class ("hidden" )
98118
99119 else :
100- explorer .remove_class ("hidden" )
120+ _ = explorer .remove_class ("hidden" )
101121 self .screen .set_focus (explorer )
102122
103123 def action_toggle_devtools (self ) -> None :
104124 devtools = self .query_one (MessageBrowser )
105125 is_visible = not devtools .has_class ("hidden" )
106126
107127 if is_visible :
108- devtools .add_class ("hidden" )
128+ _ = devtools .add_class ("hidden" )
109129
110130 else :
111- devtools .remove_class ("hidden" )
131+ _ = devtools .remove_class ("hidden" )
112132 self .screen .set_focus (devtools )
113133
114134 def action_toggle_panel (self ) -> None :
115135 panel = self .query_one (Panel )
116136 is_visible = not panel .has_class ("hidden" )
117137
118138 if is_visible :
119- panel .add_class ("hidden" )
139+ _ = panel .add_class ("hidden" )
120140
121141 else :
122- panel .remove_class ("hidden" )
142+ _ = panel .remove_class ("hidden" )
123143 self .screen .set_focus (panel )
124144
125- def on_ready (self , event : events .Ready ):
126- self .run_worker (self .start_server (), name = "lsp-connection" )
145+ async def action_run_server (self ):
146+ _ = self .run_worker (self .run_server ())
147+
148+ async def on_ready (self , event : events .Ready ):
149+ # Auto start server if possible.
150+ if len (self .config .server .command ) > 0 :
151+ await self .action_run_server ()
127152
128153 @on (LiveSqlHandler .MessageReceived )
129154 def on_message_received (self , event : LiveSqlHandler .MessageReceived ):
130155 browser = self .query_one (MessageBrowser )
131156 browser .reload (follow = True )
132157
133- @on (StderrReceived )
134- def on_stderr_received (self , event : StderrReceived ):
135- panel = self .query_one (Panel )
136- log = panel .query_one ("#stderr-window" , OutputWindow )
137- log .write (event .data )
158+ def on_button_pressed (self , event : Button .Pressed ):
159+ if event .button .id == "open-settings-btn" :
160+ self .action_open_settings ()
138161
139162 def on_directory_tree_file_selected (self , event : DirectoryTree .FileSelected ):
140163 """Handle file-open."""
141164 editor = self .query_one (TextEditorView )
142165 editor .open_text_document (event .path )
143166
144- async def start_server (self ):
167+ async def run_server (self ):
168+ """Start, or restart the server."""
169+ if self .client is None :
170+ self .client = await self .start_server ()
171+ return
172+
173+ # Don't interfere with a client that is starting up.
174+ if self .client .state in {ClientState .Starting }:
175+ return
176+
177+ if self .client .state in {ClientState .Running }:
178+ await self .stop_server ()
179+
180+ self .client = await self .start_server ()
181+
182+ async def start_server (self ) -> LanguageClient | None :
145183 """Start the server and connect to it."""
146184
147- def stderr_handler (data : bytes ):
148- self .app .post_message (StderrReceived (data ))
185+ server_config = self .config .server
186+ if len (server_config .command ) == 0 :
187+ # TODO: Prompt user to set a command.
188+ return
149189
150- self .client = LanguageClient (
190+ output_window = self .query_one ("#stderr-window" , OutputWindow )
191+ output_window .clear ()
192+
193+ server_logger = logging .getLogger ("server" )
194+ server_logger .setLevel (logging .DEBUG )
195+ server_logger .addHandler (output_window .log_handler )
196+
197+ client = LanguageClient (
151198 self .db ,
152- stderr_handler = stderr_handler ,
199+ logger = server_logger ,
153200 name = "lsp-devtools" ,
154201 version = importlib .metadata .version ("lsp-devtools" ),
155202 )
156- await self .client .start_io (* self .server_command )
157203
158- result = await self .client .initialize_async (
204+ await client .start_io (* server_config .command )
205+
206+ result = await client .initialize_async (
159207 types .InitializeParams (
160208 capabilities = types .ClientCapabilities (),
161209 process_id = os .getpid (),
@@ -167,16 +215,28 @@ def stderr_handler(data: bytes):
167215 ],
168216 )
169217 )
170- self .client .initialized (types .InitializedParams ())
218+ client .initialized (types .InitializedParams ())
219+ return client
220+
221+ async def stop_server (self ):
222+ if self .client is None or self .client .state not in {ClientState .Running }:
223+ return
224+
225+ await self .client .shutdown_async (None )
226+ self .client .exit (None )
227+
228+ await self .client .stop ()
171229
172230
173231def client (args , extra : list [str ]):
174- if len (extra ) == 0 :
175- raise ValueError (
176- "Missing server command. (e.g. lsp-devtools client -- server-cmd --stdio)"
177- )
232+ # TODO: Read configs from file.
233+ config = AppConfig ()
234+
235+ # Allow for a server command to be passed on the cli.
236+ if len (extra ) > 0 :
237+ config .server .command = extra
178238
179- app = LSPClient (server_command = extra )
239+ app = LSPClient (config = config )
180240 app .run ()
181241
182242
0 commit comments