Repository: sublimelsp/LSP

Issue:
clangd: duplicate bracket on completion
When clangd offers a completion option, it will include (closing?) brackets as well.
The problem is that if the closing bracket is already in place, the auto-completion will cause a duplicate character.
Is there a way to  tweak/disable this behaviour?

Here are some screenshots:

<img width="341" alt="Screenshot 2020-04-08 at 23 35 47" src="https://user-images.githubusercontent.com/61948614/78836323-25b6b980-79f2-11ea-8a46-ed3351aa4e52.png">
<img width="405" alt="Screenshot 2020-04-08 at 23 35 59" src="https://user-images.githubusercontent.com/61948614/78836292-1d5e7e80-79f2-11ea-9274-ecf1c791f8b6.png">
<img width="218" alt="Screenshot 2020-04-08 at 23 35 35" src="https://user-images.githubusercontent.com/61948614/78836350-323b1200-79f2-11ea-9bbf-e795ffa94085.png">


Discussion:

Resolving patch:
diff --git a/plugin/completion.py b/plugin/completion.py
--- a/plugin/completion.py
+++ b/plugin/completion.py
@@ -4,12 +4,11 @@
 from .core.configurations import is_supported_syntax
 from .core.edit import parse_text_edit
 from .core.logging import debug
-from .core.protocol import Request, Range, InsertTextFormat
+from .core.protocol import Request, InsertTextFormat
 from .core.registry import session_for_view, client_from_session, LSPViewEventListener
 from .core.sessions import Session
 from .core.settings import settings, client_configs
-from .core.typing import Any, List, Dict, Optional, Union
-from .core.views import range_to_region
+from .core.typing import Any, List, Dict, Optional, Union, Iterable, Tuple
 from .core.views import text_document_position_params
 
 
@@ -42,25 +41,8 @@
 }
 
 
-def format_completion(item: dict, change_id: Any) -> sublime.CompletionItem:
-    item_kind = item.get("kind")
-    if item_kind:
-        kind = completion_kinds.get(item_kind, sublime.KIND_AMBIGUOUS)
-    else:
-        kind = sublime.KIND_AMBIGUOUS
-
-    if item.get("deprecated", False):
-        kind = (kind[0], '⚠', "⚠ {} - Deprecated".format(kind[2]))
-
-    item["change_id"] = change_id
-
-    return sublime.CompletionItem.command_completion(
-        trigger=item["label"],
-        command="lsp_select_completion_item",
-        args=item,
-        annotation=item.get('detail') or "",
-        kind=kind
-    )
+_changes_since_sent = []  # type: List[sublime.TextChange]
+_change_id = None  # type: Any
 
 
 class LspSelectCompletionItemCommand(sublime_plugin.TextCommand):
@@ -80,10 +62,10 @@ def run(self, edit: sublime.Edit, **item: Any) -> None:
         text_edit = item.get('textEdit')
         if text_edit:
             new_text = text_edit['newText']
-            # this region was valid a few view.change_count() moments back ...
-            edit_region = range_to_region(Range.from_lsp(text_edit['range']), self.view)
-            # ... but this brings it to the present.
-            edit_region = self.view.transform_region_from(edit_region, item["change_id"])
+            old_region = item.pop("native_region")  # type: Tuple[int, int]
+            # This brings the historic sublime.Region to the present.
+            global _change_id
+            edit_region = self.view.transform_region_from(sublime.Region(old_region[0], old_region[1]), _change_id)
             selection = self.view.sel()
             primary_cursor_position = selection[0].b
             for region in reversed(selection):
@@ -91,6 +73,7 @@ def run(self, edit: sublime.Edit, **item: Any) -> None:
                 # To do that, translate, or offset, the LSP edit region into the non-"primary" regions.
                 # The concept of "primary" is our own, and there is no mention of it in the LSP spec.
                 translation = region.b - primary_cursor_position
+                # We will assume that the user was typing the same characters.
                 self.view.erase(edit, sublime.Region(edit_region.a + translation, edit_region.b + translation))
         else:
             new_text = item.get('insertText') or item['label']
@@ -145,6 +128,7 @@ def __init__(self, view: sublime.View) -> None:
         super().__init__(view)
         self.initialized = False
         self.enabled = False
+        self._request_in_flight = False
 
     @classmethod
     def is_applicable(cls, view_settings: dict) -> bool:
@@ -164,8 +148,7 @@ def initialize(self) -> None:
             # usual query for completions. So the explicit check for None is necessary.
             self.enabled = True
 
-            trigger_chars = completionProvider.get(
-                'triggerCharacters') or []
+            trigger_chars = completionProvider.get('triggerCharacters') or []
             if trigger_chars:
                 self.register_trigger_chars(session, trigger_chars)
             # This is to make ST match with labels that have a weird prefix like a space character.
@@ -181,6 +164,11 @@ def register_trigger_chars(self, session: Session, trigger_chars: List[str]) ->
 
         self.view.settings().set('auto_complete_triggers', completion_triggers)
 
+    def on_text_changed(self, changes: Iterable[sublime.TextChange]) -> None:
+        if self._request_in_flight:
+            global _changes_since_sent
+            _changes_since_sent.extend(changes)
+
     def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[sublime.CompletionList]:
         if not self.initialized:
             self.initialize()
@@ -191,32 +179,94 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su
             return None
         self.manager.documents.purge_changes(self.view)
         completion_list = sublime.CompletionList()
+        self._request_in_flight = True
         client.send_request(
             Request.complete(text_document_position_params(self.view, locations[0])),
-            lambda res: self.handle_response(res, completion_list, self.view.change_id()),
+            lambda res: self.handle_response(res, completion_list),
             lambda res: self.handle_error(res, completion_list))
         return completion_list
 
-    def handle_response(self, response: Optional[Union[dict, List]],
-                        completion_list: sublime.CompletionList, change_id: Any) -> None:
+    def format_completion(self, item: dict) -> sublime.CompletionItem:
+        item_kind = item.get("kind")
+        if item_kind:
+            kind = completion_kinds.get(item_kind, sublime.KIND_AMBIGUOUS)
+        else:
+            kind = sublime.KIND_AMBIGUOUS
+
+        if item.get("deprecated", False):
+            kind = (kind[0], '⚠', "⚠ {} - Deprecated".format(kind[2]))
+
+        text_edit = item.get("textEdit")
+        if text_edit:
+            r = text_edit["range"]
+            start, end = r["start"], r["end"]
+            # From the LSP spec: the range of the edit must be a single line range and it must contain the position at
+            # which completion has been requested.
+            row, start_col_utf16, end_col_utf16 = start["line"], start["character"], end["character"]
+            # The TextEdit from the language server might apply to an old version of the buffer. The user may have
+            # entered more characters in the meantime.
+            global _changes_since_sent
+            for change in _changes_since_sent:
+                # Thus, adjust the range for each buffer change.
+                start_col_utf16, end_col_utf16 = transform_region(start_col_utf16, end_col_utf16, change)
+            # We don't have to use region_to_range. That function clamps the unreliable input, but having unreliable
+            # (row, col) points for completions is pretty much disastrous. So we'll assume the (row, col) points are
+            # correct. Blame the language server if the regions are incorrect.
+            convert = self.view.text_point_utf16
+            item["native_region"] = (convert(row, start_col_utf16), convert(row, end_col_utf16))
+
+        return sublime.CompletionItem.command_completion(
+            trigger=item["label"],
+            command="lsp_select_completion_item",
+            args=item,
+            annotation=item.get('detail') or "",
+            kind=kind
+        )
+
+    def handle_response(self, response: Optional[Union[dict, List]], completion_list: sublime.CompletionList) -> None:
+        self._request_in_flight = False
         response_items = []  # type: List[Dict]
-        incomplete = False
+        flags = 0
+        if settings.only_show_lsp_completions:
+            flags |= sublime.INHIBIT_WORD_COMPLETIONS
+            flags |= sublime.INHIBIT_EXPLICIT_COMPLETIONS
         if isinstance(response, dict):
             response_items = response["items"] or []
-            incomplete = response.get("isIncomplete", False)
+            if response.get("isIncomplete", False):
+                flags |= sublime.DYNAMIC_COMPLETIONS
         elif isinstance(response, list):
             response_items = response
+        global _change_id
+        _change_id = self.view.change_id()
         response_items = sorted(response_items, key=lambda item: item.get("sortText") or item["label"])
-
-        flags = 0
-        if settings.only_show_lsp_completions:
-            flags |= sublime.INHIBIT_WORD_COMPLETIONS
-            flags |= sublime.INHIBIT_EXPLICIT_COMPLETIONS
-
-        if incomplete:
-            flags |= sublime.DYNAMIC_COMPLETIONS
-        resolve(completion_list, [format_completion(i, change_id) for i in response_items], flags)
+        items = list(map(self.format_completion, response_items))
+        _changes_since_sent.clear()
+        resolve(completion_list, items, flags)
 
     def handle_error(self, error: dict, completion_list: sublime.CompletionList) -> None:
+        self._request_in_flight = False
+        _changes_since_sent.clear()
         resolve(completion_list, [])
         sublime.status_message('Completion error: ' + str(error.get('message')))
+
+
+def transform_region(col_a: int, col_b: int, change: sublime.TextChange) -> Tuple[int, int]:
+    """
+    Here be dragons. Given an LSP region, and a change to the text buffer, transform the region into the coordinate
+    space after that change has been applied. Note that this mirrors the algorithm that View.transform_region_from uses
+    internally.
+
+    Returns the adjusted col_a and col_b.
+    """
+    a = change.a.col_utf16
+    b = change.b.col_utf16
+    length_utf16 = len(change.str.encode("UTF-16")) // 2
+    if a <= col_a <= b:
+        col_a = a
+    elif a > col_a:
+        col_a += length_utf16 + b - a
+    if a <= col_b < b:
+        col_b = a
+    elif a >= col_b:
+        col_a += length_utf16 + b - a
+    return col_a, col_b


Test patch:
diff --git a/tests/test_completion.py b/tests/test_completion.py
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -1,6 +1,6 @@
 from LSP.plugin.completion import CompletionHandler
 from LSP.plugin.core.registry import is_supported_view
-from LSP.plugin.core.typing import Any, Generator, List, Dict
+from LSP.plugin.core.typing import Any, Generator, List, Dict, Callable
 from setup import CI, SUPPORTED_SYNTAX, TextDocumentTestCase, add_config, remove_config, text_config
 from unittesting import DeferrableTestCase
 import sublime
@@ -77,21 +77,25 @@ def move_cursor(self, row: int, col: int) -> None:
         s.clear()
         s.add(point)
 
-    def select_completion(self) -> 'Generator':
-        current_change_count = self.view.change_count()
-        self.view.run_command('auto_complete')
+    def create_commit_completion_closure(self) -> Callable[[], bool]:
         committed = False
+        current_change_count = self.view.change_count()
 
         def commit_completion() -> bool:
             if not self.view.is_auto_complete_visible():
                 return False
             nonlocal committed
+            nonlocal current_change_count
             if not committed:
                 self.view.run_command("commit_completion")
                 committed = True
             return self.view.change_count() > current_change_count
 
-        yield commit_completion
+        return commit_completion
+
+    def select_completion(self) -> 'Generator':
+        self.view.run_command('auto_complete')
+        yield self.create_commit_completion_closure()
 
     def read_file(self) -> str:
         return self.view.substr(sublime.Region(0, self.view.size()))
@@ -484,3 +488,79 @@ def test_multi_cursor_snippet_text_edit(self) -> 'Generator':
                 'range': {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 2}}
             }
         })
+
+    def test_nontrivial_text_edit_removal(self) -> 'Generator':
+        self.type('#include <u>')
+        self.move_cursor(0, 11)  # Put the cursor inbetween 'u' and '>'
+        self.set_response("textDocument/completion", [{
+            'filterText': 'uchar.h>',
+            'label': ' uchar.h>',
+            'textEdit': {
+                # This range should remove "u>" and then insert "uchar.h>"
+                'range': {'start': {'line': 0, 'character': 10}, 'end': {'line': 0, 'character': 12}},
+                'newText': 'uchar.h>'
+            },
+            'insertText': 'uchar.h>',
+            'kind': 17,
+            'insertTextFormat': 2
+        }])
+        yield from self.select_completion()
+        yield from self.await_message("textDocument/completion")
+        self.assertEqual(self.read_file(), '#include <uchar.h>')
+
+    def test_nontrivial_text_edit_removal_with_buffer_modifications_clangd(self) -> 'Generator':
+        self.type('#include <u>')
+        self.move_cursor(0, 11)  # Put the cursor inbetween 'u' and '>'
+        self.set_response("textDocument/completion", [{
+            'filterText': 'uchar.h>',
+            'label': ' uchar.h>',
+            'textEdit': {
+                # This range should remove "u>" and then insert "uchar.h>"
+                'range': {'start': {'line': 0, 'character': 10}, 'end': {'line': 0, 'character': 12}},
+                'newText': 'uchar.h>'
+            },
+            'insertText': 'uchar.h>',
+            'kind': 17,
+            'insertTextFormat': 2
+        }])
+        self.view.run_command('auto_complete')  # show the AC widget
+        yield from self.await_message("textDocument/completion")
+        yield 100
+        self.view.run_command('insert', {'characters': 'c'})  # type characters
+        yield 100
+        self.view.run_command('insert', {'characters': 'h'})  # while the AC widget
+        yield 100
+        self.view.run_command('insert', {'characters': 'a'})  # is visible
+        yield 100
+        # Commit the completion. The buffer has been modified in the meantime, so the old text edit that says to
+        # remove "u>" is invalid. The code in completion.py must be able to handle this.
+        yield self.create_commit_completion_closure()
+        self.assertEqual(self.read_file(), '#include <uchar.h>')
+
+    def test_nontrivial_text_edit_removal_with_buffer_modifications_json(self) -> 'Generator':
+        self.type('{"k"}')
+        self.move_cursor(0, 3)  # Put the cursor inbetween 'k' and '"'
+        self.set_response("textDocument/completion", [{
+            'kind': 10,
+            'documentation': 'Array of single or multiple keys',
+            'insertTextFormat': 2,
+            'label': 'keys',
+            'textEdit': {
+                # This range should remove '"k"' and then insert '"keys": []'
+                'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 4}},
+                'newText': '"keys": [$1]'
+            },
+            "filterText": '"keys"',
+            "insertText": 'keys": [$1]'
+        }])
+        self.view.run_command('auto_complete')  # show the AC widget
+        yield from self.await_message("textDocument/completion")
+        yield 100
+        self.view.run_command('insert', {'characters': 'e'})  # type characters
+        yield 100
+        self.view.run_command('insert', {'characters': 'y'})  # while the AC widget is open
+        yield 100
+        # Commit the completion. The buffer has been modified in the meantime, so the old text edit that says to
+        # remove '"k"' is invalid. The code in completion.py must be able to handle this.
+        yield self.create_commit_completion_closure()
+        self.assertEqual(self.read_file(), '{"keys": []}')