Repository: sublimelsp/LSP

Issue:
Recursive variable expansion in lsp_execute command
**Is your feature request related to a problem? Please describe.**

The documentation states ...

```js
[
  // ...
  {
    "caption": "Thread First",
    "command": "lsp_execute",
    "args": {
      "command_name": "thread-first",
      "command_args": ["${file_uri}", 0, 0]
    }
  }
]
```

Note: `command_args` is optional depending on the `workspace/executeCommand` that are supported by the LSP server.
The following **variables will be expanded, but only if they are top-level array items** and not within nested arrays or objects:

The **LemMinX** language server provides a validation command which expects [textDocumentIdentifie](https://microsoft.github.io/language-server-protocol/specification#textDocumentIdentifier) as first parameter.

see: https://github.com/eclipse/lemminx/pull/938

The proper command definition in ST would look like

```
[
	{
		"caption": "XML: Validate File",
		"command": "lsp_execute",
		"args": {
			"command_name": "xml.validation.current.file",
			"command_args": [{"uri": "${file_uri}"}]
		}
	}
]
```

Unfortunatelly `${file_uri}` is not expanded as it is not in the top-level array.

**Describe the solution you'd like**

The most flexible and straight forward solution would probably be to support recursive variable expansions in all nested arrays and objects.

**Describe alternatives you've considered**

An `$document_id` variable which is expanded to `{"uri": "file:///path/to/file.xml"}` would do the job as well. The command definition would look as follows then.

```
[
	{
		"caption": "XML: Validate File",
		"command": "lsp_execute",
		"args": {
			"command_name": "xml.validation.current.file",
			"command_args": ["$document_id"]
		}
	}
]
```






Discussion:
The way to replace some selected variables as top-level items implemented in https://github.com/sublimelsp/LSP/blob/bd0dfc23334da392e15f2332bf2de68e112658e2/plugin/execute_command.py#L44-L57 was used because it was just the most simple approach to do it. It would have been nice if we could use `sublime.expand_variables()` here to automatically search through arrays/mappings, but this isn't possible because it only works like a "string interpolation", i.e. all values would have to be strings, while in practice values can also be numbers or other valid JSON values (arrays, objects).

I'm not sure whether it would be worth to recursively search through all arrays/objects here, because I would assume that the overwhelming majority of language servers use some of the predefined structures from LSP if they expect a more complex value than some scalar int/string/boolean. And even if a recursive search would be implemented, it still isn't guaranteed that all possible values can be constructed from the variables we defined. So for this application here, I would suggest to just add another variable for textDocumentIdentifier like the suggested `${document_id}`. The following lines should do it:

```python
from .core.views import text_document_identifier

# [....]

            elif arg in ["$document_id", "${document_id}"]:
                command_args[i] = text_document_identifier(self.view)
```

Perhaps we should name it more similar to the actual name from LSP `${text_document_identifier}`? (And mabye we better should have used the LSP name `${document_uri}` for DocumentUri instead of `${file_uri}`, but it's unfeasible to change it now.)

And if a server requires another kind of LSP structure as value in the future, we can easily add it when necessary.

Resolving patch:
diff --git a/plugin/execute_command.py b/plugin/execute_command.py
--- a/plugin/execute_command.py
+++ b/plugin/execute_command.py
@@ -3,7 +3,7 @@
 from .core.protocol import ExecuteCommandParams
 from .core.registry import LspTextCommand
 from .core.typing import List, Optional, Any
-from .core.views import uri_from_view, offset_to_point, region_to_range
+from .core.views import uri_from_view, offset_to_point, region_to_range, text_document_identifier
 
 
 class LspExecuteCommand(LspTextCommand):
@@ -41,6 +41,8 @@ def handle_response(response: Any) -> None:
     def _expand_variables(self, command_args: List[Any]) -> None:
         region = self.view.sel()[0]
         for i, arg in enumerate(command_args):
+            if arg in ["$document_id", "${document_id}"]:
+                command_args[i] = text_document_identifier(self.view)
             if arg in ["$file_uri", "${file_uri}"]:
                 command_args[i] = uri_from_view(self.view)
             elif arg in ["$selection", "${selection}"]:


Test patch: