Skip to content

Commit

Permalink
Fix inconsistency in indent adjustment behavior when completing
Browse files Browse the repository at this point in the history
There is a small inconsistency when we're dealing with a text edit and the text
edit is not a snippet. In that case, if the new text of the completion
contained newlines, the subsequent line would not be "auto-indented". Whereas
with the three other possible cases, it would be auto-indented. That is because
the other three cases use the "insert" and "insert_snippet" commands, whereas
with this special case we were using the view.insert method.
  • Loading branch information
rwols committed Aug 18, 2021
1 parent 5798706 commit b12404d
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 41 deletions.
66 changes: 29 additions & 37 deletions plugin/completion.py
Expand Up @@ -9,6 +9,7 @@
from .core.views import range_to_region
from .core.views import show_lsp_popup
from .core.views import update_lsp_popup
import functools

SessionName = str

Expand Down Expand Up @@ -72,49 +73,29 @@ def run(self, edit: sublime.Edit, item: CompletionItem, session_name: str) -> No
if text_edit:
new_text = text_edit["newText"].replace("\r", "")
edit_region = range_to_region(Range.from_lsp(text_edit['range']), self.view)
if item.get("insertTextFormat", InsertTextFormat.PlainText) == InsertTextFormat.Snippet:
for region in self.translated_regions(edit_region):
self.view.erase(edit, region)
self.view.run_command("insert_snippet", {"contents": new_text})
else:
for region in self.translated_regions(edit_region):
# NOTE: Cannot do .replace, because ST will select the replacement.
self.view.erase(edit, region)
self.view.insert(edit, region.a, new_text)
for region in self._translated_regions(edit_region):
self.view.erase(edit, region)
else:
insert_text = item.get("insertText") or item["label"]
insert_text = insert_text.replace("\r", "")
if item.get("insertTextFormat", InsertTextFormat.PlainText) == InsertTextFormat.Snippet:
self.view.run_command("insert_snippet", {"contents": insert_text})
else:
self.view.run_command("insert", {"characters": insert_text})
self.epilogue(item, session_name)

def translated_regions(self, edit_region: sublime.Region) -> Generator[sublime.Region, None, None]:
selection = self.view.sel()
primary_cursor_position = selection[0].b
for region in reversed(selection):
# For each selection region, apply the same removal as for the "primary" region.
# 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
translated_edit_region = sublime.Region(edit_region.a + translation, edit_region.b + translation)
yield translated_edit_region

def epilogue(self, item: CompletionItem, session_name: str) -> None:
new_text = item.get("insertText") or item["label"]
new_text = new_text.replace("\r", "")
if item.get("insertTextFormat", InsertTextFormat.PlainText) == InsertTextFormat.Snippet:
self.view.run_command("insert_snippet", {"contents": new_text})
else:
self.view.run_command("insert", {"characters": new_text})
# todo: this should all run from the worker thread
session = self.session_by_name(session_name, 'completionProvider.resolveProvider')

def resolve_on_main_thread(item: CompletionItem, session_name: str) -> None:
sublime.set_timeout(lambda: self.on_resolved(item, session_name))

additional_text_edits = item.get('additionalTextEdits')
if session and not additional_text_edits:
request = Request.resolveCompletionItem(item, self.view)
session.send_request_async(request, lambda response: resolve_on_main_thread(response, session_name))
session.send_request_async(
Request.resolveCompletionItem(item, self.view),
functools.partial(self._on_resolved_async, session_name))
else:
self.on_resolved(item, session_name)
self._on_resolved(session_name, item)

def on_resolved(self, item: CompletionItem, session_name: str) -> None:
def _on_resolved_async(self, session_name: str, item: CompletionItem) -> None:
sublime.set_timeout(functools.partial(self._on_resolved, session_name, item))

def _on_resolved(self, session_name: str, item: CompletionItem) -> None:
additional_edits = item.get('additionalTextEdits')
if additional_edits:
edits = [parse_text_edit(additional_edit) for additional_edit in additional_edits]
Expand All @@ -128,3 +109,14 @@ def on_resolved(self, item: CompletionItem, session_name: str) -> None:
"session_name": session_name
}
self.view.run_command("lsp_execute", args)

def _translated_regions(self, edit_region: sublime.Region) -> Generator[sublime.Region, None, None]:
selection = self.view.sel()
primary_cursor_position = selection[0].b
for region in reversed(selection):
# For each selection region, apply the same removal as for the "primary" region.
# 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
translated_edit_region = sublime.Region(edit_region.a + translation, edit_region.b + translation)
yield translated_edit_region
25 changes: 21 additions & 4 deletions tests/test_completion.py
Expand Up @@ -2,6 +2,7 @@
from LSP.plugin.core.protocol import CompletionItem
from LSP.plugin.core.protocol import CompletionItemLabelDetails
from LSP.plugin.core.protocol import CompletionItemTag
from LSP.plugin.core.protocol import InsertTextFormat
from LSP.plugin.core.typing import Any, Generator, List, Dict, Callable, Optional
from LSP.plugin.core.views import format_completion
from setup import TextDocumentTestCase
Expand Down Expand Up @@ -212,7 +213,7 @@ def test_space_added_in_label(self) -> 'Generator':
}
}
},
"insertTextFormat": 2,
"insertTextFormat": InsertTextFormat.Snippet,
"insertText": "const",
"filterText": "const",
"score": 6
Expand Down Expand Up @@ -244,7 +245,7 @@ def test_dash_missing_from_label(self) -> 'Generator':
"additionalTextEdits": None,
"data": None,
"range": None,
"insertTextFormat": 1,
"insertTextFormat": InsertTextFormat.PlainText,
"sortText": "0001UniqueId",
"kind": 6,
"detail": "[string[]]"
Expand Down Expand Up @@ -309,7 +310,7 @@ def test_edit_after_nonword(self) -> 'Generator':
"label": "apply[A](xs: A*): List[A]",
"sortText": "00000",
"preselect": True,
"insertTextFormat": 2,
"insertTextFormat": InsertTextFormat.Snippet,
"filterText": "apply",
"data": {
"symbol": "scala/collection/immutable/List.apply().",
Expand Down Expand Up @@ -342,7 +343,7 @@ def test_filter_text_is_not_a_prefix_of_label(self) -> 'Generator':
"kind": 12,
"sortText": "00002",
"filterText": "e",
"insertTextFormat": 2,
"insertTextFormat": InsertTextFormat.Snippet,
"textEdit": {
"range": {
"start": {"line": 0, "character": 0},
Expand Down Expand Up @@ -588,6 +589,22 @@ def test_nontrivial_text_edit_removal_with_buffer_modifications_json(self) -> 'G
yield self.create_commit_completion_closure()
self.assertEqual(self.read_file(), '{"keys": []}')

def test_text_edit_plaintext_with_multiple_lines_indented(self) -> Generator[None, None, None]:
self.type("\t\n\t")
self.move_cursor(1, 2)
self.set_response("textDocument/completion", [{
'label': 'a',
'textEdit': {
'range': {'start': {'line': 1, 'character': 4}, 'end': {'line': 1, 'character': 4}},
'newText': 'a\n\tb'
},
'insertTextFormat': InsertTextFormat.PlainText
}])
yield from self.select_completion()
yield from self.await_message("textDocument/completion")
# the "b" should be intended one level deeper
self.assertEqual(self.read_file(), '\t\n\ta\n\t\tb')

def test_show_deprecated_flag(self) -> None:
item_with_deprecated_flag = {
"label": 'hello',
Expand Down

0 comments on commit b12404d

Please sign in to comment.