Repository: sublimelsp/LSP

Issue:
Add LspWindowCommand
It would be good to have a LspWindowCommand, which is enabled when there is any active session with a certain name/capability in this window (i.e. attached to any existing view of the window).

An example use case is to show documentation provided by the language server in a separate tab as `HtmlSheet` (rendered minihtml). It might be desired to run a request to the server, when a link within the HtmlSheet is clicked, but this is not possible via a LspTextCommand, because a HtmlSheet is not a `View`.

I have an implementation for the Julia language server, currently with an [ugly workaround](https://github.com/sublimelsp/LSP-julia/blob/eba9ea3bb9ab043dd21a422ed62a2b1e9fc30bc6/plugin.py#L34) to store a reference to the session in a global variable when a certain LspTextCommand is run. Later, this global variable is used from a normal `sublime_plugin.WindowCommand` to run the server request. This has several drawbacks:
- The WindowCommand only works after the LspTextCommand has been invoked at least one time
- By using a single global variable for the session, the WindowCommand is not actually bound to the particular window. It is also active for other, entirely unrelated windows, and also it would be problematic if there exist multiple sessions of the language server at the same time.
- There is no way to check if the session is still alive


Discussion:

Resolving patch:
diff --git a/plugin/__init__.py b/plugin/__init__.py
--- a/plugin/__init__.py
+++ b/plugin/__init__.py
@@ -9,6 +9,8 @@
 from .core.protocol import Request
 from .core.protocol import Response
 from .core.protocol import WorkspaceFolder
+from .core.registry import LspTextCommand
+from .core.registry import LspWindowCommand
 from .core.sessions import AbstractPlugin
 from .core.sessions import register_plugin
 from .core.sessions import Session
@@ -34,6 +36,8 @@
     'FileWatcherEvent',
     'FileWatcherEventType',
     'FileWatcherProtocol',
+    'LspTextCommand',
+    'LspWindowCommand',
     'MarkdownLangMap',
     'matches_pattern',
     'Notification',
diff --git a/plugin/core/registry.py b/plugin/core/registry.py
--- a/plugin/core/registry.py
+++ b/plugin/core/registry.py
@@ -38,18 +38,46 @@ def get_position(view: sublime.View, event: Optional[dict] = None, point: Option
         return None
 
 
+class LspWindowCommand(sublime_plugin.WindowCommand):
+    """
+    Inherit from this class to define requests which are not bound to a particular view. This allows to run requests
+    for example from links in HtmlSheets or when an unrelated file has focus.
+    """
+
+    # When this is defined in a derived class, the command is enabled only if there exists a session with the given
+    # capability attached to a view in the window.
+    capability = ''
+
+    # When this is defined in a derived class, the command is enabled only if there exists a session with the given
+    # name attached to a view in the window.
+    session_name = ''
+
+    def is_enabled(self) -> bool:
+        return self.session() is not None
+
+    def session(self) -> Optional[Session]:
+        for session in windows.lookup(self.window).get_sessions():
+            if self.capability and not session.has_capability(self.capability):
+                continue
+            if self.session_name and session.config.name != self.session_name:
+                continue
+            return session
+        else:
+            return None
+
+
 class LspTextCommand(sublime_plugin.TextCommand):
     """
     Inherit from this class to define your requests that should be triggered via the command palette and/or a
     keybinding.
     """
 
-    # When this is defined in a derived class, the command is enabled only if there exists a session attached to the
-    # view that has the given capability.
+    # When this is defined in a derived class, the command is enabled only if there exists a session with the given
+    # capability attached to the active view.
     capability = ''
 
-    # When this is defined in a derived class, the command is enabled only if there exists a session attached to the
-    # view that has the given name.
+    # When this is defined in a derived class, the command is enabled only if there exists a session with the given
+    # name attached to the active view.
     session_name = ''
 
     def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) -> bool:
diff --git a/plugin/core/windows.py b/plugin/core/windows.py
--- a/plugin/core/windows.py
+++ b/plugin/core/windows.py
@@ -85,6 +85,9 @@ def __init__(
     def get_config_manager(self) -> WindowConfigManager:
         return self._configs
 
+    def get_sessions(self) -> Generator[Session, None, None]:
+        yield from self._sessions
+
     def on_load_project_async(self) -> None:
         self.update_workspace_folders_async()
         self._configs.update()


Test patch: