Skip to content

Commit

Permalink
Use same value for trigger and content, add flag for textEdit
Browse files Browse the repository at this point in the history
Mismatching trigger and content results in lost/extra chars.
Fix for #368
  • Loading branch information
tomv564 committed Aug 12, 2018
1 parent 24ea0ce commit 7b107f7
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 18 deletions.
32 changes: 17 additions & 15 deletions plugin/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,25 +274,27 @@ def format_completion(self, item: dict) -> 'Tuple[str, str]':
hint = completion_item_kind_names.get(kind)
# label is an alternative for insertText if neither textEdit nor insertText is provided
insert_text = self.text_edit_text(item) or item.get("insertText") or label
trigger = insert_text
if len(insert_text) > 0 and insert_text[0] == '$': # sublime needs leading '$' escaped.
insert_text = '\\$' + insert_text[1:]
# only return label with a hint if available
return "\t ".join((label, hint)) if hint else label, insert_text
# only return trigger with a hint if available
return "\t ".join((trigger, hint)) if hint else trigger, insert_text

def text_edit_text(self, item) -> 'Optional[str]':
# try to handle textEdit if present
text_edit = item.get("textEdit")
if text_edit:
edit_range, edit_text = text_edit.get("range"), text_edit.get("newText")
if edit_range and edit_text:
edit_range = Range.from_lsp(edit_range)
last_start = self.last_location - len(self.last_prefix)
last_row, last_col = self.view.rowcol(last_start)
if last_row == edit_range.start.row == edit_range.end.row and edit_range.start.col <= last_col:
# sublime does not support explicit replacement with completion
# at given range, but we try to trim the textEdit range and text
# to the start location of the completion
return edit_text[last_col - edit_range.start.col:]
if settings.complete_using_text_edit:
# try to handle textEdit if present
text_edit = item.get("textEdit")
if text_edit:
edit_range, edit_text = text_edit.get("range"), text_edit.get("newText")
if edit_range and edit_text:
edit_range = Range.from_lsp(edit_range)
last_start = self.last_location - len(self.last_prefix)
last_row, last_col = self.view.rowcol(last_start)
if last_row == edit_range.start.row == edit_range.end.row and edit_range.start.col <= last_col:
# sublime does not support explicit replacement with completion
# at given range, but we try to trim the textEdit range and text
# to the start location of the completion
return edit_text[last_col - edit_range.start.col:]
return None

def handle_response(self, response: 'Optional[Dict]'):
Expand Down
1 change: 1 addition & 0 deletions plugin/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def update_settings(settings: Settings, settings_obj: sublime.Settings):
settings.only_show_lsp_completions = read_bool_setting(settings_obj, "only_show_lsp_completions", False)
settings.complete_all_chars = read_bool_setting(settings_obj, "complete_all_chars", True)
settings.completion_hint_type = read_str_setting(settings_obj, "completion_hint_type", "auto")
settings.complete_using_text_edit = read_bool_setting(settings_obj, "complete_using_text_edit", False)
settings.resolve_completion_for_snippets = read_bool_setting(settings_obj, "resolve_completion_for_snippets", False)
settings.log_debug = read_bool_setting(settings_obj, "log_debug", False)
settings.log_server = read_bool_setting(settings_obj, "log_server", True)
Expand Down
1 change: 1 addition & 0 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(self):
self.diagnostics_gutter_marker = "dot"
self.complete_all_chars = False
self.completion_hint_type = "auto"
self.complete_using_text_edit = False
self.resolve_completion_for_snippets = False
self.log_debug = True
self.log_server = True
Expand Down
87 changes: 84 additions & 3 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import unittest
from unittesting import DeferrableTestCase
from unittest.mock import MagicMock
import sublime
from LSP.plugin.completion import CompletionHandler, CompletionState
from LSP.plugin.core.settings import client_configs, ClientConfig
from os.path import dirname


def create_completion_item(item: str):
def create_completion_item(item: str, insert_text: 'Optional[str]'=None) -> dict:
return {
"label": item
"label": item,
"insertText": insert_text
}


Expand Down Expand Up @@ -38,7 +40,7 @@ def get_capability(self, capability_name: str):


@unittest.skip('asd')
class InitializationTests(unittest.TestCase):
class InitializationTests(DeferrableTestCase):

def setUp(self):
self.view = sublime.active_window().new_file()
Expand Down Expand Up @@ -114,3 +116,82 @@ def tearDown(self):
client_configs.all = self.old_configs
if self.view:
self.view.window().run_command("close_file")


class CompletionFormattingTests(DeferrableTestCase):

def setUp(self):
self.view = sublime.active_window().open_file(test_file_path)

def test_only_label_item(self):
handler = CompletionHandler(self.view)
result = handler.format_completion(create_completion_item("asdf"))
self.assertEqual(len(result), 2)
self.assertEqual("asdf", result[0])
self.assertEqual("asdf", result[1])

def test_prefers_insert_text(self):
handler = CompletionHandler(self.view)
result = handler.format_completion(create_completion_item("asdf", "Asdf"))
self.assertEqual(len(result), 2)
self.assertEqual("Asdf", result[0])
self.assertEqual("Asdf", result[1])

def test_ignores_text_edit(self):
handler = CompletionHandler(self.view)

# partial completion from cursor (instead of full word) causes issues.
item = {
'insertText': '$true',
'label': 'true',
'textEdit': {
'newText': 'rue',
'range': {
'start': {'line': 0, 'character': 2},
'end': {'line': 0, 'character': 2}
}
}
}

result = handler.format_completion(item)
self.assertEqual(len(result), 2)
self.assertEqual("$true", result[0])
self.assertEqual("\\$true", result[1])

def test_item_with_partial_edit(self):
# issue #368, where using textEdit breaks PHP completions:
# $|
# PHP language server returns a partial completion "what"
# sublime uses it to replace '$' with 'what'
# to test:
# settings.complete_using_text_edit = True
yield 100 # wait for file to be open

item = {
'insertText': None,
# 'sortText': None,
# 'filterText': None,
'label': '$what',
'textEdit': {
'range': {
'end': {'character': 1, 'line': 0},
'start': {'character': 1, 'line': 0}
},
'newText': 'what'
}
}

handler = CompletionHandler(self.view)
handler.last_location = 1
handler.last_prefix = ""
result = handler.format_completion(item)
self.assertEqual(len(result), 2)
self.assertEqual("$what", result[0])
self.assertEqual("\\$what", result[1])
# settings.complete_using_text_edit = False

def tearDown(self):
if self.view:
self.view.set_scratch(True)
self.view.window().focus_view(self.view)
self.view.window().run_command("close_file")

0 comments on commit 7b107f7

Please sign in to comment.