Repository: sublimelsp/LSP

Issue:
env handling in client settings not portable
There is no way to alter the env paths in a way that would work across all systems.

For example `LSP-pylsp` defines some env variables (https://github.com/sublimelsp/LSP-pylsp/blob/75a07c497fcca88cb2780230e559ae0e7402c361/LSP-pylsp.sublime-settings#L37-L37) that are PATH-like and are using path separator `:` but that is obviously not correct on Windows.

The only way to handle that comes to my mind right now is to have a template variable for the separator. The only small downside would be that it would not be easily discoverable.


Discussion:
Since you cannot have `:` in a filename on Windows, I guess you can replace all `:`s with `;`s behind the scene if the OS is Windows? And always use `:` as the separator in settings.
So we end up with paths like C;/Users/jfcherng?
> So we end up with paths like C;/Users/jfcherng?

What if the reverse way? Convert `;`s into `:`s if on non-Windows.


---

Or allowing using a list in `env` values and merged it into a str since all env values are str, actually.
> What if the reverse way? Convert `;`s into `:`s if on non-Windows.

Error prone. We don't know in which variables we have to do the replacement and if we do that in all, we might break something.

> Or allowing using a list in `env` values and merged it into a str since all env values are str, actually.

That is an option.
So we have two options right now:

```
"env": {
  "LIKEPATH": "/usr/lib/bin${pathsep}/usr/var/foo"
}
```

or

```
"env": {
  "LIKEPATH": ["/usr/lib/bin", "/usr/var/foo"]
}
```
I like the list approach best :)
Don't mind the list handling being implemented but I'll add `pathsep` template variable in `lsp_utils` for the time being since I want this to work in ST3 also.

https://github.com/sublimelsp/lsp_utils/pull/75

Resolving patch:
diff --git a/plugin/core/types.py b/plugin/core/types.py
--- a/plugin/core/types.py
+++ b/plugin/core/types.py
@@ -553,7 +553,7 @@ def __init__(self,
                  enabled: bool = True,
                  init_options: DottedDict = DottedDict(),
                  settings: DottedDict = DottedDict(),
-                 env: Dict[str, str] = {},
+                 env: Dict[str, Union[str, List[str]]] = {},
                  experimental_capabilities: Optional[Dict[str, Any]] = None,
                  disabled_capabilities: DottedDict = DottedDict(),
                  file_watcher: FileWatcherConfig = {},
@@ -693,6 +693,8 @@ def resolve_transport_config(self, variables: Dict[str, str]) -> TransportConfig
             command = [a.replace('{port}', str(tcp_port)) for a in command]
         env = os.environ.copy()
         for key, value in self.env.items():
+            if isinstance(value, list):
+                value = os.path.pathsep.join(value)
             if key == 'PATH':
                 env[key] = sublime.expand_variables(value, variables) + os.path.pathsep + env[key]
             else:


Test patch:
diff --git a/tests/test_configs.py b/tests/test_configs.py
--- a/tests/test_configs.py
+++ b/tests/test_configs.py
@@ -1,3 +1,4 @@
+import sublime
 from LSP.plugin.core.settings import read_client_config, update_client_config
 from LSP.plugin.core.views import get_uri_and_position_from_location
 from LSP.plugin.core.views import to_encoded_filename
@@ -81,6 +82,23 @@ def test_transport_config_extends_env_path(self):
         resolved_path = transport_config.env['PATH']
         self.assertEqual(resolved_path, '/a/b/{}{}'.format(pathsep, original_path))
 
+    def test_list_in_environment(self):
+        settings = {
+            "command": ["pyls"],
+            "selector": "source.python",
+            "env": {
+                "FOO": ["C:/hello", "X:/there", "Y:/$foobar"],
+                "BAR": "baz"
+            }
+        }
+        config = read_client_config("pyls", settings)
+        resolved = config.resolve_transport_config({"foobar": "asdf"})
+        if sublime.platform() == "windows":
+            self.assertEqual(resolved.env["FOO"], "C:/hello;X:/there;Y:/asdf")
+        else:
+            self.assertEqual(resolved.env["FOO"], "C:/hello:X:/there:Y:/asdf")
+        self.assertEqual(resolved.env["BAR"], "baz")
+
     def test_disabled_capabilities(self):
         settings = {
             "command": ["pyls"],