Repository: sublimelsp/LSP

Issue:
Noticeable lag on closing a file
* OSX 10.15
* branch: st4000-exploration (8d81a29e54bd7ad148592f0d5c9443b795e32bf2)

There is a noticeable delay on closing a file before the previous file gets activated.

Uploading two videos - one with LSP enabled and one with disabled. You can notice that after closing the tab, the current tab remains in the tab bar for a while.
[demonstration.zip](https://github.com/sublimelsp/LSP/files/4649142/demonstration.zip)

"Profile plugins" doesn't seem to represent that lag in numbers.


Discussion:
Note that when using mouse to close the file, the previous file shows up immediately even though the lag is still there.
When closing file with the keyboard (more realistic scenario) the previous file doesn't show up until after the short lag.
`master` branch doesn't have this issue and feels much more snappy on closing the file.
I'm guessing this is due to a `__del__` in the completion view event listener. It is needed though.

Expect to see more usage of `__del__` as cleaning up resources is important and allows a more dynamic approach and/or allows one to reason about objects locally.
It doesn't seem to be that. I've disabled it and it didn't help. Also tried disabling `on_close` and a couple of other things but nothing made it as snappy as on `master`. Don't have more time to investigate right now.
Oh... then I have no idea 😄
Git bisect found out the guilty one: 54b9adb31dcff9802ee9b1639a0eb707aa7c5519

It looks like it's `is_supported_syntax` function that is called a lot on de(activating) views and makes things slow.

Not sure if the main offender is `sublime.list_syntaxes` or `sublime.score_selector` but probably one of those.
You can try making it a little faster by optimizing `_needed_configs` in WindowManager, perhaps. We're also calling that function twice for reasons I don't quite understand.

And we could use [lru_cache](https://docs.python.org/3/library/functools.html#functools.lru_cache) for the `syntax2scope` and `view2scope` functions, perhaps.
The `is_supported_syntax` function is also called by every view event listener instance. Yet another reason to move towards one view event listener.
#1059 cleans up the code so that we are using `is_supported_syntax` only from one place (`LSPViewEventListener`). It will make it easier to implement caching.

Caching needs to be invalidated when configs change though so that makes things a bit more tricky.

Resolving patch:
diff --git a/plugin/core/configurations.py b/plugin/core/configurations.py
--- a/plugin/core/configurations.py
+++ b/plugin/core/configurations.py
@@ -1,19 +1,12 @@
 from .collections import DottedDict
 from .logging import debug
-from .types import ClientConfig, WindowLike, syntax2scope, view2scope
-from .typing import Any, Generator, List, Dict, Iterable
+from .types import ClientConfig, WindowLike, view2scope
+from .typing import Any, Generator, List, Dict
 from .workspace import enable_in_project, disable_in_project
 from copy import deepcopy
 import sublime
 
 
-def is_supported_syntax(syntax: str, configs: Iterable[ClientConfig]) -> bool:
-    scope = syntax2scope(syntax)
-    if scope is not None:
-        return any(config.match_document(scope) for config in configs)
-    return False
-
-
 class ConfigManager(object):
     """Distributes language client configuration between windows"""
 
diff --git a/plugin/core/registry.py b/plugin/core/registry.py
--- a/plugin/core/registry.py
+++ b/plugin/core/registry.py
@@ -1,7 +1,7 @@
 import sublime
 import sublime_plugin
 from .clients import start_window_config
-from .configurations import ConfigManager, is_supported_syntax
+from .configurations import ConfigManager
 from .handlers import LanguageHandler
 from .logging import debug
 from .rpc import Client
@@ -24,10 +24,7 @@ def __init__(self, view: sublime.View) -> None:
     @classmethod
     def has_supported_syntax(cls, view_settings: dict) -> bool:
         syntax = view_settings.get('syntax')
-        if syntax:
-            return is_supported_syntax(syntax, client_configs.all)
-        else:
-            return False
+        return bool(syntax and client_configs.is_syntax_supported(syntax))
 
     @property
     def manager(self) -> WindowManager:
diff --git a/plugin/core/settings.py b/plugin/core/settings.py
--- a/plugin/core/settings.py
+++ b/plugin/core/settings.py
@@ -92,6 +92,7 @@ def __init__(self) -> None:
         self._external_configs = dict()  # type: Dict[str, ClientConfig]
         self.all = []  # type: List[ClientConfig]
         self._listener = None  # type: Optional[Callable]
+        self._supported_syntaxes_cache = dict()  # type: Dict[str, bool]
 
     def update(self, settings_obj: sublime.Settings) -> None:
         self._default_settings = read_dict_setting(settings_obj, "default_clients", {})
@@ -103,6 +104,7 @@ def add_external_config(self, config: ClientConfig) -> None:
 
     def update_configs(self) -> None:
         del self.all[:]
+        self._supported_syntaxes_cache = dict()
 
         for config_name, config in self._external_configs.items():
             user_settings = self._global_settings.get(config_name, dict())
@@ -136,6 +138,14 @@ def disable(self, config_name: str) -> None:
     def set_listener(self, recipient: Callable) -> None:
         self._listener = recipient
 
+    def is_syntax_supported(self, syntax: str) -> bool:
+        if syntax in self._supported_syntaxes_cache:
+            return self._supported_syntaxes_cache[syntax]
+        scope = syntax2scope(syntax)
+        supported = bool(scope and any(config.match_document(scope) for config in self.all))
+        self._supported_syntaxes_cache[syntax] = supported
+        return supported
+
 
 _settings_obj = None  # type: Optional[sublime.Settings]
 settings = Settings()


Test patch:
diff --git a/tests/test_configurations.py b/tests/test_configurations.py
--- a/tests/test_configurations.py
+++ b/tests/test_configurations.py
@@ -1,6 +1,6 @@
 from LSP.plugin.core.configurations import ConfigManager
-from LSP.plugin.core.configurations import is_supported_syntax
 from LSP.plugin.core.configurations import WindowConfigManager
+from LSP.plugin.core.settings import client_configs
 from test_mocks import DISABLED_CONFIG
 from test_mocks import TEST_CONFIG
 from test_mocks import TEST_LANGUAGE
@@ -92,9 +92,22 @@ def test_disables_temporarily(self):
 
 class IsSupportedSyntaxTests(unittest.TestCase):
 
-    def test_no_configs(self):
-        self.assertFalse(is_supported_syntax('asdf', []))
+    def tearDown(self) -> None:
+        # Resets state of configs to match settings
+        client_configs.update_configs()
+
+    def test_has_no_matching_config(self) -> None:
+        self.assertFalse(client_configs.is_syntax_supported('asdf'))
 
-    def test_single_config(self):
+    def test_has_matching_config(self) -> None:
+        client_configs.all.append(TEST_CONFIG)
         self.assertEqual(TEST_LANGUAGE.feature_selector, TEST_CONFIG.languages[0].feature_selector)
-        self.assertTrue(is_supported_syntax("Packages/Text/Plain text.tmLanguage", [TEST_CONFIG]))
+        self.assertTrue(client_configs.is_syntax_supported("Packages/Text/Plain text.tmLanguage"))
+        client_configs.all.remove(TEST_CONFIG)
+
+    def test_does_not_match_after_removing_config(self) -> None:
+        client_configs.all.append(TEST_CONFIG)
+        self.assertTrue(client_configs.is_syntax_supported("Packages/Text/Plain text.tmLanguage"))
+        # Removes manually added config
+        client_configs.update_configs()
+        self.assertFalse(client_configs.is_syntax_supported("Packages/Text/Plain text.tmLanguage"))