Repository: sublimelsp/LSP

Issue:
Handle allowed messages from server while initializing.
As would have been helpful in the #613 troubleshooting:

> during the initialize request the server is allowed to send the notifications 
> window/showMessage, window/logMessage and telemetry/event as well as the window/showMessageRequest request to the client.


Discussion:

Resolving patch:
diff --git a/plugin/core/clients.py b/plugin/core/clients.py
--- a/plugin/core/clients.py
+++ b/plugin/core/clients.py
@@ -36,14 +36,22 @@ def get_window_env(window: sublime.Window, config: ClientConfig) -> 'Tuple[List[
     return expanded_args, env
 
 
-def start_window_config(window: sublime.Window, project_path: str, config: ClientConfig,
-                        on_created: 'Callable', on_ended: 'Callable[[str], None]') -> 'Optional[Session]':
+def start_window_config(window: sublime.Window,
+                        project_path: str,
+                        config: ClientConfig,
+                        on_pre_initialize: 'Callable[[Session], None]',
+                        on_post_initialize: 'Callable[[Session], None]',
+                        on_post_exit: 'Callable[[str], None]') -> 'Optional[Session]':
     args, env = get_window_env(window, config)
     config.binary_args = args
-    return create_session(config, project_path, env, settings,
-                          on_created=on_created,
-                          on_ended=lambda config_name: on_session_ended(window, config.name, on_ended))
+    return create_session(config=config,
+                          project_path=project_path,
+                          env=env,
+                          settings=settings,
+                          on_pre_initialize=on_pre_initialize,
+                          on_post_initialize=on_post_initialize,
+                          on_post_exit=lambda config_name: on_session_ended(window, config_name, on_post_exit))
 
 
-def on_session_ended(window: sublime.Window, config_name: str, on_ended_handler: 'Callable[[str], None]') -> None:
-    on_ended_handler(config_name)
+def on_session_ended(window: sublime.Window, config_name: str, on_post_exit_handler: 'Callable[[str], None]') -> None:
+    on_post_exit_handler(config_name)
diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py
--- a/plugin/core/sessions.py
+++ b/plugin/core/sessions.py
@@ -14,18 +14,32 @@
     pass
 
 
-def create_session(config: ClientConfig, project_path: str, env: dict, settings: Settings,
-                   on_created=None, on_ended: 'Optional[Callable[[str], None]]' = None,
+def create_session(config: ClientConfig,
+                   project_path: str,
+                   env: dict,
+                   settings: Settings,
+                   on_pre_initialize: 'Optional[Callable[[Session], None]]' = None,
+                   on_post_initialize: 'Optional[Callable[[Session], None]]' = None,
+                   on_post_exit: 'Optional[Callable[[str], None]]' = None,
                    bootstrap_client=None) -> 'Optional[Session]':
+
+    def with_client(client) -> 'Session':
+        return Session(
+            config=config,
+            project_path=project_path,
+            client=client,
+            on_pre_initialize=on_pre_initialize,
+            on_post_initialize=on_post_initialize,
+            on_post_exit=on_post_exit)
+
     session = None
     if config.binary_args:
-
         process = start_server(config.binary_args, project_path, env, settings.log_stderr)
         if process:
             if config.tcp_port:
                 transport = start_tcp_transport(config.tcp_port, config.tcp_host)
                 if transport:
-                    session = Session(config, project_path, Client(transport, settings), on_created, on_ended)
+                    session = with_client(Client(transport, settings))
                 else:
                     # try to terminate the process
                     try:
@@ -33,20 +47,15 @@ def create_session(config: ClientConfig, project_path: str, env: dict, settings:
                     except Exception:
                         pass
             else:
-                client = attach_stdio_client(process, settings)
-                session = Session(config, project_path, client, on_created, on_ended)
+                session = with_client(attach_stdio_client(process, settings))
     else:
         if config.tcp_port:
             transport = start_tcp_transport(config.tcp_port)
-
-            session = Session(config, project_path, Client(transport, settings),
-                              on_created, on_ended)
+            session = with_client(Client(transport, settings))
         elif bootstrap_client:
-            session = Session(config, project_path, bootstrap_client,
-                              on_created, on_ended)
+            session = with_client(bootstrap_client)
         else:
             debug("No way to start session")
-
     return session
 
 
@@ -120,15 +129,22 @@ def get_initialize_params(project_path: str, config: ClientConfig):
 
 
 class Session(object):
-    def __init__(self, config: ClientConfig, project_path, client: Client,
-                 on_created=None, on_ended: 'Optional[Callable[[str], None]]' = None) -> None:
+    def __init__(self,
+                 config: ClientConfig,
+                 project_path: str,
+                 client: Client,
+                 on_pre_initialize: 'Optional[Callable[[Session], None]]' = None,
+                 on_post_initialize: 'Optional[Callable[[Session], None]]' = None,
+                 on_post_exit: 'Optional[Callable[[str], None]]' = None) -> None:
         self.config = config
         self.project_path = project_path
         self.state = ClientStates.STARTING
-        self._on_created = on_created
-        self._on_ended = on_ended
+        self._on_post_initialize = on_post_initialize
+        self._on_post_exit = on_post_exit
         self.capabilities = dict()  # type: Dict[str, Any]
         self.client = client
+        if on_pre_initialize:
+            on_pre_initialize(self)
         self.initialize()
 
     def has_capability(self, capability):
@@ -146,8 +162,8 @@ def initialize(self):
     def _handle_initialize_result(self, result):
         self.state = ClientStates.READY
         self.capabilities = result.get('capabilities', dict())
-        if self._on_created:
-            self._on_created(self)
+        if self._on_post_initialize:
+            self._on_post_initialize(self)
 
     def end(self):
         self.state = ClientStates.STOPPING
@@ -160,5 +176,5 @@ def _handle_shutdown_result(self):
         self.client.exit()
         self.client = None
         self.capabilities = dict()
-        if self._on_ended:
-            self._on_ended(self.config.name)
+        if self._on_post_exit:
+            self._on_post_exit(self.config.name)
diff --git a/plugin/core/windows.py b/plugin/core/windows.py
--- a/plugin/core/windows.py
+++ b/plugin/core/windows.py
@@ -383,9 +383,13 @@ def _start_client(self, config: ClientConfig):
         debug("starting in", project_path)
         session = None  # type: Optional[Session]
         try:
-            session = self._start_session(self._window, project_path, config,
-                                          lambda session: self._handle_session_started(session, project_path, config),
-                                          lambda config_name: self._handle_session_ended(config_name))
+            session = self._start_session(
+                window=self._window,
+                project_path=project_path,
+                config=config,
+                on_pre_initialize=self._handle_pre_initialize,
+                on_post_initialize=self._handle_post_initialize,
+                on_post_exit=self._handle_post_exit)
         except Exception as e:
             message = "\n\n".join([
                 "Could not start {}",
@@ -447,33 +451,36 @@ def _apply_workspace_edit(self, params: 'Dict[str, Any]', client: Client, reques
         # reconstruct/get the actual Client object back. Maybe we can (ab)use our homebrew event system for this?
         client.send_response(Response(request_id, {"applied": True}))
 
-    def _handle_session_started(self, session, project_path, config):
+    def _handle_pre_initialize(self, session: 'Session') -> None:
         client = session.client
-        client.set_crash_handler(lambda: self._handle_server_crash(config))
-        client.set_error_display_handler(lambda msg: self._window.status_message(msg))
-
-        # handle server requests and notifications
-        client.on_request(
-            "workspace/applyEdit",
-            lambda params, request_id: self._apply_workspace_edit(params, client, request_id))
+        client.set_crash_handler(lambda: self._handle_server_crash(session.config))
+        client.set_error_display_handler(self._window.status_message)
 
         client.on_request(
             "window/showMessageRequest",
             lambda params, request_id: self._handle_message_request(params, client, request_id))
 
-        client.on_notification(
-            "textDocument/publishDiagnostics",
-            lambda params: self._diagnostics.handle_client_diagnostics(config.name, params))
-
         client.on_notification(
             "window/showMessage",
             lambda params: self._sublime.message_dialog(params.get("message")))
 
         client.on_notification(
             "window/logMessage",
-            lambda params: server_log(config.name, params.get("message", "???") if params else "???"))
+            lambda params: server_log(session.config.name, params.get("message", "???") if params else "???"))
 
-        self._handlers.on_initialized(config.name, self._window, client)
+    def _handle_post_initialize(self, session: 'Session') -> None:
+        client = session.client
+
+        # handle server requests and notifications
+        client.on_request(
+            "workspace/applyEdit",
+            lambda params, request_id: self._apply_workspace_edit(params, client, request_id))
+
+        client.on_notification(
+            "textDocument/publishDiagnostics",
+            lambda params: self._diagnostics.handle_client_diagnostics(session.config.name, params))
+
+        self._handlers.on_initialized(session.config.name, self._window, client)
 
         client.send_notification(Notification.initialized())
 
@@ -483,13 +490,13 @@ def _handle_session_started(self, session, project_path, config):
 
         global_events.subscribe('view.on_close', lambda view: self._handle_view_closed(view, session))
 
-        if config.settings:
+        if session.config.settings:
             configParams = {
-                'settings': config.settings
+                'settings': session.config.settings
             }
             client.send_notification(Notification.didChangeConfiguration(configParams))
 
-        self._window.status_message("{} initialized".format(config.name))
+        self._window.status_message("{} initialized".format(session.config.name))
 
     def _handle_view_closed(self, view, session):
         if view.file_name():
@@ -523,12 +530,13 @@ def _handle_all_sessions_ended(self):
             if self._on_closed:
                 self._on_closed()
 
-    def _handle_session_ended(self, config_name):
+    def _handle_post_exit(self, config_name: str) -> None:
         self._documents.remove_session(config_name)
         del self._sessions[config_name]
         for view in self._window.views():
-            if view.file_name():
-                self._diagnostics.remove(view.file_name(), config_name)
+            file_name = view.file_name()
+            if file_name:
+                self._diagnostics.remove(file_name, config_name)
 
         debug("session", config_name, "ended")
         if not self._sessions:


Test patch:
diff --git a/plugin/core/test_session.py b/plugin/core/test_session.py
--- a/plugin/core/test_session.py
+++ b/plugin/core/test_session.py
@@ -89,39 +89,62 @@ def test_can_create_session(self):
 
     def test_can_get_started_session(self):
         project_path = "/"
-        created_callback = unittest.mock.Mock()
+        post_initialize_callback = unittest.mock.Mock()
         session = self.assert_if_none(
-            create_session(test_config, project_path, dict(), Settings(),
+            create_session(config=test_config,
+                           project_path=project_path,
+                           env=dict(),
+                           settings=Settings(),
                            bootstrap_client=MockClient(),
-                           on_created=created_callback))
+                           on_post_initialize=post_initialize_callback))
+        self.assertEqual(session.state, ClientStates.READY)
+        self.assertIsNotNone(session.client)
+        self.assertEqual(session.project_path, project_path)
+        self.assertTrue(session.has_capability("testing"))
+        self.assertTrue(session.get_capability("testing"))
+        post_initialize_callback.assert_called_once()
 
+    def test_pre_initialize_callback_is_invoked(self):
+        project_path = "/"
+        pre_initialize_callback = unittest.mock.Mock()
+        post_initialize_callback = unittest.mock.Mock()
+        session = self.assert_if_none(
+            create_session(config=test_config,
+                           project_path=project_path,
+                           env=dict(),
+                           settings=Settings(),
+                           bootstrap_client=MockClient(),
+                           on_pre_initialize=pre_initialize_callback,
+                           on_post_initialize=post_initialize_callback))
         self.assertEqual(session.state, ClientStates.READY)
         self.assertIsNotNone(session.client)
         self.assertEqual(session.project_path, project_path)
         self.assertTrue(session.has_capability("testing"))
         self.assertTrue(session.get_capability("testing"))
-        created_callback.assert_called_once()
+        pre_initialize_callback.assert_called_once()
+        post_initialize_callback.assert_called_once()
 
     def test_can_shutdown_session(self):
         project_path = "/"
-        created_callback = unittest.mock.Mock()
-        ended_callback = unittest.mock.Mock()
+        post_initialize_callback = unittest.mock.Mock()
+        post_exit_callback = unittest.mock.Mock()
         session = self.assert_if_none(
-            create_session(test_config, project_path, dict(), Settings(),
+            create_session(config=test_config,
+                           project_path=project_path,
+                           env=dict(),
+                           settings=Settings(),
                            bootstrap_client=MockClient(),
-                           on_created=created_callback,
-                           on_ended=ended_callback))
-
+                           on_post_initialize=post_initialize_callback,
+                           on_post_exit=post_exit_callback))
         self.assertEqual(session.state, ClientStates.READY)
         self.assertIsNotNone(session.client)
         self.assertEqual(session.project_path, project_path)
         self.assertTrue(session.has_capability("testing"))
-        created_callback.assert_called_once()
-
+        post_initialize_callback.assert_called_once()
         session.end()
         self.assertEqual(session.state, ClientStates.STOPPING)
         self.assertEqual(session.project_path, project_path)
         self.assertIsNone(session.client)
         self.assertFalse(session.has_capability("testing"))
         self.assertIsNone(session.get_capability("testing"))
-        ended_callback.assert_called_once()
+        post_exit_callback.assert_called_once()
diff --git a/plugin/core/test_windows.py b/plugin/core/test_windows.py
--- a/plugin/core/test_windows.py
+++ b/plugin/core/test_windows.py
@@ -220,11 +220,21 @@ def for_window(self, window, configs):
         return MockDocuments()
 
 
-def mock_start_session(window, project_path, config, on_created: 'Callable', on_ended: 'Callable'):
-    return create_session(test_config, project_path, dict(), MockSettings(),
-                          bootstrap_client=MockClient(),
-                          on_created=on_created,
-                          on_ended=on_ended)
+def mock_start_session(window: MockWindow,
+                       project_path: str,
+                       config: ClientConfig,
+                       on_pre_initialize: 'Callable[[Session], None]',
+                       on_post_initialize: 'Callable[[Session], None]',
+                       on_post_exit: 'Callable[[str], None]') -> 'Optional[Session]':
+    return create_session(
+        config=test_config,
+        project_path=project_path,
+        env=dict(),
+        settings=MockSettings(),
+        bootstrap_client=MockClient(),
+        on_pre_initialize=on_pre_initialize,
+        on_post_initialize=on_post_initialize,
+        on_post_exit=on_post_exit)
 
 
 class WindowRegistryTests(unittest.TestCase):
diff --git a/tests/setup.py b/tests/setup.py
--- a/tests/setup.py
+++ b/tests/setup.py
@@ -37,11 +37,11 @@ def inject_session(wm, config, client):
     # session.state = ClientStates.READY
     wm.update_configs(client_configs.all)
     wm._sessions[config.name] = session
-    wm._handle_session_started(session, "", config)
+    wm._handle_post_initialize(session)
 
 
 def remove_session(wm, config_name):
-    wm._handle_session_ended(config_name)
+    wm._handle_post_exit(config_name)
 
 
 def close_test_view(view):