Repository: sublimelsp/LSP

Issue:
"Rename…" code action is broken
(v1.1.6) The _Rename_ code action doesn’t work correctly. I wasn’t able to use it in VSCode so I can’t tell whether it comes from the language server of the Sublime extension.

Here is a minimal example:

```elm
module Test exposing (..)

import Html exposing (Html)


view : Html msg
view =
    Html.text body


body : String
body =
    "..."
```

When renaming `body` for instance:

* if the new name has the same length, it seems to work fine
* if the new name is longer (`bodyxyz` in the example below), the last few characters are duplicated:
```elm

view : Html msg
view =
    Html.text bodyxyzxyz


bodyxyzxyz : String
bodyxyzxyz =
    "..."
```
* if the new name is shorter (`a` in this example), well…
```elm
view : Html msg
view =
    Html.text aaString
a    "..."
```


Discussion:
On first glance the server returns the correct text and range. So it seems like a LSP plugin bug (the text edit is not applied correctly)

I will need to investigate more

Also it works correctly in VSCode. It’s worth noting that the Rename action seems to work ok in ST with the other LSP plugin that I use, which is Intelephense (PHP).
given a document:
when I try to preform a rename for `filename` -> `filenamed`
```
37|image : String -> Image
38|image filename =
      Image ("/assets/images/" ++ filename)
         ^6     ^14                ^32    ^40     
```

here is the rename response and for some reason 4 didChange(expected 2) `contentChanges` are send:
```
:: <<< LSP-elm 6: {
    'documentChanges': [{'edits': [
        {'range': {'end': {'character': 14, 'line': 37}, 'start': {'character': 6, 'line': 37}}, 'newText': 'filenamed'}, 
        {'range': {'end': {'character': 40, 'line': 38}, 'start': {'character': 32, 'line': 38}}, 'newText': 'filenamed'}
    ], 
    'textDocument': {'uri': 'file:///home/predrag/Documents/sandbox/elm-spa-example/src/Asset.elm', 'version': None}}], 
    'changes': {'file:///home/predrag/Documents/sandbox/elm-spa-example/src/Asset.elm': [
        {'range': {'end': {'character': 14, 'line': 37}, 'start': {'character': 6, 'line': 37}}, 'newText': 'filenamed'}, 
        {'range': {'end': {'character': 40, 'line': 38}, 'start': {'character': 32, 'line': 38}}, 'newText': 'filenamed'}]}}
::  -> LSP-elm textDocument/didChange: {'textDocument': {'uri': 'file:///home/predrag/Documents/sandbox/elm-spa-example/src/Asset.elm', 'version': 16}, 'contentChanges': [
    {'text': 'filenamed', 'range': {'end': {'character': 40, 'line': 38}, 'start': {'character': 32, 'line': 38}}, 'rangeLength': 8}, 
    {'text': 'filenamed', 'range': {'end': {'character': 40, 'line': 38}, 'start': {'character': 32, 'line': 38}}, 'rangeLength': 8}, 
    {'text': 'filenamed', 'range': {'end': {'character': 14, 'line': 37}, 'start': {'character': 6, 'line': 37}}, 'rangeLength': 8}, 
    {'text': 'filenamed', 'range': {'end': {'character': 14, 'line': 37}, 'start': {'character': 6, 'line': 37}}, 'rangeLength': 8}
    ]
}

```

here is a comparison with a server where the rename command works:
rename a -> ad:
```
1|def helo():
2|    ad = 21
3|    print(ad)
```

here is the rename response and 2 didChange(expected 2) `contentChanges` are send:
```
:: --> LSP-pyright textDocument/rename(10): {'workDoneToken': 'wd10', 'newName': 'ad', 'position': {'line': 1, 'character': 5}, 'textDocument': {'uri': 'file:///home/predrag/.config/sublime-text/Packages/LSP/example.py'}}
:: <<< LSP-pyright 10: {'changes': {'file:///home/predrag/.config/sublime-text/Packages/LSP/example.py': [
    {'range': {'start': {'line': 1, 'character': 4}, 'end': {'line': 1, 'character': 5}}, 'newText': 'ad'}, 
    {'range': {'start': {'line': 2, 'character': 10}, 'end': {'line': 2, 'character': 11}}, 'newText': 'ad'}]}}
::  -> LSP-pyright textDocument/didChange: {'contentChanges': [
{'range': {'start': {'line': 2, 'character': 10}, 'end': {'line': 2, 'character': 11}}, 'rangeLength': 1, 'text': 'ad'}, 
{'range': {'start': {'line': 1, 'character': 4}, 'end': {'line': 1, 'character': 5}}, 'rangeLength': 1, 'text': 'ad'}
], 'textDocument': {'version': 6, 'uri': 'file:///home/predrag/.config/sublime-text/Packages/LSP/example.py'}}
```

Will continue looking at the bug :)
The server sends edits both as `changes` and `documentChanges`. I guess we are applying both while we should only pick one (`documentChanges`).

> If the client can handle versioned document edits and if documentChanges are present, the latter are preferred over changes.
https://microsoft.github.io/language-server-protocol/specification#workspaceEdit

Resolving patch:
diff --git a/plugin/core/edit.py b/plugin/core/edit.py
--- a/plugin/core/edit.py
+++ b/plugin/core/edit.py
@@ -14,10 +14,6 @@
 
 def parse_workspace_edit(workspace_edit: Dict[str, Any]) -> Dict[str, List[TextEdit]]:
     changes = {}  # type: Dict[str, List[TextEdit]]
-    raw_changes = workspace_edit.get('changes')
-    if isinstance(raw_changes, dict):
-        for uri, file_changes in raw_changes.items():
-            changes[uri_to_filename(uri)] = list(parse_text_edit(change) for change in file_changes)
     document_changes = workspace_edit.get('documentChanges')
     if isinstance(document_changes, list):
         for document_change in document_changes:
@@ -28,6 +24,11 @@ def parse_workspace_edit(workspace_edit: Dict[str, Any]) -> Dict[str, List[TextE
             version = document_change.get('textDocument').get('version')
             text_edit = list(parse_text_edit(change, version) for change in document_change.get('edits'))
             changes.setdefault(uri_to_filename(uri), []).extend(text_edit)
+    else:
+        raw_changes = workspace_edit.get('changes')
+        if isinstance(raw_changes, dict):
+            for uri, file_changes in raw_changes.items():
+                changes[uri_to_filename(uri)] = list(parse_text_edit(change) for change in file_changes)
     return changes
 
 


Test patch:
diff --git a/tests/test_edit.py b/tests/test_edit.py
--- a/tests/test_edit.py
+++ b/tests/test_edit.py
@@ -141,6 +141,20 @@
     ]
 }
 
+LSP_EDIT_DOCUMENT_CHANGES_4 = {
+    'changes': {
+        "file:///asdf/foo/bar": [
+            {"newText": "hello there", "range": LSP_RANGE},
+            {"newText": "general", "range": LSP_RANGE},
+            {"newText": "kenobi", "range": LSP_RANGE}
+        ]
+    },
+    'documentChanges': [{
+        'textDocument': {'uri': URI},
+        'edits': [LSP_TEXT_EDIT]
+    }]
+}
+
 
 class TextEditTests(unittest.TestCase):
 
@@ -185,6 +199,12 @@ def test_no_clobbering_of_previous_edits(self):
         self.assertEqual(len(edit), 1)
         self.assertEqual(len(edit[FILENAME]), 5)
 
+    def test_prefers_document_edits_over_changes(self):
+        edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_4)
+        self.assertIn(FILENAME, edit)
+        self.assertEqual(len(edit), 1)
+        self.assertEqual(len(edit[FILENAME]), 1)  # not 3
+
 
 class SortByApplicationOrderTests(unittest.TestCase):