From 2a13eb58baf050a4abc5811db735097feda6dbd9 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 4 Jan 2020 12:46:09 +0100 Subject: [PATCH 01/62] Don't trigger AC on word_separators --- plugin/completion.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index a4f95f5f4..ea33f9b86 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -49,6 +49,7 @@ def __init__(self, view: sublime.View) -> None: self.initialized = False self.enabled = False self.trigger_chars = [] # type: List[str] + self.ignored_trigger_chars = [] # type: List[str] self.auto_complete_selector = view.settings().get("auto_complete_selector", "") or "" # type: str self.resolve = False self.state = CompletionState.IDLE @@ -80,6 +81,8 @@ def initialize(self) -> None: self.resolve = completionProvider.get('resolveProvider') or False self.trigger_chars = completionProvider.get( 'triggerCharacters') or [] + word_separators = self.view.settings().get("word_separators") + self.ignored_trigger_chars = [char for char in word_separators if char not in self.trigger_chars] if self.trigger_chars: self.register_trigger_chars(session) @@ -107,13 +110,6 @@ def register_trigger_chars(self, session: Session) -> None: self.view.settings().set('auto_complete_triggers', completion_triggers) - def is_after_trigger_character(self, location: int) -> bool: - if location > 0: - prev_char = self.view.substr(location - 1) - return prev_char in self.trigger_chars - else: - return False - def is_same_completion(self, prefix: str, locations: 'List[int]') -> bool: if self.response_incomplete: return False @@ -260,7 +256,10 @@ def do_request(self, prefix: str, locations: 'List[int]') -> None: if not client: return - if settings.complete_all_chars or self.is_after_trigger_character(locations[0]): + prev_point = locations[0] - 1 if locations[0] - 1 >= 0 else 0 + prev_char = view.substr(prev_point) + + if prev_char not in self.ignored_trigger_chars or prev_char in self.trigger_chars: self.manager.documents.purge_changes(self.view) document_position = get_document_position(view, locations[0]) if document_position: From 7cff50379a41e8e8055c426e8601a82a4c110fc2 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 4 Jan 2020 13:05:49 +0100 Subject: [PATCH 02/62] Add back complete_all_chars --- plugin/completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/completion.py b/plugin/completion.py index ea33f9b86..a09718626 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -259,7 +259,7 @@ def do_request(self, prefix: str, locations: 'List[int]') -> None: prev_point = locations[0] - 1 if locations[0] - 1 >= 0 else 0 prev_char = view.substr(prev_point) - if prev_char not in self.ignored_trigger_chars or prev_char in self.trigger_chars: + if settings.complete_all_chars or prev_char not in self.ignored_trigger_chars or prev_char in self.trigger_chars: self.manager.documents.purge_changes(self.view) document_position = get_document_position(view, locations[0]) if document_position: From 5dd6977141bbb32963437035797ea75801a2d00e Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 4 Jan 2020 13:13:56 +0100 Subject: [PATCH 03/62] fix pycodestyle error --- plugin/completion.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin/completion.py b/plugin/completion.py index a09718626..47088d636 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -259,7 +259,11 @@ def do_request(self, prefix: str, locations: 'List[int]') -> None: prev_point = locations[0] - 1 if locations[0] - 1 >= 0 else 0 prev_char = view.substr(prev_point) - if settings.complete_all_chars or prev_char not in self.ignored_trigger_chars or prev_char in self.trigger_chars: + if ( + settings.complete_all_chars or + prev_char in self.trigger_chars or + prev_char not in self.ignored_trigger_chars + ): self.manager.documents.purge_changes(self.view) document_position = get_document_position(view, locations[0]) if document_position: From 0a15bf68f7fbcda04b907847245990cbb7010b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Wed, 8 Jan 2020 23:22:22 +0100 Subject: [PATCH 04/62] Update plugin/completion.py Co-Authored-By: Raoul Wols --- plugin/completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/completion.py b/plugin/completion.py index 47088d636..045a2cce6 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -263,7 +263,7 @@ def do_request(self, prefix: str, locations: 'List[int]') -> None: settings.complete_all_chars or prev_char in self.trigger_chars or prev_char not in self.ignored_trigger_chars - ): + if prev_char in self.trigger_chars or (settings.complete_all_chars and prev_char not in self.view.settings().get("word_separators")): self.manager.documents.purge_changes(self.view) document_position = get_document_position(view, locations[0]) if document_position: From 3ea2e42622d366ad806e3f0c4d7973ab3e873d7a Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 9 Jan 2020 00:11:21 +0100 Subject: [PATCH 05/62] Remove ignored_trigger_chars --- plugin/completion.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 045a2cce6..8c80e1588 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -49,7 +49,6 @@ def __init__(self, view: sublime.View) -> None: self.initialized = False self.enabled = False self.trigger_chars = [] # type: List[str] - self.ignored_trigger_chars = [] # type: List[str] self.auto_complete_selector = view.settings().get("auto_complete_selector", "") or "" # type: str self.resolve = False self.state = CompletionState.IDLE @@ -81,8 +80,6 @@ def initialize(self) -> None: self.resolve = completionProvider.get('resolveProvider') or False self.trigger_chars = completionProvider.get( 'triggerCharacters') or [] - word_separators = self.view.settings().get("word_separators") - self.ignored_trigger_chars = [char for char in word_separators if char not in self.trigger_chars] if self.trigger_chars: self.register_trigger_chars(session) @@ -256,14 +253,11 @@ def do_request(self, prefix: str, locations: 'List[int]') -> None: if not client: return - prev_point = locations[0] - 1 if locations[0] - 1 >= 0 else 0 + prev_point = max(0, locations[0] - 1) prev_char = view.substr(prev_point) + is_word_separator = prev_char in self.view.settings().get("word_separators") - if ( - settings.complete_all_chars or - prev_char in self.trigger_chars or - prev_char not in self.ignored_trigger_chars - if prev_char in self.trigger_chars or (settings.complete_all_chars and prev_char not in self.view.settings().get("word_separators")): + if prev_char in self.trigger_chars or (settings.complete_all_chars and not is_word_separator): self.manager.documents.purge_changes(self.view) document_position = get_document_position(view, locations[0]) if document_position: From 0b58895296e2f59df9d91f4f065645f6c228e5a0 Mon Sep 17 00:00:00 2001 From: Predrag Nikolic Date: Mon, 13 Jan 2020 09:38:14 +0100 Subject: [PATCH 06/62] Integrate new completion api --- plugin/completion.py | 299 +++++++++++--------------------------- plugin/core/completion.py | 112 ++++++++------ plugin/core/protocol.py | 5 + plugin/core/sessions.py | 3 +- stubs/sublime.pyi | 10 ++ tests/test_completion.py | 2 - 6 files changed, 166 insertions(+), 265 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 8c80e1588..e2ebf493c 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -7,7 +7,7 @@ except ImportError: pass -from .core.protocol import Request +from .core.protocol import Request, Range from .core.settings import settings, client_configs from .core.logging import debug from .core.completion import parse_completion_response, format_completion @@ -16,16 +16,52 @@ from .core.documents import get_document_position, position_is_word from .core.sessions import Session from .core.edit import parse_text_edit +from .core.views import range_to_region -class CompletionState(object): - IDLE = 0 - REQUESTING = 1 - APPLYING = 2 - CANCELLING = 3 +last_text_command = None -last_text_command = None +class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): + def run(self, edit: 'Any', item) -> None: + + textEdit = item.get('textEdit') + if textEdit: + range = Range.from_lsp(textEdit['range']) + region = range_to_region(range, self.view) + new_text = text_edit.get('newText') + self.view.replace(edit, region, new_text) + else: + completion = item.get('insertText') or item.get('label') + current_point = self.view.sel()[0].begin() + self.view.insert(edit, current_point, completion) + + # import statements, etc. some servers only return these after a resolve. + additional_edits = item.get('additionalTextEdits') + if additional_edits: + self.apply_additional_edits(additional_edits) + # elif self.resolve: + elif True: + self.do_resolve(item) + + def do_resolve(self, item: dict) -> None: + client = client_from_session(session_for_view(self.view, 'completionProvider', self.view.sel()[0].begin())) + if not client: + return + + client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response) + + def handle_resolve_response(self, response: 'Optional[Dict]') -> None: + if response: + additional_edits = response.get('additionalTextEdits') + if additional_edits: + self.apply_additional_edits(additional_edits) + + def apply_additional_edits(self, additional_edits: 'List[Dict]') -> None: + edits = list(parse_text_edit(additional_edit) for additional_edit in additional_edits) + debug('applying additional edits:', edits) + self.view.run_command("lsp_apply_document_edit", {'changes': edits}) + sublime.status_message('Applied additional edits for completion') class CompletionHelper(sublime_plugin.EventListener): @@ -49,16 +85,13 @@ def __init__(self, view: sublime.View) -> None: self.initialized = False self.enabled = False self.trigger_chars = [] # type: List[str] - self.auto_complete_selector = view.settings().get("auto_complete_selector", "") or "" # type: str - self.resolve = False - self.state = CompletionState.IDLE - self.completions = [] # type: List[Any] - self.next_request = None # type: Optional[Tuple[str, List[int]]] + + self.completion_list = sublime.CompletionList() self.last_prefix = "" self.last_location = -1 self.committing = False self.response_items = [] # type: List[dict] - self.response_incomplete = False + self.selected_item_index = -1 @classmethod def is_applicable(cls, view_settings: dict) -> bool: @@ -77,7 +110,7 @@ def initialize(self) -> None: # no trigger characters will be registered but we'll still respond to Sublime's # usual query for completions. So the explicit check for None is necessary. self.enabled = True - self.resolve = completionProvider.get('resolveProvider') or False + self.trigger_chars = completionProvider.get( 'triggerCharacters') or [] if self.trigger_chars: @@ -107,170 +140,61 @@ def register_trigger_chars(self, session: Session) -> None: self.view.settings().set('auto_complete_triggers', completion_triggers) - def is_same_completion(self, prefix: str, locations: 'List[int]') -> bool: - if self.response_incomplete: - return False - - if self.last_location < 0: - return False - - # completion requests from the same location with the same prefix are cached. - current_start = locations[0] - len(prefix) - last_start = self.last_location - len(self.last_prefix) - return prefix.startswith(self.last_prefix) and current_start == last_start - - def find_completion_item(self, inserted: str) -> 'Optional[dict]': - """ - - Returns the completionItem for a given replacement string. - Matches exactly or up to first snippet placeholder ($s) - - """ - # TODO: candidate for extracting and thorough testing. - if self.completions: - for index, item in enumerate(self.completions): - trigger, replacement = item - - snippet_offset = replacement.find('$', 2) - if snippet_offset > -1: - if inserted.startswith(replacement[:snippet_offset]): - return self.response_items[index] - else: - if replacement == inserted: - return self.response_items[index] - return None - - def on_modified(self) -> None: - - # hide completion when backspacing past last completion. - if self.view.sel()[0].begin() < self.last_location: - self.last_location = -1 - self.view.run_command("hide_auto_complete") - - # cancel current completion if the previous input is an space - prev_char = self.view.substr(self.view.sel()[0].begin() - 1) - if self.state == CompletionState.REQUESTING and prev_char.isspace(): - self.state = CompletionState.CANCELLING - - if self.committing: - self.committing = False - self.on_completion_inserted() - else: - if self.view.is_auto_complete_visible(): - if self.response_incomplete: - # debug('incomplete, triggering new completions') - self.view.run_command("hide_auto_complete") - sublime.set_timeout(self.run_auto_complete, 0) - - def on_completion_inserted(self) -> None: - # get text inserted from last completion - begin = self.last_location - - if begin < 0: - return + def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional[sublime.CompletionList]': - if position_is_word(self.view, begin): - word = self.view.word(self.last_location) - begin = word.begin() - - region = sublime.Region(begin, self.view.sel()[0].end()) - inserted = self.view.substr(region) - - item = self.find_completion_item(inserted) - if not item: - # issues 714 and 720 - calling view.word() on last_location includes a trigger char that is not part of - # inserted completion. - debug('No match for inserted "{}", skipping first char'.format(inserted)) - begin += 1 - item = self.find_completion_item(inserted[1:]) - - if item: - # the newText is already inserted, now we need to check where it should start. - edit = item.get('textEdit') - if edit: - parsed_edit = parse_text_edit(edit) - start, end, newText = parsed_edit - edit_start_loc = self.view.text_point(*start) - - # if the edit started before the word, we need to trim back to the start of the edit. - if edit_start_loc < begin: - trim_range = (edit_start_loc, begin) - debug('trimming between', trim_range, 'because textEdit', parsed_edit) - self.view.run_command("lsp_trim_completion", {'range': trim_range}) - - # import statements, etc. some servers only return these after a resolve. - additional_edits = item.get('additionalTextEdits') - if additional_edits: - self.apply_additional_edits(additional_edits) - elif self.resolve: - self.do_resolve(item) - - else: - debug('could not find completion item for inserted "{}"'.format(inserted)) - - def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional[Tuple[List[Tuple[str,str]], int]]': if not self.initialized: self.initialize() - flags = 0 - if settings.only_show_lsp_completions: - flags |= sublime.INHIBIT_WORD_COMPLETIONS - flags |= sublime.INHIBIT_EXPLICIT_COMPLETIONS - - if self.enabled: - if not self.view.match_selector(locations[0], self.auto_complete_selector): - return ([], flags) - - reuse_completion = self.is_same_completion(prefix, locations) - if self.state == CompletionState.IDLE: - if not reuse_completion: - self.last_prefix = prefix - self.last_location = locations[0] - self.do_request(prefix, locations) - self.completions = [] - - elif self.state in (CompletionState.REQUESTING, CompletionState.CANCELLING): - if not reuse_completion: - self.next_request = (prefix, locations) - self.state = CompletionState.CANCELLING - - elif self.state == CompletionState.APPLYING: - self.state = CompletionState.IDLE + if not self.enabled: + return None - return (self.completions, flags) + self.completion_list = sublime.CompletionList() + self.last_prefix = prefix + self.last_location = locations[0] + self.do_request(prefix, locations) - return None + return self.completion_list def on_text_command(self, command_name: str, args: 'Optional[Any]') -> None: self.committing = command_name in ('commit_completion', 'auto_complete') def do_request(self, prefix: str, locations: 'List[int]') -> None: - self.next_request = None - view = self.view - + print('send request') # don't store client so we can handle restarts - client = client_from_session(session_for_view(view, 'completionProvider', locations[0])) + client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) if not client: return - prev_point = max(0, locations[0] - 1) - prev_char = view.substr(prev_point) - is_word_separator = prev_char in self.view.settings().get("word_separators") + self.manager.documents.purge_changes(self.view) + document_position = get_document_position(self.view, locations[0]) + if document_position: + client.send_request( + Request.complete(document_position), + self.handle_response, + self.handle_error) + + def handle_response(self, response: 'Optional[Union[Dict,List]]') -> None: + _last_row, last_col = self.view.rowcol(self.last_location) - if prev_char in self.trigger_chars or (settings.complete_all_chars and not is_word_separator): - self.manager.documents.purge_changes(self.view) - document_position = get_document_position(view, locations[0]) - if document_position: - client.send_request( - Request.complete(document_position), - self.handle_response, - self.handle_error) - self.state = CompletionState.REQUESTING + response_items, response_incomplete = parse_completion_response(response) + self.response_items = response_items + items = list(format_completion(item, last_col) for item in response_items) - def do_resolve(self, item: dict) -> None: - view = self.view + flags = 0 + if settings.only_show_lsp_completions: + flags |= sublime.INHIBIT_WORD_COMPLETIONS + flags |= sublime.INHIBIT_EXPLICIT_COMPLETIONS + + if response_incomplete: + flags |= sublime.DYNAMIC_COMPLETIONS + + self.completion_list.set_completions(items, flags) + + def handle_error(self, error: dict) -> None: + sublime.status_message('Completion error: ' + str(error.get('message'))) - client = client_from_session(session_for_view(view, 'completionProvider', self.last_location)) + def do_resolve(self, item: dict) -> None: + client = client_from_session(session_for_view(self.view, 'completionProvider', self.last_location)) if not client: return @@ -287,58 +211,3 @@ def apply_additional_edits(self, additional_edits: 'List[Dict]') -> None: debug('applying additional edits:', edits) self.view.run_command("lsp_apply_document_edit", {'changes': edits}) sublime.status_message('Applied additional edits for completion') - - def handle_response(self, response: 'Optional[Union[Dict,List]]') -> None: - if self.state == CompletionState.REQUESTING: - - completion_start = self.last_location - if position_is_word(self.view, self.last_location): - # if completion is requested in the middle of a word, where does it start? - word = self.view.word(self.last_location) - completion_start = word.begin() - - current_word_start = self.view.sel()[0].begin() - if position_is_word(self.view, current_word_start): - current_word_region = self.view.word(current_word_start) - current_word_start = current_word_region.begin() - - if current_word_start != completion_start: - debug('completion results for', completion_start, 'now at', current_word_start, 'discarding') - self.state = CompletionState.IDLE - return - - _last_row, last_col = self.view.rowcol(completion_start) - - response_items, response_incomplete = parse_completion_response(response) - self.response_items = response_items - self.response_incomplete = response_incomplete - self.completions = list(format_completion(item, last_col, settings) for item in self.response_items) - - # if insert_best_completion was just ran, undo it before presenting new completions. - prev_char = self.view.substr(self.view.sel()[0].begin() - 1) - if prev_char.isspace(): - if last_text_command == "insert_best_completion": - self.view.run_command("undo") - - self.state = CompletionState.APPLYING - self.view.run_command("hide_auto_complete") - self.run_auto_complete() - elif self.state == CompletionState.CANCELLING: - self.state = CompletionState.IDLE - if self.next_request: - prefix, locations = self.next_request - self.do_request(prefix, locations) - else: - debug('Got unexpected response while in state {}'.format(self.state)) - - def handle_error(self, error: dict) -> None: - sublime.status_message('Completion error: ' + str(error.get('message'))) - self.state = CompletionState.IDLE - - def run_auto_complete(self) -> None: - self.view.run_command( - "auto_complete", { - 'disable_auto_insert': True, - 'api_completions_only': settings.only_show_lsp_completions, - 'next_completion_if_showing': False - }) diff --git a/plugin/core/completion.py b/plugin/core/completion.py index ca7af25c8..89dd877db 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -1,4 +1,6 @@ -from .protocol import CompletionItemKind, Range +import sublime +import sublime_plugin +from .protocol import CompletionItemKind, InsertTextFormat, Range from .types import Settings from .logging import debug try: @@ -11,52 +13,68 @@ completion_item_kind_names = {v: k for k, v in CompletionItemKind.__dict__.items()} -def get_completion_hint(item: dict, settings: 'Settings') -> 'Optional[str]': - # choose hint based on availability and user preference - hint = None - if settings.completion_hint_type == "auto": - hint = item.get("detail") - if not hint: - kind = item.get("kind") - if kind: - hint = completion_item_kind_names[kind] - elif settings.completion_hint_type == "detail": - hint = item.get("detail") - elif settings.completion_hint_type == "kind": - kind = item.get("kind") - if kind: - hint = completion_item_kind_names.get(kind) - return hint - - -def format_completion(item: dict, word_col: int, settings: 'Settings') -> 'Tuple[str, str]': - # Sublime handles snippets automatically, so we don't have to care about insertTextFormat. - trigger = item["label"] - - hint = get_completion_hint(item, settings) - - # label is an alternative for insertText if neither textEdit nor insertText is provided - replacement = text_edit_text(item, word_col) or item.get("insertText") or trigger - - if replacement[0] != trigger[0]: - # fix some common cases when server sends different start on label and replacement. - if replacement[0] == '$': - trigger = '$' + trigger # add missing $ - elif replacement[0] == '-': - trigger = '-' + trigger # add missing - - elif trigger[0] == ':': - replacement = ':' + replacement # add missing : - elif trigger[0] == '$': - trigger = trigger[1:] # remove leading $ - elif trigger[0] == ' ' or trigger[0] == '•': - trigger = trigger[1:] # remove clangd insertion indicator - else: - debug("WARNING: Replacement prefix does not match trigger '{}'".format(trigger)) - - if len(replacement) > 0 and replacement[0] == '$': # sublime needs leading '$' escaped. - replacement = '\\$' + replacement[1:] - # only return trigger with a hint if available - return "\t ".join((trigger, hint)) if hint else trigger, replacement +compleiton_kinds = { + 1: (sublime.KIND_ID_MARKUP, "Ξ", "Text"), + 2: (sublime.KIND_ID_FUNCTION, "λ", "Method"), + 3: (sublime.KIND_ID_FUNCTION, "λ", "Function"), + 4: (sublime.KIND_ID_FUNCTION, "c", "Constructor"), + 5: (sublime.KIND_ID_VARIABLE, "f", "Field"), + 6: (sublime.KIND_ID_VARIABLE, "v", "Variable"), + 7: (sublime.KIND_ID_TYPE, "⊂", "Class"), + 8: (sublime.KIND_ID_TYPE, "i", "Interface"), + 9: (sublime.KIND_ID_NAMESPACE, "❒", "Module"), + 10: (sublime.KIND_ID_VARIABLE, "ρ", "Property"), + 11: (sublime.KIND_ID_VARIABLE, "u", "Unit"), + 12: (sublime.KIND_ID_VARIABLE, "ν", "Value"), + 13: (sublime.KIND_ID_TYPE, "ε", "Enum"), + 14: (sublime.KIND_ID_KEYWORD, "ㆁ", "Keyword"), + 15: (sublime.KIND_ID_SNIPPET, "s", "Snippet"), + 16: (sublime.KIND_ID_AMBIGUOUS, "c", "Color"), + 17: (sublime.KIND_ID_AMBIGUOUS, "ʃ", "File"), + 18: (sublime.KIND_ID_AMBIGUOUS, "⇢", "Reference"), + 19: (sublime.KIND_ID_AMBIGUOUS, "ʃ", "Folder"), + 20: (sublime.KIND_ID_TYPE, "ε", "EnumMember"), + 21: (sublime.KIND_ID_VARIABLE, "π", "Constant"), + 22: (sublime.KIND_ID_TYPE, "s", "Struct"), + 23: (sublime.KIND_ID_FUNCTION, "e", "Event"), + 24: (sublime.KIND_ID_KEYWORD, "ο", "Operator"), + 25: (sublime.KIND_ID_TYPE, "τ", "Type Parameter") +} + + +def format_completion(item: dict, word_col: int, settings: 'Settings' = None) -> 'Tuple[str, str]': + trigger = item.get('label') + annotation = item.get('detail', "") + kind = sublime.KIND_AMBIGUOUS + + item_kind = item.get("kind") + if item_kind: + kind = compleiton_kinds.get(item_kind) + + is_deprecated = item.get("deprecated", False) + if is_deprecated: + list_kind = list(kind) + list_kind[1] = '⚠' + list_kind[2] = "⚠ {} - Deprecated".format(list_kind[2]) + kind = tuple(list_kind) + + completion = item.get('insertText') or item.get('label') + + insert_text_format = item.get("insertTextFormat") + if insert_text_format == InsertTextFormat.Snippet: + return sublime.CompletionItem.snippet_completion(trigger, completion, annotation, kind) + + return sublime.CompletionItem.command_completion( + trigger, + command="lsp_select_completion_item", + args={ + "item": item + }, + annotation=annotation, + kind=kind + ) + + return sublime.CompletionItem(trigger, annotation, completion, kind=kind) def text_edit_text(item: dict, word_col: int) -> 'Optional[str]': diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 5e3cfd19e..21fc82385 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -84,6 +84,11 @@ class CompletionItemKind(object): completion_item_kinds = list(range(CompletionItemKind.Text, CompletionItemKind.TypeParameter + 1)) +class InsertTextFormat: + PlainText = 1 + Snippet = 2 + + class DocumentHighlightKind(object): Unknown = 0 Text = 1 diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 621cc0548..bcceb52ee 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -91,7 +91,8 @@ def get_initialize_params(workspace_folders: 'List[WorkspaceFolder]', config: Cl }, "completion": { "completionItem": { - "snippetSupport": True + "snippetSupport": True, + "deprecatedSupport": True }, "completionItemKind": { "valueSet": completion_item_kinds diff --git a/stubs/sublime.pyi b/stubs/sublime.pyi index d60ebe8c2..56d7b95de 100644 --- a/stubs/sublime.pyi +++ b/stubs/sublime.pyi @@ -55,6 +55,7 @@ CLASS_LINE_END = ... # type: int CLASS_EMPTY_LINE = ... # type: int INHIBIT_WORD_COMPLETIONS = ... # type: int INHIBIT_EXPLICIT_COMPLETIONS = ... # type: int +DYNAMIC_COMPLETIONS = ... # type: int DIALOG_CANCEL = ... # type: int DIALOG_YES = ... # type: int DIALOG_NO = ... # type: int @@ -238,6 +239,15 @@ def get_macro() -> Sequence[dict]: ... +class CompletionItem: + ... + + +class CompletionList: + def set_completions(self, completions: List[CompletionItem], flags: int = 0) -> None: + ... + + class Window: window_id = ... # type: int settings_object = ... # type: Settings diff --git a/tests/test_completion.py b/tests/test_completion.py index 36a59f49d..bc6c9bfe8 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1,5 +1,4 @@ from LSP.plugin.completion import CompletionHandler -from LSP.plugin.completion import CompletionState from LSP.plugin.core.registry import is_supported_view from setup import CI, SUPPORTED_SYNTAX, TextDocumentTestCase, add_config, remove_config, text_config from unittesting import DeferrableTestCase @@ -176,7 +175,6 @@ def test_simple_label(self) -> 'Generator': # now wait for server response yield from self.await_message('textDocument/completion') - self.assertEquals(handler.state, CompletionState.IDLE) self.assertEquals(len(handler.completions), 2) # verify insertion works From 12e196e061fcb07f3f04c9d1fd534f4760789625 Mon Sep 17 00:00:00 2001 From: Predrag Nikolic Date: Mon, 13 Jan 2020 10:58:44 +0100 Subject: [PATCH 07/62] fix typo --- plugin/completion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index e2ebf493c..f0599bfe9 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -25,9 +25,9 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): def run(self, edit: 'Any', item) -> None: - textEdit = item.get('textEdit') - if textEdit: - range = Range.from_lsp(textEdit['range']) + text_edit = item.get('textEdit') + if text_edit: + range = Range.from_lsp(text_edit['range']) region = range_to_region(range, self.view) new_text = text_edit.get('newText') self.view.replace(edit, region, new_text) From 8320d05a0a522828197fafb673c9e374dc4b2d48 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 13 Jan 2020 19:16:46 +0100 Subject: [PATCH 08/62] Support snippets --- plugin/completion.py | 13 ++++++++++--- plugin/core/completion.py | 10 +--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index f0599bfe9..79109c7fe 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -7,7 +7,7 @@ except ImportError: pass -from .core.protocol import Request, Range +from .core.protocol import Request, Range, InsertTextFormat from .core.settings import settings, client_configs from .core.logging import debug from .core.completion import parse_completion_response, format_completion @@ -24,17 +24,24 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): def run(self, edit: 'Any', item) -> None: + insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') if text_edit: range = Range.from_lsp(text_edit['range']) region = range_to_region(range, self.view) new_text = text_edit.get('newText') - self.view.replace(edit, region, new_text) + if insert_text_format == InsertTextFormat.Snippet: + self.view.run_command("insert_snippet", { "contents": new_text }) + else: + self.view.replace(edit, region, new_text) else: completion = item.get('insertText') or item.get('label') current_point = self.view.sel()[0].begin() - self.view.insert(edit, current_point, completion) + if insert_text_format == InsertTextFormat.Snippet: + self.view.run_command("insert_snippet", { "contents": completion }) + else: + self.view.insert(edit, current_point, completion) # import statements, etc. some servers only return these after a resolve. additional_edits = item.get('additionalTextEdits') diff --git a/plugin/core/completion.py b/plugin/core/completion.py index 89dd877db..903c15b29 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -1,6 +1,6 @@ import sublime import sublime_plugin -from .protocol import CompletionItemKind, InsertTextFormat, Range +from .protocol import CompletionItemKind, Range from .types import Settings from .logging import debug try: @@ -58,12 +58,6 @@ def format_completion(item: dict, word_col: int, settings: 'Settings' = None) -> list_kind[2] = "⚠ {} - Deprecated".format(list_kind[2]) kind = tuple(list_kind) - completion = item.get('insertText') or item.get('label') - - insert_text_format = item.get("insertTextFormat") - if insert_text_format == InsertTextFormat.Snippet: - return sublime.CompletionItem.snippet_completion(trigger, completion, annotation, kind) - return sublime.CompletionItem.command_completion( trigger, command="lsp_select_completion_item", @@ -74,8 +68,6 @@ def format_completion(item: dict, word_col: int, settings: 'Settings' = None) -> kind=kind ) - return sublime.CompletionItem(trigger, annotation, completion, kind=kind) - def text_edit_text(item: dict, word_col: int) -> 'Optional[str]': text_edit = item.get('textEdit') From c43c278fcfbf4755e90db29a12179830b6465df3 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 13 Jan 2020 19:47:17 +0100 Subject: [PATCH 09/62] Insert textEdit completion, Dont replace them --- plugin/completion.py | 4 ++-- plugin/core/completion.py | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 8ba43bd54..ec464f1be 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -29,12 +29,12 @@ def run(self, edit: 'Any', item) -> None: text_edit = item.get('textEdit') if text_edit: range = Range.from_lsp(text_edit['range']) - region = range_to_region(range, self.view) + current_point = self.view.sel()[0].begin() new_text = text_edit.get('newText') if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", { "contents": new_text }) else: - self.view.replace(edit, region, new_text) + self.view.insert(edit, current_point, new_text) else: completion = item.get('insertText') or item.get('label') current_point = self.view.sel()[0].begin() diff --git a/plugin/core/completion.py b/plugin/core/completion.py index 184f47e52..fec667039 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -6,9 +6,6 @@ from .typing import Tuple, Optional, Dict, List, Union -completion_item_kind_names = {v: k for k, v in CompletionItemKind.__dict__.items()} - - compleiton_kinds = { 1: (sublime.KIND_ID_MARKUP, "Ξ", "Text"), 2: (sublime.KIND_ID_FUNCTION, "λ", "Method"), @@ -16,19 +13,19 @@ 4: (sublime.KIND_ID_FUNCTION, "c", "Constructor"), 5: (sublime.KIND_ID_VARIABLE, "f", "Field"), 6: (sublime.KIND_ID_VARIABLE, "v", "Variable"), - 7: (sublime.KIND_ID_TYPE, "⊂", "Class"), + 7: (sublime.KIND_ID_TYPE, "c", "Class"), 8: (sublime.KIND_ID_TYPE, "i", "Interface"), - 9: (sublime.KIND_ID_NAMESPACE, "❒", "Module"), + 9: (sublime.KIND_ID_NAMESPACE, "◪", "Module"), 10: (sublime.KIND_ID_VARIABLE, "ρ", "Property"), 11: (sublime.KIND_ID_VARIABLE, "u", "Unit"), 12: (sublime.KIND_ID_VARIABLE, "ν", "Value"), 13: (sublime.KIND_ID_TYPE, "ε", "Enum"), - 14: (sublime.KIND_ID_KEYWORD, "ㆁ", "Keyword"), + 14: (sublime.KIND_ID_KEYWORD, "κ", "Keyword"), 15: (sublime.KIND_ID_SNIPPET, "s", "Snippet"), 16: (sublime.KIND_ID_AMBIGUOUS, "c", "Color"), - 17: (sublime.KIND_ID_AMBIGUOUS, "ʃ", "File"), + 17: (sublime.KIND_ID_AMBIGUOUS, "ƒ", "File"), 18: (sublime.KIND_ID_AMBIGUOUS, "⇢", "Reference"), - 19: (sublime.KIND_ID_AMBIGUOUS, "ʃ", "Folder"), + 19: (sublime.KIND_ID_AMBIGUOUS, "ƒ", "Folder"), 20: (sublime.KIND_ID_TYPE, "ε", "EnumMember"), 21: (sublime.KIND_ID_VARIABLE, "π", "Constant"), 22: (sublime.KIND_ID_TYPE, "s", "Struct"), From e21b1a90aa52cc03c3aec059c93253065439128f Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 13 Jan 2020 20:25:46 +0100 Subject: [PATCH 10/62] update text icon and remove unused function --- plugin/core/completion.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/plugin/core/completion.py b/plugin/core/completion.py index fec667039..04518367d 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -23,7 +23,7 @@ 14: (sublime.KIND_ID_KEYWORD, "κ", "Keyword"), 15: (sublime.KIND_ID_SNIPPET, "s", "Snippet"), 16: (sublime.KIND_ID_AMBIGUOUS, "c", "Color"), - 17: (sublime.KIND_ID_AMBIGUOUS, "ƒ", "File"), + 17: (sublime.KIND_ID_AMBIGUOUS, "Ξ", "File"), 18: (sublime.KIND_ID_AMBIGUOUS, "⇢", "Reference"), 19: (sublime.KIND_ID_AMBIGUOUS, "ƒ", "Folder"), 20: (sublime.KIND_ID_TYPE, "ε", "EnumMember"), @@ -34,7 +34,6 @@ 25: (sublime.KIND_ID_TYPE, "τ", "Type Parameter") } - def format_completion(item: dict, word_col: int, settings: 'Settings' = None) -> 'Tuple[str, str]': trigger = item.get('label') annotation = item.get('detail', "") @@ -62,24 +61,6 @@ def format_completion(item: dict, word_col: int, settings: 'Settings' = None) -> ) -def text_edit_text(item: dict, word_col: int) -> Optional[str]: - 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) - - # debug('textEdit from col {}, {} applied at col {}'.format( - # edit_range.start.col, edit_range.end.col, word_col)) - - if edit_range.start.col <= word_col: - # if edit starts at current word, we can use it. - # if edit starts before current word, use the whole thing and we'll fix it up later. - return edit_text - - return None - - def parse_completion_response(response: Optional[Union[Dict, List]]) -> Tuple[List[Dict], bool]: items = [] # type: List[Dict] is_incomplete = False From a86c249517b1289f43f30040936657be4b680cd2 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 14 Jan 2020 22:31:49 +0100 Subject: [PATCH 11/62] FIX AttributeError: module 'asyncio' has no attribute 'get_running_loop' --- tests/server.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/server.py b/tests/server.py index 4f1a55f04..3be28b569 100644 --- a/tests/server.py +++ b/tests/server.py @@ -29,10 +29,10 @@ if sys.version_info[0] < 3: - print("only works for python3.5 and higher") + print("only works for python3.6 and higher") exit(1) -if sys.version_info[1] < 5: - print("only works for python3.5 and higher") +if sys.version_info[1] < 6: + print("only works for python3.6 and higher") exit(1) @@ -149,15 +149,15 @@ def _log(self, message: str) -> None: {"type": MessageType.info, "message": message}) def _notify(self, method: str, params: PayloadLike) -> None: - asyncio.create_task(self._send_payload( + asyncio.get_event_loop().create_task(self._send_payload( make_notification(method, params))) def _reply(self, request_id: int, params: PayloadLike) -> None: - asyncio.create_task(self._send_payload( + asyncio.get_event_loop().create_task(self._send_payload( make_response(request_id, params))) def _error(self, request_id: int, err: Error) -> None: - asyncio.create_task(self._send_payload( + asyncio.get_event_loop().create_task(self._send_payload( make_error_response(request_id, err))) async def _send_payload(self, payload: StringDict) -> None: @@ -245,7 +245,7 @@ async def _handle(self, typestr: str, message: 'Dict[str, Any]', handlers: Dict[ ErrorCode.InternalError, str(ex))) else: # handle notification - task = asyncio.create_task(handler(params)) + task = asyncio.get_event_loop().create_task(handler(params)) task.add_done_callback(self._handle_notification_handler_exception) async def _handle_body(self, body: bytes) -> None: @@ -275,7 +275,7 @@ async def run_forever(self) -> bool: if not line: continue body = await self._reader.readexactly(num_bytes) - asyncio.create_task(self._handle_body(body)) + asyncio.get_event_loop().create_task(self._handle_body(body)) except (BrokenPipeError, ConnectionResetError, StopLoopException): pass return self._received_shutdown @@ -335,7 +335,7 @@ async def _on_exit(self, params: PayloadLike) -> None: # START: https://stackoverflow.com/a/52702646/990142 async def stdio() -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]: - loop = asyncio.get_running_loop() + loop = asyncio.get_event_loop() if sys.platform == 'win32': return _win32_stdio(loop) else: From 2f5aeaeb79aa3758e04c1a556d451b380092a65b Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 25 Jan 2020 20:31:50 +0100 Subject: [PATCH 12/62] Use text edit Range --- plugin/completion.py | 19 +++++++++++++++++-- plugin/core/sessions.py | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index ec464f1be..e3bc86c7e 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -24,17 +24,30 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): def run(self, edit: 'Any', item) -> None: + print('CompletionHandler prefix', CompletionHandler.prefix) insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') if text_edit: range = Range.from_lsp(text_edit['range']) + text_edit_region = range_to_region(range, self.view) current_point = self.view.sel()[0].begin() new_text = text_edit.get('newText') if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", { "contents": new_text }) else: - self.view.insert(edit, current_point, new_text) + # subtract the prefix length from the end + end_edit_position = text_edit_region.end() - len(CompletionHandler.prefix) + edit_range = sublime.Region(text_edit_region.begin(), end_edit_position) + self.view.replace(edit, edit_range, new_text) + + # move the cursor to the end + sel = self.view.sel() + sel.clear() + sel.add(edit_range.begin() + len(new_text)) + + # reset the prefix + CompletionHandler.prefix = "" else: completion = item.get('insertText') or item.get('label') current_point = self.view.sel()[0].begin() @@ -87,6 +100,8 @@ def run(self, edit: sublime.Edit, range: 'Optional[Tuple[int, int]]' = None) -> class CompletionHandler(LSPViewEventListener): + prefix="" + def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False @@ -148,7 +163,7 @@ def register_trigger_chars(self, session: Session) -> None: self.view.settings().set('auto_complete_triggers', completion_triggers) def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional[sublime.CompletionList]': - + CompletionHandler.prefix=prefix if not self.initialized: self.initialize() diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 943dfa9f9..aa7c82547 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -10,7 +10,7 @@ import threading -ACQUIRE_READY_LOCK_TIMEOUT = 3 +ACQUIRE_READY_LOCK_TIMEOUT = 5 def get_initialize_params(workspace_folders: List[WorkspaceFolder], config: ClientConfig) -> dict: From 534ae8a991c9d3848cea3b92dd79c935dba50403 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 25 Jan 2020 23:13:47 +0100 Subject: [PATCH 13/62] use text edit range --- plugin/completion.py | 41 +++++++++++++++++++++------------------ plugin/core/completion.py | 2 +- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index e3bc86c7e..a32bf25f4 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -24,24 +24,26 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): def run(self, edit: 'Any', item) -> None: - print('CompletionHandler prefix', CompletionHandler.prefix) insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') if text_edit: - range = Range.from_lsp(text_edit['range']) - text_edit_region = range_to_region(range, self.view) - current_point = self.view.sel()[0].begin() new_text = text_edit.get('newText') + + range = Range.from_lsp(text_edit['range']) + edit_region = range_to_region(range, self.view) + + # subtract the prefix from the end + end_edit_position = edit_region.end() - len(CompletionHandler.prefix) + edit_range = sublime.Region(edit_region.begin(), end_edit_position) + if insert_text_format == InsertTextFormat.Snippet: - self.view.run_command("insert_snippet", { "contents": new_text }) + self.view.replace(edit, edit_range, "") + self.view.run_command("insert_snippet", {"contents": new_text}) else: - # subtract the prefix length from the end - end_edit_position = text_edit_region.end() - len(CompletionHandler.prefix) - edit_range = sublime.Region(text_edit_region.begin(), end_edit_position) self.view.replace(edit, edit_range, new_text) - # move the cursor to the end + # move the cursor to the end of the text edit sel = self.view.sel() sel.clear() sel.add(edit_range.begin() + len(new_text)) @@ -52,7 +54,7 @@ def run(self, edit: 'Any', item) -> None: completion = item.get('insertText') or item.get('label') current_point = self.view.sel()[0].begin() if insert_text_format == InsertTextFormat.Snippet: - self.view.run_command("insert_snippet", { "contents": completion }) + self.view.run_command("insert_snippet", {"contents": completion}) else: self.view.insert(edit, current_point, completion) @@ -60,16 +62,19 @@ def run(self, edit: 'Any', item) -> None: additional_edits = item.get('additionalTextEdits') if additional_edits: self.apply_additional_edits(additional_edits) - # elif self.resolve: - elif True: - self.do_resolve(item) + + self.do_resolve(item) def do_resolve(self, item: dict) -> None: - client = client_from_session(session_for_view(self.view, 'completionProvider', self.view.sel()[0].begin())) + session = session_for_view(self.view, 'completionProvider', self.view.sel()[0].begin()) + client = client_from_session(session) + if not client: return - client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response) + resolve_provider = session.get_capability('completionProvider').get('resolveProvider') or False + if resolve_provider: + client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response) def handle_resolve_response(self, response: 'Optional[Dict]') -> None: if response: @@ -100,7 +105,7 @@ def run(self, edit: sublime.Edit, range: 'Optional[Tuple[int, int]]' = None) -> class CompletionHandler(LSPViewEventListener): - prefix="" + prefix = "" def __init__(self, view: sublime.View) -> None: super().__init__(view) @@ -108,7 +113,6 @@ def __init__(self, view: sublime.View) -> None: self.enabled = False self.trigger_chars = [] # type: List[str] self.completion_list = sublime.CompletionList() - self.last_prefix = "" self.last_location = -1 self.committing = False self.response_items = [] # type: List[dict] @@ -163,7 +167,7 @@ def register_trigger_chars(self, session: Session) -> None: self.view.settings().set('auto_complete_triggers', completion_triggers) def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional[sublime.CompletionList]': - CompletionHandler.prefix=prefix + CompletionHandler.prefix = prefix if not self.initialized: self.initialize() @@ -171,7 +175,6 @@ def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional return None self.completion_list = sublime.CompletionList() - self.last_prefix = prefix self.last_location = locations[0] self.do_request(prefix, locations) diff --git a/plugin/core/completion.py b/plugin/core/completion.py index 04518367d..087acb757 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -23,7 +23,7 @@ 14: (sublime.KIND_ID_KEYWORD, "κ", "Keyword"), 15: (sublime.KIND_ID_SNIPPET, "s", "Snippet"), 16: (sublime.KIND_ID_AMBIGUOUS, "c", "Color"), - 17: (sublime.KIND_ID_AMBIGUOUS, "Ξ", "File"), + 17: (sublime.KIND_ID_AMBIGUOUS, "#", "File"), 18: (sublime.KIND_ID_AMBIGUOUS, "⇢", "Reference"), 19: (sublime.KIND_ID_AMBIGUOUS, "ƒ", "Folder"), 20: (sublime.KIND_ID_TYPE, "ε", "EnumMember"), From c3d8807d9dfc280a814e52c4772f73192f59324f Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 27 Jan 2020 23:59:11 +0100 Subject: [PATCH 14/62] make flake and mypy happy --- plugin/completion.py | 28 +++++++++++++--------------- plugin/core/completion.py | 20 +++++++++++--------- stubs/sublime.pyi | 31 ++++++++++++++++++++++++++++--- tests/test_completion_core.py | 18 +++++++----------- 4 files changed, 59 insertions(+), 38 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index a32bf25f4..a329fa093 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -13,7 +13,7 @@ from .core.completion import parse_completion_response, format_completion from .core.registry import session_for_view, client_from_session, LSPViewEventListener from .core.configurations import is_supported_syntax -from .core.documents import get_document_position, position_is_word +from .core.documents import get_document_position from .core.sessions import Session from .core.edit import parse_text_edit from .core.views import range_to_region @@ -23,7 +23,7 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): - def run(self, edit: 'Any', item) -> None: + def run(self, edit: 'Any', item: 'Dict') -> None: insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') @@ -33,25 +33,20 @@ def run(self, edit: 'Any', item) -> None: range = Range.from_lsp(text_edit['range']) edit_region = range_to_region(range, self.view) - # subtract the prefix from the end - end_edit_position = edit_region.end() - len(CompletionHandler.prefix) - edit_range = sublime.Region(edit_region.begin(), end_edit_position) - if insert_text_format == InsertTextFormat.Snippet: - self.view.replace(edit, edit_range, "") self.view.run_command("insert_snippet", {"contents": new_text}) else: + # subtract the prefix from the end + end_edit_position = edit_region.end() - len(CompletionHandler.prefix) + edit_range = sublime.Region(edit_region.begin(), end_edit_position) self.view.replace(edit, edit_range, new_text) # move the cursor to the end of the text edit sel = self.view.sel() sel.clear() sel.add(edit_range.begin() + len(new_text)) - - # reset the prefix - CompletionHandler.prefix = "" else: - completion = item.get('insertText') or item.get('label') + completion = item.get('insertText') or item.get('label') or "" current_point = self.view.sel()[0].begin() if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", {"contents": completion}) @@ -67,13 +62,16 @@ def run(self, edit: 'Any', item) -> None: def do_resolve(self, item: dict) -> None: session = session_for_view(self.view, 'completionProvider', self.view.sel()[0].begin()) - client = client_from_session(session) + if not session: + return + client = client_from_session(session) if not client: return - resolve_provider = session.get_capability('completionProvider').get('resolveProvider') or False - if resolve_provider: + completion_provider = session.get_capability('completionProvider') + has_resolve_provider = completion_provider and completion_provider.get('resolveProvider', False) + if has_resolve_provider: client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response) def handle_resolve_response(self, response: 'Optional[Dict]') -> None: @@ -105,6 +103,7 @@ def run(self, edit: sublime.Edit, range: 'Optional[Tuple[int, int]]' = None) -> class CompletionHandler(LSPViewEventListener): + # the last known prefix prefix = "" def __init__(self, view: sublime.View) -> None: @@ -184,7 +183,6 @@ def on_text_command(self, command_name: str, args: 'Optional[Any]') -> None: self.committing = command_name in ('commit_completion', 'auto_complete') def do_request(self, prefix: str, locations: 'List[int]') -> None: - print('send request') # don't store client so we can handle restarts client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) if not client: diff --git a/plugin/core/completion.py b/plugin/core/completion.py index 087acb757..fab898f1b 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -1,10 +1,11 @@ import sublime -import sublime_plugin -from .protocol import CompletionItemKind, Range -from .types import Settings -from .logging import debug from .typing import Tuple, Optional, Dict, List, Union +try: + from typing import cast +except ImportError: + pass + compleiton_kinds = { 1: (sublime.KIND_ID_MARKUP, "Ξ", "Text"), @@ -34,21 +35,22 @@ 25: (sublime.KIND_ID_TYPE, "τ", "Type Parameter") } -def format_completion(item: dict, word_col: int, settings: 'Settings' = None) -> 'Tuple[str, str]': - trigger = item.get('label') - annotation = item.get('detail', "") + +def format_completion(item: dict, word_col: int) -> sublime.CompletionItem: + trigger = item.get('label') or "" + annotation = item.get('detail') or "" kind = sublime.KIND_AMBIGUOUS item_kind = item.get("kind") if item_kind: - kind = compleiton_kinds.get(item_kind) + kind = compleiton_kinds.get(item_kind, sublime.KIND_AMBIGUOUS) is_deprecated = item.get("deprecated", False) if is_deprecated: list_kind = list(kind) list_kind[1] = '⚠' list_kind[2] = "⚠ {} - Deprecated".format(list_kind[2]) - kind = tuple(list_kind) + kind = cast(Tuple[int, str, str], tuple(list_kind)) return sublime.CompletionItem.command_completion( trigger, diff --git a/stubs/sublime.pyi b/stubs/sublime.pyi index 56d7b95de..b5315204d 100644 --- a/stubs/sublime.pyi +++ b/stubs/sublime.pyi @@ -68,6 +68,24 @@ UI_ELEMENT_OPEN_FILES = ... # type: int LAYOUT_INLINE = ... # type: int LAYOUT_BELOW = ... # type: int LAYOUT_BLOCK = ... # type: int +KIND_ID_AMBIGUOUS = ... # type: int +KIND_ID_KEYWORD = ... # type: int +KIND_ID_TYPE = ... # type: int +KIND_ID_FUNCTION = ... # type: int +KIND_ID_NAMESPACE = ... # type: int +KIND_ID_NAVIGATION = ... # type: int +KIND_ID_MARKUP = ... # type: int +KIND_ID_VARIABLE = ... # type: int +KIND_ID_SNIPPET = ... # type: int +KIND_AMBIGUOUS = ... # type: Tuple[int, str, str] +KIND_KEYWORD = ... # type: Tuple[int, str, str] +KIND_TYPE = ... # type: Tuple[int, str, str] +KIND_FUNCTION = ... # type: Tuple[int, str, str] +KIND_NAMESPACE = ... # type: Tuple[int, str, str] +KIND_NAVIGATION = ... # type: Tuple[int, str, str] +KIND_MARKUP = ... # type: Tuple[int, str, str] +KIND_VARIABLE = ... # type: Tuple[int, str, str] +KIND_SNIPPET = ... # type: Tuple[int, str, str] class Settings: @@ -240,8 +258,15 @@ def get_macro() -> Sequence[dict]: class CompletionItem: - ... - + @classmethod + def command_completion(cls, + trigger: str, + command: str, + args: dict = {}, + annotation: str = "", + kind: Tuple[int, str, str] = KIND_AMBIGUOUS + ) -> 'CompletionItem': + ... class CompletionList: def set_completions(self, completions: List[CompletionItem], flags: int = 0) -> None: @@ -501,7 +526,7 @@ class Selection(Sized): def clear(self) -> None: ... - def add(self, x: Region) -> None: + def add(self, x: Union[Region, int]) -> None: ... def add_all(self, regions: Sequence[Region]) -> None: diff --git a/tests/test_completion_core.py b/tests/test_completion_core.py index b204aae4d..0e700df4e 100644 --- a/tests/test_completion_core.py +++ b/tests/test_completion_core.py @@ -2,7 +2,6 @@ from os import path import json from LSP.plugin.core.completion import format_completion, parse_completion_response -from LSP.plugin.core.types import Settings try: from typing import Optional, Dict assert Optional and Dict @@ -19,9 +18,6 @@ def load_completion_sample(name: str) -> 'Dict': intelephense_completion_sample = load_completion_sample("intelephense_completion_sample") -settings = Settings() - - class CompletionResponseParsingTests(unittest.TestCase): def test_no_response(self): @@ -40,7 +36,7 @@ def test_incomplete_dict_response(self): class CompletionFormattingTests(unittest.TestCase): def test_only_label_item(self): - result = format_completion({"label": "asdf"}, 0, settings) + result = format_completion({"label": "asdf"}, 0) self.assertEqual(len(result), 2) self.assertEqual("asdf", result[0]) self.assertEqual("asdf", result[1]) @@ -48,7 +44,7 @@ def test_only_label_item(self): def test_prefers_insert_text(self): result = format_completion( {"label": "asdf", "insertText": "Asdf"}, - 0, settings) + 0) self.assertEqual(len(result), 2) self.assertEqual("asdf", result[0]) self.assertEqual("Asdf", result[1]) @@ -68,7 +64,7 @@ def test_ignores_text_edit(self): } } - result = format_completion(item, 0, settings) + result = format_completion(item, 0) self.assertEqual(len(result), 2) self.assertEqual("$true", result[0]) self.assertEqual("\\$true", result[1]) @@ -93,12 +89,12 @@ def test_use_label_as_is(self): } } last_col = 1 - result = format_completion(item, last_col, settings) + result = format_completion(item, last_col) self.assertEqual(result, ('const\t Keyword', 'const')) def test_text_edit_intelephense(self): last_col = 1 - result = [format_completion(item, last_col, settings) for item in intelephense_completion_sample] + result = [format_completion(item, last_col) for item in intelephense_completion_sample] self.assertEqual( result, [ @@ -125,7 +121,7 @@ def test_text_edit_clangd(self): # handler.last_location = 1 # handler.last_prefix = "" last_col = 1 - result = [format_completion(item, last_col, settings) for item in clangd_completion_sample] + result = [format_completion(item, last_col) for item in clangd_completion_sample] # We should prefer textEdit over insertText. This test covers that. self.assertEqual( result, [('argc\t int', 'argc'), ('argv\t const char **', 'argv'), @@ -150,7 +146,7 @@ def test_text_edit_clangd(self): def test_missing_text_edit_but_we_do_have_insert_text_for_pyls(self): last_col = 1 - result = [format_completion(item, last_col, settings) for item in pyls_completion_sample] + result = [format_completion(item, last_col) for item in pyls_completion_sample] self.assertEqual( result, [ From ea29e0bcabda950f04904d4f4993230867650c20 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 28 Jan 2020 19:16:48 +0100 Subject: [PATCH 15/62] Add typings and remove unused class property --- plugin/completion.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index be279187b..5f77169e1 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -1,12 +1,6 @@ import sublime import sublime_plugin -try: - from typing import Any, List, Dict, Tuple, Callable, Optional, Union - assert Any and List and Dict and Tuple and Callable and Optional and Union -except ImportError: - pass - from .core.protocol import Request, Range, InsertTextFormat from .core.settings import settings, client_configs from .core.logging import debug @@ -17,6 +11,7 @@ from .core.sessions import Session from .core.edit import parse_text_edit from .core.views import range_to_region +from .core.typing import Any, List, Dict, Tuple, Optional, Union last_text_command = None @@ -115,7 +110,6 @@ def __init__(self, view: sublime.View) -> None: self.last_location = -1 self.committing = False self.response_items = [] # type: List[dict] - self.selected_item_index = -1 @classmethod def is_applicable(cls, view_settings: dict) -> bool: From 7d78abbab24cd392fbb928981647b8c5b4f33a72 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 28 Jan 2020 23:10:40 +0100 Subject: [PATCH 16/62] When choosing a completion item the typed text (prefix) will be deleted and that the completion item text will be inserted, But because edit_region.end() will take in account the previously typed text(the prefix), which is erased, we wont be using the end edit region. because it deletes some lines that it shouldn't, so we use the current_point as the end of the edit Region --- plugin/completion.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 5f77169e1..23c7ff049 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -32,8 +32,8 @@ def run(self, edit: 'Any', item: 'Dict') -> None: self.view.run_command("insert_snippet", {"contents": new_text}) else: # subtract the prefix from the end - end_edit_position = edit_region.end() - len(CompletionHandler.prefix) - edit_range = sublime.Region(edit_region.begin(), end_edit_position) + current_point = self.view.sel()[0].begin() + edit_range = sublime.Region(edit_region.begin(), current_point) self.view.replace(edit, edit_range, new_text) # move the cursor to the end of the text edit @@ -98,9 +98,6 @@ def run(self, edit: sublime.Edit, range: Optional[Tuple[int, int]] = None) -> No class CompletionHandler(LSPViewEventListener): - # the last known prefix - prefix = "" - def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False @@ -160,7 +157,6 @@ def register_trigger_chars(self, session: Session) -> None: self.view.settings().set('auto_complete_triggers', completion_triggers) def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional[sublime.CompletionList]': - CompletionHandler.prefix = prefix if not self.initialized: self.initialize() From 97ae903b302ed3b8b2cb1c6958bf685c533b8a0b Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 28 Jan 2020 23:21:14 +0100 Subject: [PATCH 17/62] Add cache --- plugin/completion.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 23c7ff049..16a34f1a2 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -106,7 +106,6 @@ def __init__(self, view: sublime.View) -> None: self.completion_list = sublime.CompletionList() self.last_location = -1 self.committing = False - self.response_items = [] # type: List[dict] @classmethod def is_applicable(cls, view_settings: dict) -> bool: @@ -163,6 +162,10 @@ def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional if not self.enabled: return None + if self.last_location == locations[0]: + # return cache + return self.completion_list + self.completion_list = sublime.CompletionList() self.last_location = locations[0] self.do_request(prefix, locations) @@ -190,7 +193,6 @@ def handle_response(self, response: 'Optional[Union[Dict,List]]') -> None: _last_row, last_col = self.view.rowcol(self.last_location) response_items, response_incomplete = parse_completion_response(response) - self.response_items = response_items items = list(format_completion(item, last_col) for item in response_items) flags = 0 From ea4dd306d4292f31870b138eb3f3d9cbd6bd4394 Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 30 Jan 2020 20:18:25 +0100 Subject: [PATCH 18/62] Ignore mypy error instead of casting a type --- plugin/core/completion.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/plugin/core/completion.py b/plugin/core/completion.py index fab898f1b..3a15053e2 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -1,11 +1,6 @@ import sublime from .typing import Tuple, Optional, Dict, List, Union -try: - from typing import cast -except ImportError: - pass - compleiton_kinds = { 1: (sublime.KIND_ID_MARKUP, "Ξ", "Text"), @@ -45,12 +40,12 @@ def format_completion(item: dict, word_col: int) -> sublime.CompletionItem: if item_kind: kind = compleiton_kinds.get(item_kind, sublime.KIND_AMBIGUOUS) - is_deprecated = item.get("deprecated", False) - if is_deprecated: - list_kind = list(kind) - list_kind[1] = '⚠' - list_kind[2] = "⚠ {} - Deprecated".format(list_kind[2]) - kind = cast(Tuple[int, str, str], tuple(list_kind)) + is_deprecated = item.get("deprecated", True) + # if is_deprecated: + # list_kind = list(kind) + # list_kind[1] = '⚠' + # list_kind[2] = "⚠ {} - Deprecated".format(list_kind[2]) + # kind = tuple(list_kind) # type: ignore return sublime.CompletionItem.command_completion( trigger, From 8a117216014641ebbb193a848dee2980ca8efdf7 Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 30 Jan 2020 20:30:17 +0100 Subject: [PATCH 19/62] revert back timeout --- plugin/core/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index aa7c82547..943dfa9f9 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -10,7 +10,7 @@ import threading -ACQUIRE_READY_LOCK_TIMEOUT = 5 +ACQUIRE_READY_LOCK_TIMEOUT = 3 def get_initialize_params(workspace_folders: List[WorkspaceFolder], config: ClientConfig) -> dict: From f9d8bb97a928e8316f80da14bb6f2463758898d7 Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 30 Jan 2020 20:38:05 +0100 Subject: [PATCH 20/62] Remove committing flag --- plugin/completion.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 16a34f1a2..6cf8bd453 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -105,7 +105,6 @@ def __init__(self, view: sublime.View) -> None: self.trigger_chars = [] # type: List[str] self.completion_list = sublime.CompletionList() self.last_location = -1 - self.committing = False @classmethod def is_applicable(cls, view_settings: dict) -> bool: @@ -172,9 +171,6 @@ def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional return self.completion_list - def on_text_command(self, command_name: str, args: 'Optional[Any]') -> None: - self.committing = command_name in ('commit_completion', 'auto_complete') - def do_request(self, prefix: str, locations: 'List[int]') -> None: # don't store client so we can handle restarts client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) From 793ec405e991a0dab577f4472fd49e7b31918f1b Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 30 Jan 2020 20:47:30 +0100 Subject: [PATCH 21/62] better comment --- plugin/completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/completion.py b/plugin/completion.py index 6cf8bd453..6377cf63a 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -31,7 +31,7 @@ def run(self, edit: 'Any', item: 'Dict') -> None: if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", {"contents": new_text}) else: - # subtract the prefix from the end + # use current point as the end of the edit region current_point = self.view.sel()[0].begin() edit_range = sublime.Region(edit_region.begin(), current_point) self.view.replace(edit, edit_range, new_text) From 10ad58aa9aa054899de3b3b32a9a20b668f56d4a Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 30 Jan 2020 21:05:44 +0100 Subject: [PATCH 22/62] revert accidentally committed code --- plugin/core/completion.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/core/completion.py b/plugin/core/completion.py index 3a15053e2..cdab4e3b8 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -40,12 +40,12 @@ def format_completion(item: dict, word_col: int) -> sublime.CompletionItem: if item_kind: kind = compleiton_kinds.get(item_kind, sublime.KIND_AMBIGUOUS) - is_deprecated = item.get("deprecated", True) - # if is_deprecated: - # list_kind = list(kind) - # list_kind[1] = '⚠' - # list_kind[2] = "⚠ {} - Deprecated".format(list_kind[2]) - # kind = tuple(list_kind) # type: ignore + is_deprecated = item.get("deprecated", False) + if is_deprecated: + list_kind = list(kind) + list_kind[1] = '⚠' + list_kind[2] = "⚠ {} - Deprecated".format(list_kind[2]) + kind = tuple(list_kind) # type: ignore return sublime.CompletionItem.command_completion( trigger, From d2505b062b6c10b79fa77af2c3e5cfd49ac7e905 Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 30 Jan 2020 21:54:38 +0100 Subject: [PATCH 23/62] remove last_text_command as it is not necessary --- plugin/completion.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 6377cf63a..7fa121d48 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -14,9 +14,6 @@ from .core.typing import Any, List, Dict, Tuple, Optional, Union -last_text_command = None - - class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): def run(self, edit: 'Any', item: 'Dict') -> None: insert_text_format = item.get("insertTextFormat") @@ -82,12 +79,6 @@ def apply_additional_edits(self, additional_edits: 'List[Dict]') -> None: sublime.status_message('Applied additional edits for completion') -class CompletionHelper(sublime_plugin.EventListener): - def on_text_command(self, view: sublime.View, command_name: str, args: Optional[Any]) -> None: - global last_text_command - last_text_command = command_name - - class LspTrimCompletionCommand(sublime_plugin.TextCommand): def run(self, edit: sublime.Edit, range: Optional[Tuple[int, int]] = None) -> None: From 542b489f85072a442587008a94a293a9b98b8038 Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 30 Jan 2020 21:56:16 +0100 Subject: [PATCH 24/62] remove completion_hint_type setting --- LSP.sublime-settings | 7 ------- docs/features.md | 1 - plugin/core/settings.py | 1 - plugin/core/types.py | 1 - 4 files changed, 10 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index a666c1d4b..5f1605fb6 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -62,13 +62,6 @@ // or just after trigger characters only otherwise. "complete_all_chars": true, - // Controls which hints the completion panel displays - // "auto": completion details if available or kind otherwise - // "detail": completion details if available - // "kind": completion kind if available - // "none": completion item label only - "completion_hint_type": "auto", - // Disable Sublime Text's explicit and word completion. "only_show_lsp_completions": false, diff --git a/docs/features.md b/docs/features.md index 9a2c7a81a..e60c0600d 100644 --- a/docs/features.md +++ b/docs/features.md @@ -72,7 +72,6 @@ Add these settings to your Sublime settings, Syntax-specific settings and/or in * `complete_all_chars` `true` *request completions for all characters, not just trigger characters* * `only_show_lsp_completions` `false` *disable sublime word completion and snippets from autocomplete lists* -* `completion_hint_type` `"auto"` *override automatic completion hints with "detail", "kind" or "none"* * `show_references_in_quick_panel` `false` *show symbol references in Sublime's quick panel instead of the bottom panel* * `show_view_status` `true` *show permanent language server status in the status bar* * `auto_show_diagnostics_panel` `always` (`never`, `saved`) *open the diagnostics panel automatically if there are diagnostics* diff --git a/plugin/core/settings.py b/plugin/core/settings.py index fc01de66d..75e53a42d 100644 --- a/plugin/core/settings.py +++ b/plugin/core/settings.py @@ -76,7 +76,6 @@ def update_settings(settings: Settings, settings_obj: sublime.Settings) -> None: settings.show_symbol_action_links = read_bool_setting(settings_obj, "show_symbol_action_links", False) 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.show_references_in_quick_panel = read_bool_setting(settings_obj, "show_references_in_quick_panel", False) settings.disabled_capabilities = read_array_setting(settings_obj, "disabled_capabilities", []) settings.log_debug = read_bool_setting(settings_obj, "log_debug", False) diff --git a/plugin/core/types.py b/plugin/core/types.py index e26c7211b..e04c3630f 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -23,7 +23,6 @@ def __init__(self) -> None: self.show_code_actions_bulb = False self.show_symbol_action_links = False self.complete_all_chars = False - self.completion_hint_type = "auto" self.show_references_in_quick_panel = False self.disabled_capabilities = [] # type: List[str] self.log_debug = True From d5a5d62e124312d7e7f703f5c4d9b4452e5c12fc Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 30 Jan 2020 22:02:45 +0100 Subject: [PATCH 25/62] remove quotes from types --- plugin/completion.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 7fa121d48..45e33919d 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -15,7 +15,7 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): - def run(self, edit: 'Any', item: 'Dict') -> None: + def run(self, edit: Any, item: dict) -> None: insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') @@ -66,13 +66,13 @@ def do_resolve(self, item: dict) -> None: if has_resolve_provider: client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response) - def handle_resolve_response(self, response: 'Optional[Dict]') -> None: + def handle_resolve_response(self, response: Optional[dict]) -> None: if response: additional_edits = response.get('additionalTextEdits') if additional_edits: self.apply_additional_edits(additional_edits) - def apply_additional_edits(self, additional_edits: 'List[Dict]') -> None: + def apply_additional_edits(self, additional_edits: List[dict]) -> None: edits = list(parse_text_edit(additional_edit) for additional_edit in additional_edits) debug('applying additional edits:', edits) self.view.run_command("lsp_apply_document_edit", {'changes': edits}) @@ -80,7 +80,6 @@ def apply_additional_edits(self, additional_edits: 'List[Dict]') -> None: class LspTrimCompletionCommand(sublime_plugin.TextCommand): - def run(self, edit: sublime.Edit, range: Optional[Tuple[int, int]] = None) -> None: if range: start, end = range @@ -145,7 +144,7 @@ def register_trigger_chars(self, session: Session) -> None: self.view.settings().set('auto_complete_triggers', completion_triggers) - def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional[sublime.CompletionList]': + def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[sublime.CompletionList]: if not self.initialized: self.initialize() @@ -162,7 +161,7 @@ def on_query_completions(self, prefix: str, locations: 'List[int]') -> 'Optional return self.completion_list - def do_request(self, prefix: str, locations: 'List[int]') -> None: + def do_request(self, prefix: str, locations: List[int]) -> None: # don't store client so we can handle restarts client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) if not client: @@ -176,7 +175,7 @@ def do_request(self, prefix: str, locations: 'List[int]') -> None: self.handle_response, self.handle_error) - def handle_response(self, response: 'Optional[Union[Dict,List]]') -> None: + def handle_response(self, response: Optional[Union[dict, List]]) -> None: _last_row, last_col = self.view.rowcol(self.last_location) response_items, response_incomplete = parse_completion_response(response) @@ -202,13 +201,13 @@ def do_resolve(self, item: dict) -> None: client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response) - def handle_resolve_response(self, response: Optional[Dict]) -> None: + def handle_resolve_response(self, response: Optional[dict]) -> None: if response: additional_edits = response.get('additionalTextEdits') if additional_edits: self.apply_additional_edits(additional_edits) - def apply_additional_edits(self, additional_edits: List[Dict]) -> None: + def apply_additional_edits(self, additional_edits: List[dict]) -> None: edits = list(parse_text_edit(additional_edit) for additional_edit in additional_edits) debug('applying additional edits:', edits) self.view.run_command("lsp_apply_document_edit", {'changes': edits}) From f46548a11c1ede367cb635667de414408367a405 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 3 Feb 2020 20:49:43 +0100 Subject: [PATCH 26/62] Use text_edit['range'].end and remove duplicated functions --- plugin/completion.py | 59 ++++++++++------------------------- plugin/core/completion.py | 2 +- tests/test_completion_core.py | 16 +++++----- 3 files changed, 25 insertions(+), 52 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 45e33919d..f8d537c42 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -24,25 +24,25 @@ def run(self, edit: Any, item: dict) -> None: range = Range.from_lsp(text_edit['range']) edit_region = range_to_region(range, self.view) + # subtract the typed prefix form the end of the text edit + edit_region = sublime.Region(edit_region.begin(), edit_region.end() - len(CompletionHandler.last_prefix)) if insert_text_format == InsertTextFormat.Snippet: + self.view.erase(edit, edit_region) self.view.run_command("insert_snippet", {"contents": new_text}) else: - # use current point as the end of the edit region - current_point = self.view.sel()[0].begin() - edit_range = sublime.Region(edit_region.begin(), current_point) - self.view.replace(edit, edit_range, new_text) + self.view.replace(edit, edit_region, new_text) # move the cursor to the end of the text edit sel = self.view.sel() sel.clear() - sel.add(edit_range.begin() + len(new_text)) + sel.add(edit_region.begin() + len(new_text)) else: completion = item.get('insertText') or item.get('label') or "" - current_point = self.view.sel()[0].begin() if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", {"contents": completion}) else: + current_point = self.view.sel()[0].begin() self.view.insert(edit, current_point, completion) # import statements, etc. some servers only return these after a resolve. @@ -88,13 +88,13 @@ def run(self, edit: sublime.Edit, range: Optional[Tuple[int, int]] = None) -> No class CompletionHandler(LSPViewEventListener): + last_prefix = "" + def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False self.enabled = False self.trigger_chars = [] # type: List[str] - self.completion_list = sublime.CompletionList() - self.last_location = -1 @classmethod def is_applicable(cls, view_settings: dict) -> bool: @@ -151,17 +151,12 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su if not self.enabled: return None - if self.last_location == locations[0]: - # return cache - return self.completion_list + completion_list = sublime.CompletionList() + self.do_request(completion_list, prefix, locations) - self.completion_list = sublime.CompletionList() - self.last_location = locations[0] - self.do_request(prefix, locations) + return completion_list - return self.completion_list - - def do_request(self, prefix: str, locations: List[int]) -> None: + def do_request(self, completion_list, prefix: str, locations: List[int]) -> None: # don't store client so we can handle restarts client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) if not client: @@ -172,14 +167,12 @@ def do_request(self, prefix: str, locations: List[int]) -> None: if document_position: client.send_request( Request.complete(document_position), - self.handle_response, + lambda res: self.handle_response(res, completion_list, prefix), self.handle_error) - def handle_response(self, response: Optional[Union[dict, List]]) -> None: - _last_row, last_col = self.view.rowcol(self.last_location) - + def handle_response(self, response: Optional[Union[dict, List]], completion_list, prefix: str) -> None: response_items, response_incomplete = parse_completion_response(response) - items = list(format_completion(item, last_col) for item in response_items) + items = list(format_completion(item) for item in response_items) flags = 0 if settings.only_show_lsp_completions: @@ -189,26 +182,8 @@ def handle_response(self, response: Optional[Union[dict, List]]) -> None: if response_incomplete: flags |= sublime.DYNAMIC_COMPLETIONS - self.completion_list.set_completions(items, flags) + completion_list.set_completions(items, flags) + CompletionHandler.last_prefix = prefix def handle_error(self, error: dict) -> None: sublime.status_message('Completion error: ' + str(error.get('message'))) - - def do_resolve(self, item: dict) -> None: - client = client_from_session(session_for_view(self.view, 'completionProvider', self.last_location)) - if not client: - return - - client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response) - - def handle_resolve_response(self, response: Optional[dict]) -> None: - if response: - additional_edits = response.get('additionalTextEdits') - if additional_edits: - self.apply_additional_edits(additional_edits) - - def apply_additional_edits(self, additional_edits: List[dict]) -> None: - edits = list(parse_text_edit(additional_edit) for additional_edit in additional_edits) - debug('applying additional edits:', edits) - self.view.run_command("lsp_apply_document_edit", {'changes': edits}) - sublime.status_message('Applied additional edits for completion') diff --git a/plugin/core/completion.py b/plugin/core/completion.py index cdab4e3b8..03c32643b 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -31,7 +31,7 @@ } -def format_completion(item: dict, word_col: int) -> sublime.CompletionItem: +def format_completion(item: dict) -> sublime.CompletionItem: trigger = item.get('label') or "" annotation = item.get('detail') or "" kind = sublime.KIND_AMBIGUOUS diff --git a/tests/test_completion_core.py b/tests/test_completion_core.py index 0e700df4e..a84971023 100644 --- a/tests/test_completion_core.py +++ b/tests/test_completion_core.py @@ -36,15 +36,13 @@ def test_incomplete_dict_response(self): class CompletionFormattingTests(unittest.TestCase): def test_only_label_item(self): - result = format_completion({"label": "asdf"}, 0) + result = format_completion({"label": "asdf"}) self.assertEqual(len(result), 2) self.assertEqual("asdf", result[0]) self.assertEqual("asdf", result[1]) def test_prefers_insert_text(self): - result = format_completion( - {"label": "asdf", "insertText": "Asdf"}, - 0) + result = format_completion({"label": "asdf", "insertText": "Asdf"}) self.assertEqual(len(result), 2) self.assertEqual("asdf", result[0]) self.assertEqual("Asdf", result[1]) @@ -64,7 +62,7 @@ def test_ignores_text_edit(self): } } - result = format_completion(item, 0) + result = format_completion(item) self.assertEqual(len(result), 2) self.assertEqual("$true", result[0]) self.assertEqual("\\$true", result[1]) @@ -89,12 +87,12 @@ def test_use_label_as_is(self): } } last_col = 1 - result = format_completion(item, last_col) + result = format_completion(item) self.assertEqual(result, ('const\t Keyword', 'const')) def test_text_edit_intelephense(self): last_col = 1 - result = [format_completion(item, last_col) for item in intelephense_completion_sample] + result = [format_completion(item) for item in intelephense_completion_sample] self.assertEqual( result, [ @@ -121,7 +119,7 @@ def test_text_edit_clangd(self): # handler.last_location = 1 # handler.last_prefix = "" last_col = 1 - result = [format_completion(item, last_col) for item in clangd_completion_sample] + result = [format_completion(item) for item in clangd_completion_sample] # We should prefer textEdit over insertText. This test covers that. self.assertEqual( result, [('argc\t int', 'argc'), ('argv\t const char **', 'argv'), @@ -146,7 +144,7 @@ def test_text_edit_clangd(self): def test_missing_text_edit_but_we_do_have_insert_text_for_pyls(self): last_col = 1 - result = [format_completion(item, last_col) for item in pyls_completion_sample] + result = [format_completion(item) for item in pyls_completion_sample] self.assertEqual( result, [ From ff9e0aecb34b6d3e17b214933b0079b0cfd00713 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 4 Feb 2020 18:57:09 +0100 Subject: [PATCH 27/62] Skip calculating the offset by inserting the removed prefix first. And don't touch selections. --- plugin/completion.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index f8d537c42..4ba526e17 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -16,33 +16,31 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): def run(self, edit: Any, item: dict) -> None: + current_point = self.view.sel()[0].begin() + insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') if text_edit: + # insert the removed command completion item prefix + # so we don't have to calculate the offset for the textEdit range + self.view.insert(edit, current_point, CompletionHandler.last_prefix) + new_text = text_edit.get('newText') range = Range.from_lsp(text_edit['range']) edit_region = range_to_region(range, self.view) - # subtract the typed prefix form the end of the text edit - edit_region = sublime.Region(edit_region.begin(), edit_region.end() - len(CompletionHandler.last_prefix)) + self.view.erase(edit, edit_region) if insert_text_format == InsertTextFormat.Snippet: - self.view.erase(edit, edit_region) self.view.run_command("insert_snippet", {"contents": new_text}) else: - self.view.replace(edit, edit_region, new_text) - - # move the cursor to the end of the text edit - sel = self.view.sel() - sel.clear() - sel.add(edit_region.begin() + len(new_text)) + self.view.insert(edit, edit_region.begin(), new_text) else: completion = item.get('insertText') or item.get('label') or "" if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", {"contents": completion}) else: - current_point = self.view.sel()[0].begin() self.view.insert(edit, current_point, completion) # import statements, etc. some servers only return these after a resolve. From 2f504f71cb5102d6b999eaa9ce918f76bbd219d5 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 4 Feb 2020 19:36:13 +0100 Subject: [PATCH 28/62] Add types and remove trigger_chars from instance properties --- plugin/completion.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 4ba526e17..bfc057dd4 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -92,7 +92,6 @@ def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False self.enabled = False - self.trigger_chars = [] # type: List[str] @classmethod def is_applicable(cls, view_settings: dict) -> bool: @@ -112,31 +111,31 @@ def initialize(self) -> None: # usual query for completions. So the explicit check for None is necessary. self.enabled = True - self.trigger_chars = completionProvider.get( + trigger_chars = completionProvider.get( 'triggerCharacters') or [] - if self.trigger_chars: - self.register_trigger_chars(session) + if trigger_chars: + self.register_trigger_chars(session, trigger_chars) self.auto_complete_selector = self.view.settings().get("auto_complete_selector", "") or "" def _view_language(self, config_name: str) -> Optional[str]: languages = self.view.settings().get('lsp_language') return languages.get(config_name) if languages else None - def register_trigger_chars(self, session: Session) -> None: + def register_trigger_chars(self, session: Session, trigger_chars: List[str]) -> None: completion_triggers = self.view.settings().get('auto_complete_triggers', []) or [] # type: List[Dict[str, str]] view_language = self._view_language(session.config.name) if view_language: for language in session.config.languages: if language.id == view_language: for scope in language.scopes: - # debug("registering", self.trigger_chars, "for", scope) + # debug("registering", trigger_chars, "for", scope) scope_trigger = next( (trigger for trigger in completion_triggers if trigger.get('selector', None) == scope), None ) if not scope_trigger: # do not override user's trigger settings. completion_triggers.append({ - 'characters': "".join(self.trigger_chars), + 'characters': "".join(trigger_chars), 'selector': scope }) @@ -154,7 +153,7 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su return completion_list - def do_request(self, completion_list, prefix: str, locations: List[int]) -> None: + def do_request(self, completion_list: sublime.CompletionList, prefix: str, locations: List[int]) -> None: # don't store client so we can handle restarts client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) if not client: @@ -168,7 +167,8 @@ def do_request(self, completion_list, prefix: str, locations: List[int]) -> None lambda res: self.handle_response(res, completion_list, prefix), self.handle_error) - def handle_response(self, response: Optional[Union[dict, List]], completion_list, prefix: str) -> None: + def handle_response(self, response: Optional[Union[dict, List]], + completion_list: sublime.CompletionList, prefix: str) -> None: response_items, response_incomplete = parse_completion_response(response) items = list(format_completion(item) for item in response_items) From 896dcef3ff7491f257752efeb1d8a318ed357143 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 4 Feb 2020 19:36:33 +0100 Subject: [PATCH 29/62] Fix flake --- tests/test_completion_core.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_completion_core.py b/tests/test_completion_core.py index a84971023..54fb1531a 100644 --- a/tests/test_completion_core.py +++ b/tests/test_completion_core.py @@ -86,12 +86,10 @@ def test_use_label_as_is(self): } } } - last_col = 1 result = format_completion(item) self.assertEqual(result, ('const\t Keyword', 'const')) def test_text_edit_intelephense(self): - last_col = 1 result = [format_completion(item) for item in intelephense_completion_sample] self.assertEqual( result, @@ -118,7 +116,6 @@ def test_text_edit_intelephense(self): def test_text_edit_clangd(self): # handler.last_location = 1 # handler.last_prefix = "" - last_col = 1 result = [format_completion(item) for item in clangd_completion_sample] # We should prefer textEdit over insertText. This test covers that. self.assertEqual( @@ -143,7 +140,6 @@ def test_text_edit_clangd(self): ('atol(const char *__nptr)\t long', 'atol(${1:const char *__nptr})')]) def test_missing_text_edit_but_we_do_have_insert_text_for_pyls(self): - last_col = 1 result = [format_completion(item) for item in pyls_completion_sample] self.assertEqual( result, From 87ab9e38701286f3b921ec059cd8aafcd5a8781d Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 4 Feb 2020 19:36:59 +0100 Subject: [PATCH 30/62] remove auto_complete_selector --- plugin/completion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/completion.py b/plugin/completion.py index bfc057dd4..04b8c1c19 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -115,7 +115,6 @@ def initialize(self) -> None: 'triggerCharacters') or [] if trigger_chars: self.register_trigger_chars(session, trigger_chars) - self.auto_complete_selector = self.view.settings().get("auto_complete_selector", "") or "" def _view_language(self, config_name: str) -> Optional[str]: languages = self.view.settings().get('lsp_language') From 1744b160a7a8e25039c789becea2ebac69bd69d7 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 8 Feb 2020 18:39:35 +0100 Subject: [PATCH 31/62] Handle multiple cursor completions --- plugin/completion.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 04b8c1c19..8d60a16b8 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -16,15 +16,14 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): def run(self, edit: Any, item: dict) -> None: - current_point = self.view.sel()[0].begin() - insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') if text_edit: # insert the removed command completion item prefix # so we don't have to calculate the offset for the textEdit range - self.view.insert(edit, current_point, CompletionHandler.last_prefix) + for sel in self.view.sel(): + self.view.insert(edit, sel.begin(), CompletionHandler.last_prefix) new_text = text_edit.get('newText') @@ -41,7 +40,8 @@ def run(self, edit: Any, item: dict) -> None: if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", {"contents": completion}) else: - self.view.insert(edit, current_point, completion) + for sel in self.view.sel(): + self.view.insert(edit, sel.begin(), completion) # import statements, etc. some servers only return these after a resolve. additional_edits = item.get('additionalTextEdits') From 5782b46c169046ea4e1ecd469455899b8428bf94 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 8 Feb 2020 18:41:51 +0100 Subject: [PATCH 32/62] Can be(will be probably) reverted. This is my try to fix the completion tests. --- plugin/completion.py | 8 +- tests/test_completion.py | 503 ++++++++++++++++++++------------------- 2 files changed, 260 insertions(+), 251 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 8d60a16b8..c62a16341 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -92,6 +92,7 @@ def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False self.enabled = False + self.test_completions = None # type: List[dict] @classmethod def is_applicable(cls, view_settings: dict) -> bool: @@ -148,7 +149,12 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su return None completion_list = sublime.CompletionList() - self.do_request(completion_list, prefix, locations) + + # this is for tests + if self.test_completions: + self.handle_response(self.test_completions, completion_list, prefix) + else: + self.do_request(completion_list, prefix, locations) return completion_list diff --git a/tests/test_completion.py b/tests/test_completion.py index d239848f6..b16fda03e 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -11,111 +11,146 @@ except ImportError: pass -label_completions = [dict(label='asdf'), dict(label='efgh')] +label_completions = [{'label': 'asdf'}, {'label': 'efcgh'}] completion_with_additional_edits = [ - dict(label='asdf', - additionalTextEdits=[{ - 'range': { - 'start': { - 'line': 0, - 'character': 0 - }, - 'end': { - 'line': 0, - 'character': 0 - } - }, - 'newText': 'import asdf;\n' - }]) + { + 'label': 'asdf', + 'additionalTextEdits': [ + { + 'range': { + 'start': { + 'line': 0, + 'character': 0 + }, + 'end': { + 'line': 0, + 'character': 0 + } + }, + 'newText': 'import asdf;\n' + } + ] + } +] +insert_text_completions = [{'label': 'asdf', 'insertText': 'asdf()'}] +var_completion_using_label = [{'label': '$what'}] +var_prefix_added_in_insertText = [ + { + "insertText": "$true", + "label": "true", + "textEdit": { + "newText": "$true", + "range": { + "end": { + "character": 5, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + } ] -insert_text_completions = [dict(label='asdf', insertText='asdf()')] -var_completion_using_label = [dict(label='$what')] -var_prefix_added_in_insertText = [dict(label='$what', insertText='what')] var_prefix_added_in_label = [ - dict(label='$what', - textEdit={ - 'range': { - 'start': { - 'line': 0, - 'character': 1 - }, - 'end': { - 'line': 0, - 'character': 1 - } - }, - 'newText': 'what' - }) + { + 'label': '$what', + 'textEdit': { + 'range': { + 'start': { + 'line': 0, + 'character': 1 + }, + 'end': { + 'line': 0, + 'character': 1 + } + }, + 'newText': 'what' + } + } ] -space_added_in_label = [dict(label=' const', insertText='const')] +space_added_in_label = [{'label': ' const', 'insertText': 'const'}] dash_missing_from_label = [ - dict(label='UniqueId', - textEdit={ - 'range': { - 'start': { - 'character': 14, - 'line': 26 - }, - 'end': { - 'character': 15, - 'line': 26 - } - }, - 'newText': '-UniqueId' - }, - insertText='-UniqueId') + { + 'label': 'UniqueId', + 'textEdit': { + 'range': { + 'start': { + 'character': 0, + 'line': 0 + }, + 'end': { + 'character': 1, + 'line': 0 + } + }, + 'newText': '-UniqueId' + }, + 'insertText': '-UniqueId' + } ] edit_before_cursor = [ - dict(label='override def myFunction(): Unit', - textEdit={ - 'newText': 'override def myFunction(): Unit = ${0:???}', - 'range': { - 'start': { - 'line': 0, - 'character': 2 - }, - 'end': { - 'line': 0, - 'character': 18 - } - } - }) + { + 'insertTextFormat': 2, + 'label': 'override def myFunction(): Unit', + 'textEdit': { + 'newText': 'override def myFunction(): Unit = ${0:???}', + 'range': { + 'start': { + 'line': 0, + 'character': 2 + }, + 'end': { + 'line': 0, + 'character': 18 + } + } + } + } ] edit_after_nonword = [ - dict(label='apply[A](xs: A*): List[A]', - textEdit={ - 'newText': 'apply($0)', - 'range': { - 'start': { - 'line': 0, - 'character': 5 - }, - 'end': { - 'line': 0, - 'character': 5 - } - } - }) + { + 'insertTextFormat': 2, + 'label': 'apply[A](xs: A*): List[A]', + 'textEdit': { + 'newText': 'apply($0)', + 'range': { + 'start': { + 'line': 0, + 'character': 5 + }, + 'end': { + 'line': 0, + 'character': 5 + } + } + } + } ] metals_implement_all_members = [ - dict(label='Implement all members', - textEdit={ - 'newText': 'def foo: Int \u003d ${0:???}\n def boo: Int \u003d ${0:???}', - 'range': { - 'start': { - 'line': 0, - 'character': 0 - }, - 'end': { - 'line': 0, - 'character': 1 - } - } - }) + { + 'insertTextFormat': 2, + 'label': 'Implement all members', + 'textEdit': { + 'newText': 'def foo: Int \u003d ${0:???}\n def boo: Int \u003d ${0:???}', + 'range': { + 'start': { + 'line': 0, + 'character': 0 + }, + 'end': { + 'line': 0, + 'character': 1 + } + } + } + } ] @@ -149,7 +184,6 @@ def tearDown(self) -> 'Generator': class QueryCompletionsTests(TextDocumentTestCase): - def init_view_settings(self) -> None: super().init_view_settings() assert self.view @@ -160,62 +194,53 @@ def await_message(self, msg: str) -> 'Generator': yield 500 yield from super().await_message(msg) - def test_simple_label(self) -> 'Generator': - self.set_response('textDocument/completion', label_completions) + def type(self, text: str) -> None: + self.view.run_command('append', {'characters': text}) + self.view.run_command('move_to', {'to': 'eol'}) + def select_completion(self) -> 'Generator': + self.view.run_command('auto_complete') + + yield 100 + self.view.run_command("commit_completion") + + def read_file(self) -> str: + return self.view.substr(sublime.Region(0, self.view.size())) + + def test_simple_label(self) -> 'Generator': handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = label_completions + self.assertIsNotNone(handler) if handler: - # todo: want to test trigger chars instead? - # self.view.run_command('insert', {"characters": '.'}) - result = handler.on_query_completions("", [0]) - - # synchronous response - self.assertTrue(handler.initialized) - self.assertTrue(handler.enabled) - self.assertIsNotNone(result) - items, mask = result - self.assertEquals(len(items), 0) - # self.assertEquals(mask, 0) - - # now wait for server response - yield from self.await_message('textDocument/completion') - self.assertEquals(len(handler.completions), 2) - - # verify insertion works - original_change_count = self.view.change_count() - self.view.run_command("commit_completion") - yield from self.await_view_change(original_change_count + 1) - self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), 'asdf') + self.type("a") + yield from self.select_completion() + + self.assertEquals(self.read_file(), 'asdf') def test_simple_inserttext(self) -> 'Generator': - self.set_response('textDocument/completion', insert_text_completions) handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = insert_text_completions + self.assertIsNotNone(handler) if handler: - handler.on_query_completions("", [0]) - yield from self.await_message('textDocument/completion') - original_change_count = self.view.change_count() - self.view.run_command("commit_completion") - yield from self.await_view_change(original_change_count + 1) + self.type("a") + yield from self.select_completion() + self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), + self.read_file(), insert_text_completions[0]["insertText"]) def test_var_prefix_using_label(self) -> 'Generator': - self.view.run_command('append', {'characters': '$'}) - self.view.run_command('move_to', {'to': 'eol'}) - self.set_response('textDocument/completion', var_completion_using_label) handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = var_completion_using_label + self.assertIsNotNone(handler) if handler: - handler.on_query_completions("", [1]) - yield from self.await_message('textDocument/completion') - original_change_count = self.view.change_count() - self.view.run_command("commit_completion") - yield from self.await_view_change(original_change_count + 2) - self.assertEquals(self.view.substr(sublime.Region(0, self.view.size())), '$what') + self.type("$") + yield from self.select_completion() + + self.assertEquals(self.read_file(), '$what') def test_var_prefix_added_in_insertText(self) -> 'Generator': """ @@ -223,39 +248,42 @@ def test_var_prefix_added_in_insertText(self) -> 'Generator': Powershell: label='true', insertText='$true' (see https://github.com/sublimelsp/LSP/issues/294) """ - self.view.run_command('append', {'characters': '$'}) - self.view.run_command('move_to', {'to': 'eol'}) - self.set_response('textDocument/completion', var_prefix_added_in_insertText) handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = var_prefix_added_in_insertText + self.assertIsNotNone(handler) if handler: - handler.on_query_completions("", [1]) - yield from self.await_message('textDocument/completion') - original_change_count = self.view.change_count() - self.view.run_command("commit_completion") - yield from self.await_view_change(original_change_count + 1) + self.type("$") + yield from self.select_completion() + self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), '$what') + self.read_file(), '$true') - def test_var_prefix_added_in_label(self) -> 'Generator': - """ + # def test_var_prefix_added_in_label(self) -> 'Generator': + # """ - PHP language server: label='$someParam', textEdit='someParam' (https://github.com/sublimelsp/LSP/issues/368) + # PHP language server: label='$someParam', textEdit='someParam' (https://github.com/sublimelsp/LSP/issues/368) + + # """ + # handler = self.get_view_event_listener("on_query_completions") + # handler.test_completions = var_prefix_added_in_label + + # self.assertIsNotNone(handler) + # if handler: + # self.view.run_command('append', {'characters': "$"}) + # self.view.run_command('move_to', {'to': 'eol'}) + + # # show autocomplete + # self.view.run_command('type') + + # # select a completion item + # yield 4000 + # self.view.run_command("commit_completion") + + # yield 4000 + # self.assertEquals( + # self.read_file(), '$what') - """ - self.view.run_command('append', {'characters': '$'}) - self.view.run_command('move_to', {'to': 'eol'}) - self.set_response('textDocument/completion', var_prefix_added_in_label) - handler = self.get_view_event_listener("on_query_completions") - self.assertIsNotNone(handler) - if handler: - handler.on_query_completions("", [1]) - yield from self.await_message('textDocument/completion') - original_change_count = self.view.change_count() - self.view.run_command("commit_completion") - yield from self.await_view_change(original_change_count + 1) - self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), '$what') def test_space_added_in_label(self) -> 'Generator': """ @@ -263,17 +291,16 @@ def test_space_added_in_label(self) -> 'Generator': Clangd: label=" const", insertText="const" (https://github.com/sublimelsp/LSP/issues/368) """ - self.set_response('textDocument/completion', space_added_in_label) handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = space_added_in_label + self.assertIsNotNone(handler) if handler: - handler.on_query_completions("", [0]) - yield from self.await_message('textDocument/completion') - original_change_count = self.view.change_count() - self.view.run_command("commit_completion") - yield from self.await_view_change(original_change_count + 1) + self.type("") + yield from self.select_completion() + self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), 'const') + self.read_file(), 'const') def test_dash_missing_from_label(self) -> 'Generator': """ @@ -281,45 +308,34 @@ def test_dash_missing_from_label(self) -> 'Generator': Powershell: label="UniqueId", insertText="-UniqueId" (https://github.com/sublimelsp/LSP/issues/572) """ - self.view.run_command('append', {'characters': '-'}) - self.view.run_command('move_to', {'to': 'eol'}) - - self.set_response('textDocument/completion', dash_missing_from_label) handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = dash_missing_from_label + self.assertIsNotNone(handler) if handler: - handler.on_query_completions("", [1]) - yield from self.await_message('textDocument/completion') - original_change_count = self.view.change_count() - self.view.run_command("commit_completion") - yield from self.await_view_change(original_change_count + 2) + self.type("u") + yield from self.select_completion() + self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), + self.read_file(), '-UniqueId') - def test_edit_before_cursor(self) -> 'Generator': - """ + # def test_edit_before_cursor(self) -> 'Generator': + # """ - Metals: label="override def myFunction(): Unit" + # Metals: label="override def myFunction(): Unit" - """ - self.view.run_command('append', {'characters': ' def myF'}) - self.view.run_command('move_to', {'to': 'eol'}) + # """ + # handler = self.get_view_event_listener("on_query_completions") + # handler.test_completions = edit_before_cursor - self.set_response('textDocument/completion', edit_before_cursor) - handler = self.get_view_event_listener("on_query_completions") - self.assertIsNotNone(handler) - if handler: - handler.on_query_completions("myF", [7]) - yield from self.await_message('textDocument/completion') - # note: invoking on_text_command manually as sublime doesn't call it. - handler.on_text_command('commit_completion', {}) - original_change_count = self.view.change_count() - self.view.run_command("commit_completion", {}) - yield from self.await_view_change(original_change_count + 3) - self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), - ' override def myFunction(): Unit = ???') + # self.assertIsNotNone(handler) + # if handler: + # self.type(" def myF") + # yield from self.select_completion() + # self.assertEquals( + # self.read_file(), + # ' override def myFunction(): Unit = ???') def test_edit_after_nonword(self) -> 'Generator': """ @@ -328,22 +344,16 @@ def test_edit_after_nonword(self) -> 'Generator': See https://github.com/sublimelsp/LSP/issues/645 """ - self.view.run_command('append', {'characters': 'List.'}) - self.view.run_command('move_to', {'to': 'eol'}) - - self.set_response('textDocument/completion', edit_after_nonword) handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = edit_after_nonword + self.assertIsNotNone(handler) if handler: - handler.on_query_completions("", [5]) - yield from self.await_message('textDocument/completion') - # note: invoking on_text_command manually as sublime doesn't call it. - handler.on_text_command('commit_completion', {}) - original_change_count = self.view.change_count() - self.view.run_command("commit_completion", {}) - yield from self.await_view_change(original_change_count + 1) + self.type("List.") + yield from self.select_completion() + self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), + self.read_file(), 'List.apply()') def test_implement_all_members_quirk(self) -> 'Generator': @@ -351,59 +361,52 @@ def test_implement_all_members_quirk(self) -> 'Generator': Metals: "Implement all members" should just select the newText. https://github.com/sublimelsp/LSP/issues/771 """ - self.view.run_command('append', {'characters': 'I'}) - self.view.run_command('move_to', {'to': 'eol'}) - self.set_response('textDocument/completion', metals_implement_all_members) - handler = self.get_view_event_listener('on_query_completions') + handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = metals_implement_all_members + self.assertIsNotNone(handler) if handler: - handler.on_query_completions("", [1]) - yield from self.await_message('textDocument/completion') - handler.on_text_command('commit_completion', {}) - original_change_count = self.view.change_count() - self.view.run_command("commit_completion", {}) - yield from self.await_view_change(original_change_count + 2) + self.type("I") + yield from self.select_completion() + self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), + self.read_file(), 'def foo: Int = ???\n def boo: Int = ???') def test_additional_edits(self) -> 'Generator': - self.set_response('textDocument/completion', completion_with_additional_edits) handler = self.get_view_event_listener("on_query_completions") - self.assertIsNotNone(handler) - if handler: - handler.on_query_completions("", [0]) - yield from self.await_message('textDocument/completion') - # note: invoking on_text_command manually as sublime doesn't call it. - handler.on_text_command('commit_completion', {}) - original_change_count = self.view.change_count() - self.view.run_command("commit_completion", {}) - yield from self.await_view_change(original_change_count + 2) - self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), - 'import asdf;\nasdf') - - def test_resolve_for_additional_edits(self) -> 'Generator': - self.set_response('textDocument/completion', label_completions) - self.set_response('completionItem/resolve', completion_with_additional_edits[0]) + handler.test_completions = completion_with_additional_edits - handler = self.get_view_event_listener("on_query_completions") self.assertIsNotNone(handler) if handler: - handler.on_query_completions("", [0]) - - # note: ideally the handler is initialized with resolveProvider capability - handler.resolve = True - - yield from self.await_message('textDocument/completion') - # note: invoking on_text_command manually as sublime doesn't call it. - handler.on_text_command('commit_completion', {}) - original_change_count = self.view.change_count() - self.view.run_command("commit_completion", {}) - yield from self.await_view_change(original_change_count + 2) - yield from self.await_message('completionItem/resolve') - yield from self.await_view_change(original_change_count + 2) # XXX: no changes? + self.type("") + yield from self.select_completion() + self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), + self.read_file(), 'import asdf;\nasdf') - handler.resolve = False + + # def test_resolve_for_additional_edits(self) -> 'Generator': + # self.set_response('textDocument/completion', label_completions) + # self.set_response('completionItem/resolve', completion_with_additional_edits[0]) + + # handler = self.get_view_event_listener("on_query_completions") + # self.assertIsNotNone(handler) + # if handler: + # handler.on_query_completions("", [0]) + + # # note: ideally the handler is initialized with resolveProvider capability + # handler.resolve = True + + # yield from self.await_message('textDocument/completion') + # # note: invoking on_text_command manually as sublime doesn't call it. + # handler.on_text_command('commit_completion', {}) + # original_change_count = self.view.change_count() + # self.view.run_command("commit_completion", {}) + # yield from self.await_view_change(original_change_count + 2) + # yield from self.await_message('completionItem/resolve') + # yield from self.await_view_change(original_change_count + 2) # XXX: no changes? + # self.assertEquals( + # self.read_file(), + # 'import asdf;\nasdf') + # handler.resolve = False From 316d29d7cddc4a148ad6de0b6eb2685ef3c2ab9e Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 9 Feb 2020 01:54:36 +0100 Subject: [PATCH 33/62] =?UTF-8?q?Change=20keyword=20icon=20"=CE=BA"=20to?= =?UTF-8?q?=20something=20that=20looks=20like=20a=20key=20=3D>=20"?= =?UTF-8?q?=E2=98=8C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/core/completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/completion.py b/plugin/core/completion.py index 03c32643b..be335efe7 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -16,7 +16,7 @@ 11: (sublime.KIND_ID_VARIABLE, "u", "Unit"), 12: (sublime.KIND_ID_VARIABLE, "ν", "Value"), 13: (sublime.KIND_ID_TYPE, "ε", "Enum"), - 14: (sublime.KIND_ID_KEYWORD, "κ", "Keyword"), + 14: (sublime.KIND_ID_KEYWORD, "☌", "Keyword"), 15: (sublime.KIND_ID_SNIPPET, "s", "Snippet"), 16: (sublime.KIND_ID_AMBIGUOUS, "c", "Color"), 17: (sublime.KIND_ID_AMBIGUOUS, "#", "File"), From 8dd14b35adcac5177b84d97312badcb956d62bb4 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 9 Feb 2020 10:07:14 +0100 Subject: [PATCH 34/62] =?UTF-8?q?Revert=20"Change=20keyword=20icon=20"?= =?UTF-8?q?=CE=BA"=20to=20something=20that=20looks=20like=20a=20key=20=3D>?= =?UTF-8?q?=20"=E2=98=8C""?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 316d29d7cddc4a148ad6de0b6eb2685ef3c2ab9e. --- plugin/core/completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/completion.py b/plugin/core/completion.py index be335efe7..03c32643b 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -16,7 +16,7 @@ 11: (sublime.KIND_ID_VARIABLE, "u", "Unit"), 12: (sublime.KIND_ID_VARIABLE, "ν", "Value"), 13: (sublime.KIND_ID_TYPE, "ε", "Enum"), - 14: (sublime.KIND_ID_KEYWORD, "☌", "Keyword"), + 14: (sublime.KIND_ID_KEYWORD, "κ", "Keyword"), 15: (sublime.KIND_ID_SNIPPET, "s", "Snippet"), 16: (sublime.KIND_ID_AMBIGUOUS, "c", "Color"), 17: (sublime.KIND_ID_AMBIGUOUS, "#", "File"), From f47083e3bf167cb1ed7baf56f394cf7cded1985d Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 9 Feb 2020 11:32:25 +0100 Subject: [PATCH 35/62] Remove if check for document_position --- plugin/completion.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 2e1355475..5dbfc9824 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -166,11 +166,10 @@ def do_request(self, completion_list: sublime.CompletionList, prefix: str, locat self.manager.documents.purge_changes(self.view) document_position = text_document_position_params(self.view, locations[0]) - if document_position: - client.send_request( - Request.complete(document_position), - lambda res: self.handle_response(res, completion_list, prefix), - self.handle_error) + client.send_request( + Request.complete(document_position), + lambda res: self.handle_response(res, completion_list, prefix), + self.handle_error) def handle_response(self, response: Optional[Union[dict, List]], completion_list: sublime.CompletionList, prefix: str) -> None: From 542245ebb16fe8cf823993eb681484772a87697c Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 9 Feb 2020 11:33:07 +0100 Subject: [PATCH 36/62] fix typo --- plugin/core/completion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/core/completion.py b/plugin/core/completion.py index 03c32643b..61a0d9299 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -2,7 +2,7 @@ from .typing import Tuple, Optional, Dict, List, Union -compleiton_kinds = { +completion_kinds = { 1: (sublime.KIND_ID_MARKUP, "Ξ", "Text"), 2: (sublime.KIND_ID_FUNCTION, "λ", "Method"), 3: (sublime.KIND_ID_FUNCTION, "λ", "Function"), @@ -38,7 +38,7 @@ def format_completion(item: dict) -> sublime.CompletionItem: item_kind = item.get("kind") if item_kind: - kind = compleiton_kinds.get(item_kind, sublime.KIND_AMBIGUOUS) + kind = completion_kinds.get(item_kind, sublime.KIND_AMBIGUOUS) is_deprecated = item.get("deprecated", False) if is_deprecated: From 44a3b2a36a87436a34a82b9c2fa2ee9c48492b83 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 15 Feb 2020 17:33:56 +0100 Subject: [PATCH 37/62] erase regoion for snippet, replace region for plain text --- plugin/completion.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 5dbfc9824..ea72c7eec 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -30,11 +30,11 @@ def run(self, edit: Any, item: dict) -> None: range = Range.from_lsp(text_edit['range']) edit_region = range_to_region(range, self.view) - self.view.erase(edit, edit_region) if insert_text_format == InsertTextFormat.Snippet: + self.view.erase(edit, edit_region) self.view.run_command("insert_snippet", {"contents": new_text}) else: - self.view.insert(edit, edit_region.begin(), new_text) + self.view.replace(edit, edit_region, new_text) else: completion = item.get('insertText') or item.get('label') or "" if insert_text_format == InsertTextFormat.Snippet: @@ -187,5 +187,6 @@ def handle_response(self, response: Optional[Union[dict, List]], completion_list.set_completions(items, flags) CompletionHandler.last_prefix = prefix + def handle_error(self, error: dict) -> None: sublime.status_message('Completion error: ' + str(error.get('message'))) From 33496ac70e8353f7ce5fce2ce91d5dbd758dea65 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 15 Feb 2020 17:58:31 +0100 Subject: [PATCH 38/62] don't allocate a dict --- plugin/completion.py | 2 +- plugin/core/completion.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index ea72c7eec..bff962887 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -15,7 +15,7 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): - def run(self, edit: Any, item: dict) -> None: + def run(self, edit: Any, **item: Any) -> None: insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') diff --git a/plugin/core/completion.py b/plugin/core/completion.py index 61a0d9299..f199bd404 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -50,9 +50,7 @@ def format_completion(item: dict) -> sublime.CompletionItem: return sublime.CompletionItem.command_completion( trigger, command="lsp_select_completion_item", - args={ - "item": item - }, + args=item, annotation=annotation, kind=kind ) From 7b154e67c61d0057361c74d344a331d71e23feae Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 15 Feb 2020 18:01:10 +0100 Subject: [PATCH 39/62] remove indent --- plugin/completion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/completion.py b/plugin/completion.py index bff962887..756365d16 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -187,6 +187,5 @@ def handle_response(self, response: Optional[Union[dict, List]], completion_list.set_completions(items, flags) CompletionHandler.last_prefix = prefix - def handle_error(self, error: dict) -> None: sublime.status_message('Completion error: ' + str(error.get('message'))) From c0805be165dca6bc0a1e9fff9efefed66260c7f1 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 15 Feb 2020 18:05:44 +0100 Subject: [PATCH 40/62] fix other failing tests --- tests/test_completion.py | 75 +++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/tests/test_completion.py b/tests/test_completion.py index b16fda03e..811f399ce 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -60,14 +60,14 @@ 'range': { 'start': { 'line': 0, - 'character': 1 + 'character': 0 }, 'end': { 'line': 0, 'character': 1 } }, - 'newText': 'what' + 'newText': '$what' } } ] @@ -213,7 +213,7 @@ def test_simple_label(self) -> 'Generator': self.assertIsNotNone(handler) if handler: - self.type("a") + handler.on_query_completions("a", [0]) yield from self.select_completion() self.assertEquals(self.read_file(), 'asdf') @@ -224,7 +224,7 @@ def test_simple_inserttext(self) -> 'Generator': self.assertIsNotNone(handler) if handler: - self.type("a") + handler.on_query_completions("a", [0]) yield from self.select_completion() self.assertEquals( @@ -259,31 +259,22 @@ def test_var_prefix_added_in_insertText(self) -> 'Generator': self.assertEquals( self.read_file(), '$true') - # def test_var_prefix_added_in_label(self) -> 'Generator': - # """ - - # PHP language server: label='$someParam', textEdit='someParam' (https://github.com/sublimelsp/LSP/issues/368) - - # """ - # handler = self.get_view_event_listener("on_query_completions") - # handler.test_completions = var_prefix_added_in_label - - # self.assertIsNotNone(handler) - # if handler: - # self.view.run_command('append', {'characters': "$"}) - # self.view.run_command('move_to', {'to': 'eol'}) + def test_var_prefix_added_in_label(self) -> 'Generator': + """ - # # show autocomplete - # self.view.run_command('type') + PHP language server: label='$someParam', textEdit='someParam' (https://github.com/sublimelsp/LSP/issues/368) - # # select a completion item - # yield 4000 - # self.view.run_command("commit_completion") + """ + handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = var_prefix_added_in_label - # yield 4000 - # self.assertEquals( - # self.read_file(), '$what') + self.assertIsNotNone(handler) + if handler: + self.type("$") + yield from self.select_completion() + self.assertEquals( + self.read_file(), '$what') def test_space_added_in_label(self) -> 'Generator': """ @@ -297,6 +288,7 @@ def test_space_added_in_label(self) -> 'Generator': self.assertIsNotNone(handler) if handler: self.type("") + handler.on_query_completions("", [0]) yield from self.select_completion() self.assertEquals( @@ -313,29 +305,30 @@ def test_dash_missing_from_label(self) -> 'Generator': self.assertIsNotNone(handler) if handler: - self.type("u") + handler.on_query_completions("u", [0]) yield from self.select_completion() self.assertEquals( self.read_file(), '-UniqueId') - # def test_edit_before_cursor(self) -> 'Generator': - # """ + def test_edit_before_cursor(self) -> 'Generator': + """ - # Metals: label="override def myFunction(): Unit" + Metals: label="override def myFunction(): Unit" - # """ - # handler = self.get_view_event_listener("on_query_completions") - # handler.test_completions = edit_before_cursor + """ + handler = self.get_view_event_listener("on_query_completions") + handler.test_completions = edit_before_cursor - # self.assertIsNotNone(handler) - # if handler: - # self.type(" def myF") - # yield from self.select_completion() - # self.assertEquals( - # self.read_file(), - # ' override def myFunction(): Unit = ???') + self.assertIsNotNone(handler) + if handler: + self.type(' ') + handler.on_query_completions("myF", [7]) + yield from self.select_completion() + self.assertEquals( + self.read_file(), + ' override def myFunction(): Unit = ???') def test_edit_after_nonword(self) -> 'Generator': """ @@ -350,6 +343,7 @@ def test_edit_after_nonword(self) -> 'Generator': self.assertIsNotNone(handler) if handler: self.type("List.") + handler.on_query_completions("", [0]) yield from self.select_completion() self.assertEquals( @@ -367,6 +361,7 @@ def test_implement_all_members_quirk(self) -> 'Generator': self.assertIsNotNone(handler) if handler: self.type("I") + handler.on_query_completions("I", [0]) yield from self.select_completion() self.assertEquals( @@ -379,7 +374,7 @@ def test_additional_edits(self) -> 'Generator': self.assertIsNotNone(handler) if handler: - self.type("") + handler.on_query_completions("", [0]) yield from self.select_completion() self.assertEquals( From b5284755acc6d397fdbe8d929ef03d5e871b202e Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 15 Feb 2020 18:30:21 +0100 Subject: [PATCH 41/62] Use set_response and await_message in favour of test_compmpletion property on CompletionHandler --- plugin/completion.py | 7 +- tests/test_completion.py | 216 +++++++++++++++------------------------ tests/test_mocks.py | 2 +- 3 files changed, 86 insertions(+), 139 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 756365d16..7cbdd69fa 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -92,7 +92,6 @@ def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False self.enabled = False - self.test_completions = None # type: List[dict] @classmethod def is_applicable(cls, view_settings: dict) -> bool: @@ -150,11 +149,7 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su completion_list = sublime.CompletionList() - # this is for tests - if self.test_completions: - self.handle_response(self.test_completions, completion_list, prefix) - else: - self.do_request(completion_list, prefix, locations) + self.do_request(completion_list, prefix, locations) return completion_list diff --git a/tests/test_completion.py b/tests/test_completion.py index 811f399ce..1ff7bdcc2 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -208,39 +208,32 @@ def read_file(self) -> str: return self.view.substr(sublime.Region(0, self.view.size())) def test_simple_label(self) -> 'Generator': - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = label_completions + self.set_response("textDocument/completion", label_completions) - self.assertIsNotNone(handler) - if handler: - handler.on_query_completions("a", [0]) - yield from self.select_completion() + self.type("a") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.assertEquals(self.read_file(), 'asdf') + self.assertEquals(self.read_file(), 'asdf') def test_simple_inserttext(self) -> 'Generator': - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = insert_text_completions + self.set_response("textDocument/completion", insert_text_completions) - self.assertIsNotNone(handler) - if handler: - handler.on_query_completions("a", [0]) - yield from self.select_completion() + self.type("a") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.assertEquals( - self.read_file(), - insert_text_completions[0]["insertText"]) + self.assertEquals( + self.read_file(), + insert_text_completions[0]["insertText"]) def test_var_prefix_using_label(self) -> 'Generator': - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = var_completion_using_label + self.set_response("textDocument/completion", var_completion_using_label) + self.type("$") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.assertIsNotNone(handler) - if handler: - self.type("$") - yield from self.select_completion() - - self.assertEquals(self.read_file(), '$what') + self.assertEquals(self.read_file(), '$what') def test_var_prefix_added_in_insertText(self) -> 'Generator': """ @@ -248,16 +241,13 @@ def test_var_prefix_added_in_insertText(self) -> 'Generator': Powershell: label='true', insertText='$true' (see https://github.com/sublimelsp/LSP/issues/294) """ - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = var_prefix_added_in_insertText - - self.assertIsNotNone(handler) - if handler: - self.type("$") - yield from self.select_completion() + self.set_response("textDocument/completion", var_prefix_added_in_insertText) + self.type("$") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.assertEquals( - self.read_file(), '$true') + self.assertEquals( + self.read_file(), '$true') def test_var_prefix_added_in_label(self) -> 'Generator': """ @@ -265,16 +255,13 @@ def test_var_prefix_added_in_label(self) -> 'Generator': PHP language server: label='$someParam', textEdit='someParam' (https://github.com/sublimelsp/LSP/issues/368) """ - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = var_prefix_added_in_label + self.set_response("textDocument/completion", var_prefix_added_in_label) + self.type("$") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.assertIsNotNone(handler) - if handler: - self.type("$") - yield from self.select_completion() - - self.assertEquals( - self.read_file(), '$what') + self.assertEquals( + self.read_file(), '$what') def test_space_added_in_label(self) -> 'Generator': """ @@ -282,17 +269,12 @@ def test_space_added_in_label(self) -> 'Generator': Clangd: label=" const", insertText="const" (https://github.com/sublimelsp/LSP/issues/368) """ - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = space_added_in_label - - self.assertIsNotNone(handler) - if handler: - self.type("") - handler.on_query_completions("", [0]) - yield from self.select_completion() + self.set_response("textDocument/completion", space_added_in_label) + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.assertEquals( - self.read_file(), 'const') + self.assertEquals( + self.read_file(), 'const') def test_dash_missing_from_label(self) -> 'Generator': """ @@ -300,17 +282,14 @@ def test_dash_missing_from_label(self) -> 'Generator': Powershell: label="UniqueId", insertText="-UniqueId" (https://github.com/sublimelsp/LSP/issues/572) """ - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = dash_missing_from_label + self.set_response("textDocument/completion", dash_missing_from_label) + self.type("u") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.assertIsNotNone(handler) - if handler: - handler.on_query_completions("u", [0]) - yield from self.select_completion() - - self.assertEquals( - self.read_file(), - '-UniqueId') + self.assertEquals( + self.read_file(), + '-UniqueId') def test_edit_before_cursor(self) -> 'Generator': """ @@ -318,17 +297,14 @@ def test_edit_before_cursor(self) -> 'Generator': Metals: label="override def myFunction(): Unit" """ - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = edit_before_cursor - - self.assertIsNotNone(handler) - if handler: - self.type(' ') - handler.on_query_completions("myF", [7]) - yield from self.select_completion() - self.assertEquals( - self.read_file(), - ' override def myFunction(): Unit = ???') + self.set_response("textDocument/completion", edit_before_cursor) + self.type(' omyFu') + yield from self.select_completion() + yield from self.await_message("textDocument/completion") + + self.assertEquals( + self.read_file(), + ' override def myFunction(): Unit = ???') def test_edit_after_nonword(self) -> 'Generator': """ @@ -337,71 +313,47 @@ def test_edit_after_nonword(self) -> 'Generator': See https://github.com/sublimelsp/LSP/issues/645 """ - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = edit_after_nonword - - self.assertIsNotNone(handler) - if handler: - self.type("List.") - handler.on_query_completions("", [0]) - yield from self.select_completion() - - self.assertEquals( - self.read_file(), - 'List.apply()') + self.set_response("textDocument/completion", edit_after_nonword) + self.type("List.") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") + + self.assertEquals( + self.read_file(), + 'List.apply()') def test_implement_all_members_quirk(self) -> 'Generator': """ Metals: "Implement all members" should just select the newText. https://github.com/sublimelsp/LSP/issues/771 """ - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = metals_implement_all_members - - self.assertIsNotNone(handler) - if handler: - self.type("I") - handler.on_query_completions("I", [0]) - yield from self.select_completion() + self.set_response("textDocument/completion", metals_implement_all_members) + self.type("I") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.assertEquals( - self.read_file(), - 'def foo: Int = ???\n def boo: Int = ???') + self.assertEquals( + self.read_file(), + 'def foo: Int = ???\n def boo: Int = ???') def test_additional_edits(self) -> 'Generator': - handler = self.get_view_event_listener("on_query_completions") - handler.test_completions = completion_with_additional_edits - - self.assertIsNotNone(handler) - if handler: - handler.on_query_completions("", [0]) - yield from self.select_completion() - - self.assertEquals( - self.read_file(), - 'import asdf;\nasdf') - - # def test_resolve_for_additional_edits(self) -> 'Generator': - # self.set_response('textDocument/completion', label_completions) - # self.set_response('completionItem/resolve', completion_with_additional_edits[0]) - - # handler = self.get_view_event_listener("on_query_completions") - # self.assertIsNotNone(handler) - # if handler: - # handler.on_query_completions("", [0]) - - # # note: ideally the handler is initialized with resolveProvider capability - # handler.resolve = True - - # yield from self.await_message('textDocument/completion') - # # note: invoking on_text_command manually as sublime doesn't call it. - # handler.on_text_command('commit_completion', {}) - # original_change_count = self.view.change_count() - # self.view.run_command("commit_completion", {}) - # yield from self.await_view_change(original_change_count + 2) - # yield from self.await_message('completionItem/resolve') - # yield from self.await_view_change(original_change_count + 2) # XXX: no changes? - # self.assertEquals( - # self.read_file(), - # 'import asdf;\nasdf') - # handler.resolve = False + self.set_response("textDocument/completion", completion_with_additional_edits) + + yield from self.select_completion() + yield from self.await_message("textDocument/completion") + + self.assertEquals( + self.read_file(), + 'import asdf;\nasdf') + + def test_resolve_for_additional_edits(self) -> 'Generator': + self.set_response('textDocument/completion', label_completions) + self.set_response('completionItem/resolve', completion_with_additional_edits[0]) + + yield from self.select_completion() + yield from self.await_message('textDocument/completion') + yield from self.await_message('completionItem/resolve') + + self.assertEquals( + self.read_file(), + 'import asdf;\nasdf') diff --git a/tests/test_mocks.py b/tests/test_mocks.py index 587edfa00..29d54393d 100644 --- a/tests/test_mocks.py +++ b/tests/test_mocks.py @@ -24,7 +24,7 @@ 'hoverProvider': True, 'completionProvider': { 'triggerCharacters': ['.'], - 'resolveProvider': False + 'resolveProvider': True }, 'textDocumentSync': { "openClose": True, From 2497a450af156980b0441becb8efb71f9d9a55d5 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 17 Feb 2020 14:17:06 +0100 Subject: [PATCH 42/62] Send a resolve request only if we don't have apply_additional_edits on the completion item Ideally we would send the resolveCompletionItem request if it higlighted in the autocomplete popup, but currently there is no api for that. For now we use the resove completion item just to apply additional_text_edits after the completion item is selected. --- plugin/completion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 7cbdd69fa..ed458e709 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -47,8 +47,8 @@ def run(self, edit: Any, **item: Any) -> None: additional_edits = item.get('additionalTextEdits') if additional_edits: self.apply_additional_edits(additional_edits) - - self.do_resolve(item) + else: + self.do_resolve(item) def do_resolve(self, item: dict) -> None: session = session_for_view(self.view, 'completionProvider', self.view.sel()[0].begin()) From a99cff76eb97050210b81eb4634dc00c7d36da72 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 17 Feb 2020 22:34:31 +0100 Subject: [PATCH 43/62] Add E2E tests for prefering insert text over label and text edit over insert text Some tests in test_completions_core like: - test_text_edit_clangd - test_text_edit_intelephense - test_use_label_as_is are removed becasuse they are not relevant anymore tests like - test_only_label_item - test_prefers_insert_text are moved to test_completions and are tested in a E2E test fashion --- tests/test_completion.py | 47 +++++++++++- tests/test_completion_core.py | 138 ---------------------------------- 2 files changed, 44 insertions(+), 141 deletions(-) diff --git a/tests/test_completion.py b/tests/test_completion.py index 1ff7bdcc2..9ab3a1892 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -11,7 +11,32 @@ except ImportError: pass -label_completions = [{'label': 'asdf'}, {'label': 'efcgh'}] +completions_with_label = [{'label': 'asdf'}, {'label': 'efcgh'}] +completions_with_label_and_insert_text = [ + { + "label": "Label text", + "insertText": "Insert text" + } +] +completions_with_label_and_insert_text_and_text_edit = [ + { + "label": "Label text", + "insertText": "Insert text", + "textEdit": { + "newText": "Text edit", + "range": { + "end": { + "character": 5, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + } +] completion_with_additional_edits = [ { 'label': 'asdf', @@ -208,7 +233,7 @@ def read_file(self) -> str: return self.view.substr(sublime.Region(0, self.view.size())) def test_simple_label(self) -> 'Generator': - self.set_response("textDocument/completion", label_completions) + self.set_response("textDocument/completion", completions_with_label) self.type("a") yield from self.select_completion() @@ -216,6 +241,22 @@ def test_simple_label(self) -> 'Generator': self.assertEquals(self.read_file(), 'asdf') + def test_prefer_insert_text_over_label(self) -> 'Generator': + self.set_response("textDocument/completion", completions_with_label_and_insert_text) + + yield from self.select_completion() + yield from self.await_message("textDocument/completion") + + self.assertEquals(self.read_file(), 'Insert text') + + def test_prefer_text_edit_over_insert_text(self) -> 'Generator': + self.set_response("textDocument/completion", completions_with_label_and_insert_text_and_text_edit) + + yield from self.select_completion() + yield from self.await_message("textDocument/completion") + + self.assertEquals(self.read_file(), 'Text edit') + def test_simple_inserttext(self) -> 'Generator': self.set_response("textDocument/completion", insert_text_completions) @@ -347,7 +388,7 @@ def test_additional_edits(self) -> 'Generator': 'import asdf;\nasdf') def test_resolve_for_additional_edits(self) -> 'Generator': - self.set_response('textDocument/completion', label_completions) + self.set_response('textDocument/completion', completions_with_label) self.set_response('completionItem/resolve', completion_with_additional_edits[0]) yield from self.select_completion() diff --git a/tests/test_completion_core.py b/tests/test_completion_core.py index 54fb1531a..0d79fdc23 100644 --- a/tests/test_completion_core.py +++ b/tests/test_completion_core.py @@ -32,141 +32,3 @@ def test_dict_response(self): def test_incomplete_dict_response(self): self.assertEqual(parse_completion_response({'items': [], 'isIncomplete': True}), ([], True)) - -class CompletionFormattingTests(unittest.TestCase): - - def test_only_label_item(self): - result = format_completion({"label": "asdf"}) - self.assertEqual(len(result), 2) - self.assertEqual("asdf", result[0]) - self.assertEqual("asdf", result[1]) - - def test_prefers_insert_text(self): - result = format_completion({"label": "asdf", "insertText": "Asdf"}) - self.assertEqual(len(result), 2) - self.assertEqual("asdf", result[0]) - self.assertEqual("Asdf", result[1]) - - def test_ignores_text_edit(self): - - # 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 = format_completion(item) - self.assertEqual(len(result), 2) - self.assertEqual("$true", result[0]) - self.assertEqual("\\$true", result[1]) - - def test_use_label_as_is(self): - # issue #368 - item = { - 'insertTextFormat': 2, - # insertText is present, but we should prefer textEdit instead. - 'insertText': 'const', - 'sortText': '3f800000const', - 'kind': 14, - # Note the extra space here. - 'label': ' const', - 'textEdit': { - 'newText': 'const', - 'range': { - # Replace the single character that triggered the completion request. - 'end': {'character': 13, 'line': 6}, - 'start': {'character': 12, 'line': 6} - } - } - } - result = format_completion(item) - self.assertEqual(result, ('const\t Keyword', 'const')) - - def test_text_edit_intelephense(self): - result = [format_completion(item) for item in intelephense_completion_sample] - self.assertEqual( - result, - [ - ('$x\t mixed', '\\$x'), - ('$_ENV\t array', '\\$_ENV'), - ('$php_errormsg\t string', '\\$php_errormsg'), - ('$_FILES\t array', '\\$_FILES'), - ('$GLOBALS\t array', '\\$GLOBALS'), - ('$argc\t int', '\\$argc'), - ('$argv\t array', '\\$argv'), - ('$_GET\t array', '\\$_GET'), - ('$HTTP_RAW_POST_DATA\t string', '\\$HTTP_RAW_POST_DATA'), - ('$http_response_header\t array', '\\$http_response_header'), - ('$_POST\t array', '\\$_POST'), - ('$_REQUEST\t array', '\\$_REQUEST'), - ('$_SERVER\t array', '\\$_SERVER'), - ('$_SESSION\t array', '\\$_SESSION'), - ('$_COOKIE\t array', '\\$_COOKIE'), - ('$this\t Variable', '\\$this') - ] - ) - - def test_text_edit_clangd(self): - # handler.last_location = 1 - # handler.last_prefix = "" - result = [format_completion(item) for item in clangd_completion_sample] - # We should prefer textEdit over insertText. This test covers that. - self.assertEqual( - result, [('argc\t int', 'argc'), ('argv\t const char **', 'argv'), - ('alignas(expression)\t Snippet', 'alignas(${1:expression})'), - ('alignof(type)\t size_t', 'alignof(${1:type})'), ('auto\t Keyword', 'auto'), - ('static_assert(expression, message)\t Snippet', 'static_assert(${1:expression}, ${2:message})'), - ('a64l(const char *__s)\t long', 'a64l(${1:const char *__s})'), ('abort()\t void', 'abort()'), - ('abs(int __x)\t int', 'abs(${1:int __x})'), - ('aligned_alloc(size_t __alignment, size_t __size)\t void *', - 'aligned_alloc(${1:size_t __alignment}, ${2:size_t __size})'), - ('alloca(size_t __size)\t void *', 'alloca(${1:size_t __size})'), - ('asctime(const struct tm *__tp)\t char *', 'asctime(${1:const struct tm *__tp})'), - ('asctime_r(const struct tm *__restrict __tp, char *__restrict __buf)\t char *', - 'asctime_r(${1:const struct tm *__restrict __tp}, ${2:char *__restrict __buf})'), - ('asprintf(char **__restrict __ptr, const char *__restrict __fmt, ...)\t int', - 'asprintf(${1:char **__restrict __ptr}, ${2:const char *__restrict __fmt, ...})'), - ('at_quick_exit(void (*__func)())\t int', 'at_quick_exit(${1:void (*__func)()})'), - ('atexit(void (*__func)())\t int', 'atexit(${1:void (*__func)()})'), - ('atof(const char *__nptr)\t double', 'atof(${1:const char *__nptr})'), - ('atoi(const char *__nptr)\t int', 'atoi(${1:const char *__nptr})'), - ('atol(const char *__nptr)\t long', 'atol(${1:const char *__nptr})')]) - - def test_missing_text_edit_but_we_do_have_insert_text_for_pyls(self): - result = [format_completion(item) for item in pyls_completion_sample] - self.assertEqual( - result, - [ - ('abc\t os', 'abc'), - ('abort()\t os', 'abort'), - ('access(path, mode, dir_fd, effective_ids, follow_symlinks)\t os', - 'access(${1:path}, ${2:mode}, ${3:dir_fd}, ${4:effective_ids}, ${5:follow_symlinks})$0'), - ('altsep\t os', 'altsep'), - ('chdir(path)\t os', 'chdir(${1:path})$0'), - ('chmod(path, mode, dir_fd, follow_symlinks)\t os', - 'chmod(${1:path}, ${2:mode}, ${3:dir_fd}, ${4:follow_symlinks})$0'), - ('chown(path, uid, gid, dir_fd, follow_symlinks)\t os', - 'chown(${1:path}, ${2:uid}, ${3:gid}, ${4:dir_fd}, ${5:follow_symlinks})$0'), - ('chroot(path)\t os', 'chroot(${1:path})$0'), - ('CLD_CONTINUED\t os', 'CLD_CONTINUED'), - ('CLD_DUMPED\t os', 'CLD_DUMPED'), - ('CLD_EXITED\t os', 'CLD_EXITED'), - ('CLD_TRAPPED\t os', 'CLD_TRAPPED'), - ('close(fd)\t os', 'close(${1:fd})$0'), - ('closerange(fd_low, fd_high)\t os', 'closerange(${1:fd_low}, ${2:fd_high})$0'), - ('confstr(name)\t os', 'confstr(${1:name})$0'), - ('confstr_names\t os', 'confstr_names'), - ('cpu_count()\t os', 'cpu_count'), - ('ctermid()\t os', 'ctermid'), - ('curdir\t os', 'curdir'), - ('defpath\t os', 'defpath'), - ('device_encoding(fd)\t os', 'device_encoding(${1:fd})$0') - ] - ) From 49ccda6428d6c54a2535fb06e4fcbdc2a69337c3 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 17 Feb 2020 22:56:37 +0100 Subject: [PATCH 44/62] add test to apply additional edits only one time --- tests/test_completion.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_completion.py b/tests/test_completion.py index 9ab3a1892..54add3e77 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -398,3 +398,14 @@ def test_resolve_for_additional_edits(self) -> 'Generator': self.assertEquals( self.read_file(), 'import asdf;\nasdf') + + def test_apply_additional_edits_only_once(self) -> 'Generator': + self.set_response('textDocument/completion', completion_with_additional_edits) + self.set_response('completionItem/resolve', completion_with_additional_edits[0]) + + yield from self.select_completion() + yield from self.await_message('textDocument/completion') + + self.assertEquals( + self.read_file(), + 'import asdf;\nasdf') From cc475e95c806a6ead3db46066ba7f81a50ead620 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 17 Feb 2020 23:00:59 +0100 Subject: [PATCH 45/62] set "auto_complete_preserve_order" to "none" in tests --- tests/setup.py | 3 +++ tests/test_completion.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/setup.py b/tests/setup.py index 7b1b768bb..a56994d49 100644 --- a/tests/setup.py +++ b/tests/setup.py @@ -174,6 +174,9 @@ def init_view_settings(self) -> None: s("tab_size", 4) s("translate_tabs_to_spaces", False) s("word_wrap", False) + # ST4 removes completion items when "auto_complete_preserve_order" is not "none", + # see https://github.com/sublimelsp/LSP/pull/866#discussion_r380249385 + s("auto_complete_preserve_order", "none") def get_view_event_listener(self, unique_attribute: str) -> 'Optional[ViewEventListener]': for listener in view_event_listeners[self.view.id()]: diff --git a/tests/test_completion.py b/tests/test_completion.py index 54add3e77..7f804023b 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -339,7 +339,7 @@ def test_edit_before_cursor(self) -> 'Generator': """ self.set_response("textDocument/completion", edit_before_cursor) - self.type(' omyFu') + self.type(' def myF') yield from self.select_completion() yield from self.await_message("textDocument/completion") From 61099bcfe4ffc55535dfa0d53b0d9a50115d6329 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 17 Feb 2020 23:22:21 +0100 Subject: [PATCH 46/62] make mypy and pyflake happy --- stubs/sublime.pyi | 8 +++++++- tests/test_completion.py | 2 +- tests/test_completion_core.py | 3 +-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/stubs/sublime.pyi b/stubs/sublime.pyi index b5315204d..efb71d952 100644 --- a/stubs/sublime.pyi +++ b/stubs/sublime.pyi @@ -2,7 +2,7 @@ # # NOTE: This dynamically typed stub was automatically generated by stubgen. -from typing import Any, Optional, Callable, Sequence, Tuple, Union, List, Sized +from typing import Any, Optional, Callable, Sequence, Tuple, Union, List, Sized, Iterator class _LogWriter: @@ -513,6 +513,12 @@ class Selection(Sized): def __len__(self) -> int: ... + def __iter__(self) -> Iterator[Region]: + ... + + def __next__(self) -> Region: + ... + def __getitem__(self, index: int) -> Region: ... diff --git a/tests/test_completion.py b/tests/test_completion.py index 7f804023b..0c0232a09 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -358,7 +358,7 @@ def test_edit_after_nonword(self) -> 'Generator': self.type("List.") yield from self.select_completion() yield from self.await_message("textDocument/completion") - + self.assertEquals( self.read_file(), 'List.apply()') diff --git a/tests/test_completion_core.py b/tests/test_completion_core.py index 0d79fdc23..ec470c571 100644 --- a/tests/test_completion_core.py +++ b/tests/test_completion_core.py @@ -1,7 +1,7 @@ import unittest from os import path import json -from LSP.plugin.core.completion import format_completion, parse_completion_response +from LSP.plugin.core.completion import parse_completion_response try: from typing import Optional, Dict assert Optional and Dict @@ -31,4 +31,3 @@ def test_dict_response(self): def test_incomplete_dict_response(self): self.assertEqual(parse_completion_response({'items': [], 'isIncomplete': True}), ([], True)) - From 9e54c832c6b31082f02d1608ecec61aad18a7deb Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 20 Feb 2020 18:48:15 +0100 Subject: [PATCH 47/62] Remove "complete_all_chars" setting in favor of sublime default "auto_complete" setting --- LSP.sublime-settings | 4 ---- docs/features.md | 1 - plugin/core/settings.py | 1 - plugin/core/types.py | 1 - 4 files changed, 7 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index d48bcebbd..e02cac4cf 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -58,10 +58,6 @@ // Show symbol action links in hover popup if available "show_symbol_action_links": false, - // Request completions for all characters if set to true, - // or just after trigger characters only otherwise. - "complete_all_chars": true, - // Disable Sublime Text's explicit and word completion. "only_show_lsp_completions": false, diff --git a/docs/features.md b/docs/features.md index e60c0600d..9ae95e764 100644 --- a/docs/features.md +++ b/docs/features.md @@ -70,7 +70,6 @@ Add these settings to your Sublime settings, Syntax-specific settings and/or in ### Package settings (LSP) -* `complete_all_chars` `true` *request completions for all characters, not just trigger characters* * `only_show_lsp_completions` `false` *disable sublime word completion and snippets from autocomplete lists* * `show_references_in_quick_panel` `false` *show symbol references in Sublime's quick panel instead of the bottom panel* * `show_view_status` `true` *show permanent language server status in the status bar* diff --git a/plugin/core/settings.py b/plugin/core/settings.py index ad33b0807..d409d5f13 100644 --- a/plugin/core/settings.py +++ b/plugin/core/settings.py @@ -75,7 +75,6 @@ def update_settings(settings: Settings, settings_obj: sublime.Settings) -> None: settings.show_code_actions_bulb = read_bool_setting(settings_obj, "show_code_actions_bulb", False) settings.show_symbol_action_links = read_bool_setting(settings_obj, "show_symbol_action_links", False) 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.show_references_in_quick_panel = read_bool_setting(settings_obj, "show_references_in_quick_panel", False) settings.disabled_capabilities = read_array_setting(settings_obj, "disabled_capabilities", []) settings.initialize_timeout = read_int_setting(settings_obj, "initialize_timeout", 3) diff --git a/plugin/core/types.py b/plugin/core/types.py index 6568a5076..594f7c040 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -22,7 +22,6 @@ def __init__(self) -> None: self.diagnostics_gutter_marker = "dot" self.show_code_actions_bulb = False self.show_symbol_action_links = False - self.complete_all_chars = False self.show_references_in_quick_panel = False self.disabled_capabilities = [] # type: List[str] self.initialize_timeout = 3 From 6fed5d3fa8dc81e0d79839260fbfed553eb3ad90 Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 20 Feb 2020 20:08:29 +0100 Subject: [PATCH 48/62] Add test for intelephense --- tests/intelephense_completion_sample.json | 365 ---------------------- tests/intelephense_completion_sample.py | 13 + tests/test_completion.py | 21 ++ 3 files changed, 34 insertions(+), 365 deletions(-) delete mode 100644 tests/intelephense_completion_sample.json create mode 100644 tests/intelephense_completion_sample.py diff --git a/tests/intelephense_completion_sample.json b/tests/intelephense_completion_sample.json deleted file mode 100644 index 578103373..000000000 --- a/tests/intelephense_completion_sample.json +++ /dev/null @@ -1,365 +0,0 @@ -[ - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$x" - }, - "label": "$x", - "detail": "mixed", - "data": { - "fqsenHash": 1236, - "uriId": 158 - }, - "kind": 6, - "sortText": "$x" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$_ENV" - }, - "label": "$_ENV", - "detail": "array", - "data": { - "fqsenHash": 36145714, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$_ENV" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$php_errormsg" - }, - "label": "$php_errormsg", - "detail": "string", - "data": { - "fqsenHash": -1647588988, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$php_errormsg" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$_FILES" - }, - "label": "$_FILES", - "detail": "array", - "data": { - "fqsenHash": 377059964, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$_FILES" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$GLOBALS" - }, - "label": "$GLOBALS", - "detail": "array", - "data": { - "fqsenHash": -844280724, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$GLOBALS" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$argc" - }, - "label": "$argc", - "detail": "int", - "data": { - "fqsenHash": 36249329, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$argc" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$argv" - }, - "label": "$argv", - "detail": "array", - "data": { - "fqsenHash": 36249348, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$argv" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$_GET" - }, - "label": "$_GET", - "detail": "array", - "data": { - "fqsenHash": 36147355, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$_GET" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$HTTP_RAW_POST_DATA" - }, - "label": "$HTTP_RAW_POST_DATA", - "detail": "string", - "data": { - "fqsenHash": 1354318879, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$HTTP_RAW_POST_DATA" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$http_response_header" - }, - "label": "$http_response_header", - "detail": "array", - "data": { - "fqsenHash": 985564344, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$http_response_header" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$_POST" - }, - "label": "$_POST", - "detail": "array", - "data": { - "fqsenHash": 1120845787, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$_POST" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$_REQUEST" - }, - "label": "$_REQUEST", - "detail": "array", - "data": { - "fqsenHash": -766918316, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$_REQUEST" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$_SERVER" - }, - "label": "$_SERVER", - "detail": "array", - "data": { - "fqsenHash": -827363394, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$_SERVER" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$_SESSION" - }, - "label": "$_SESSION", - "detail": "array", - "data": { - "fqsenHash": 122376539, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$_SESSION" - }, - { - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$_COOKIE" - }, - "label": "$_COOKIE", - "detail": "array", - "data": { - "fqsenHash": -1276294433, - "uriId": 82 - }, - "kind": 6, - "sortText": "z$_COOKIE" - }, - { - "label": "$this", - "sortText": "$this", - "kind": 6, - "textEdit": { - "range": { - "end": { - "character": 6, - "line": 2 - }, - "start": { - "character": 5, - "line": 2 - } - }, - "newText": "$this" - } - } -] diff --git a/tests/intelephense_completion_sample.py b/tests/intelephense_completion_sample.py new file mode 100644 index 000000000..1597b7b88 --- /dev/null +++ b/tests/intelephense_completion_sample.py @@ -0,0 +1,13 @@ +intelephense_before_state = """ +""" + +intelephense_expected_state = """ +""" + +intelephense_response = {"items": [{"label": "$hello", "textEdit": {"newText": "$hello", "range": {"end": {"line": 2, "character": 3}, "start": {"line": 2, "character": 0}}}, "data": 2369386987913238, "detail": "int", "kind": 6, "sortText": "$hello"}], "isIncomplete": False} diff --git a/tests/test_completion.py b/tests/test_completion.py index 0c0232a09..2cfae412c 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -4,6 +4,7 @@ from unittesting import DeferrableTestCase import sublime +from LSP.tests.intelephense_completion_sample import intelephense_before_state, intelephense_expected_state, intelephense_response try: from typing import Dict, Optional, List, Generator @@ -223,6 +224,13 @@ def type(self, text: str) -> None: self.view.run_command('append', {'characters': text}) self.view.run_command('move_to', {'to': 'eol'}) + def move_cursor(self, row: int, col: int) -> None: + point = self.view.text_point(row, col) + # move cursor to point + s = self.view.sel() + s.clear() + s.add(point) + def select_completion(self) -> 'Generator': self.view.run_command('auto_complete') @@ -409,3 +417,16 @@ def test_apply_additional_edits_only_once(self) -> 'Generator': self.assertEquals( self.read_file(), 'import asdf;\nasdf') + + def test__prefix_should_include_the_dollar_sign(self): + self.set_response('textDocument/completion', intelephense_response) + + self.type(intelephense_before_state) + # move cursor after `$he|` + self.move_cursor(2, 3) + yield from self.select_completion() + yield from self.await_message('textDocument/completion') + + self.assertEquals( + self.read_file(), + intelephense_expected_state) From dae5eba5bc9a1ea8f09125867b7781a4debd68ab Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 20 Feb 2020 20:35:34 +0100 Subject: [PATCH 49/62] remove old completion samples --- tests/clangd_completion_sample.json | 417 ---------------------------- tests/pyls_completion_sample.json | 179 ------------ tests/test_completion_core.py | 9 - 3 files changed, 605 deletions(-) delete mode 100644 tests/clangd_completion_sample.json delete mode 100644 tests/pyls_completion_sample.json diff --git a/tests/clangd_completion_sample.json b/tests/clangd_completion_sample.json deleted file mode 100644 index c68e0c258..000000000 --- a/tests/clangd_completion_sample.json +++ /dev/null @@ -1,417 +0,0 @@ -[ - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "argc" - }, - "filterText": "argc", - "insertText": "argc", - "label": "argc", - "detail": "int", - "kind": 6, - "sortText": "3e2cccccargc" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "argv" - }, - "filterText": "argv", - "insertText": "argv", - "label": "argv", - "detail": "const char **", - "kind": 6, - "sortText": "3e2cccccargv" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "alignas(${1:expression})" - }, - "filterText": "alignas", - "insertText": "alignas(${1:expression})", - "label": "alignas(expression)", - "kind": 15, - "sortText": "3f800000alignas" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "alignof(${1:type})" - }, - "filterText": "alignof", - "insertText": "alignof(${1:type})", - "label": "alignof(type)", - "detail": "size_t", - "kind": 15, - "sortText": "3f800000alignof" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "auto" - }, - "filterText": "auto", - "insertText": "auto", - "label": "auto", - "kind": 14, - "sortText": "3f800000auto" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "static_assert(${1:expression}, ${2:message})" - }, - "filterText": "static_assert", - "insertText": "static_assert(${1:expression}, ${2:message})", - "label": "static_assert(expression, message)", - "kind": 15, - "sortText": "40555555static_assert" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "a64l(${1:const char *__s})" - }, - "filterText": "a64l", - "insertText": "a64l(${1:const char *__s})", - "label": "a64l(const char *__s)", - "detail": "long", - "kind": 3, - "sortText": "40a7b70ba64l" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "abort()" - }, - "filterText": "abort", - "insertText": "abort()", - "label": "abort()", - "detail": "void", - "kind": 3, - "sortText": "40a7b70babort" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "abs(${1:int __x})" - }, - "filterText": "abs", - "insertText": "abs(${1:int __x})", - "label": "abs(int __x)", - "detail": "int", - "kind": 3, - "sortText": "40a7b70babs" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "aligned_alloc(${1:size_t __alignment}, ${2:size_t __size})" - }, - "filterText": "aligned_alloc", - "insertText": "aligned_alloc(${1:size_t __alignment}, ${2:size_t __size})", - "label": "aligned_alloc(size_t __alignment, size_t __size)", - "detail": "void *", - "kind": 3, - "sortText": "40a7b70baligned_alloc" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "alloca(${1:size_t __size})" - }, - "filterText": "alloca", - "insertText": "alloca(${1:size_t __size})", - "label": "alloca(size_t __size)", - "detail": "void *", - "kind": 3, - "sortText": "40a7b70balloca" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "asctime(${1:const struct tm *__tp})" - }, - "filterText": "asctime", - "insertText": "asctime(${1:const struct tm *__tp})", - "label": "asctime(const struct tm *__tp)", - "detail": "char *", - "kind": 3, - "sortText": "40a7b70basctime" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "asctime_r(${1:const struct tm *__restrict __tp}, ${2:char *__restrict __buf})" - }, - "filterText": "asctime_r", - "insertText": "asctime_r(${1:const struct tm *__restrict __tp}, ${2:char *__restrict __buf})", - "label": "asctime_r(const struct tm *__restrict __tp, char *__restrict __buf)", - "detail": "char *", - "kind": 3, - "sortText": "40a7b70basctime_r" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "asprintf(${1:char **__restrict __ptr}, ${2:const char *__restrict __fmt, ...})" - }, - "filterText": "asprintf", - "insertText": "asprintf(${1:char **__restrict __ptr}, ${2:const char *__restrict __fmt, ...})", - "label": "asprintf(char **__restrict __ptr, const char *__restrict __fmt, ...)", - "detail": "int", - "kind": 3, - "sortText": "40a7b70basprintf" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "at_quick_exit(${1:void (*__func)()})" - }, - "filterText": "at_quick_exit", - "insertText": "at_quick_exit(${1:void (*__func)()})", - "label": "at_quick_exit(void (*__func)())", - "detail": "int", - "kind": 3, - "sortText": "40a7b70bat_quick_exit" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "atexit(${1:void (*__func)()})" - }, - "filterText": "atexit", - "insertText": "atexit(${1:void (*__func)()})", - "label": "atexit(void (*__func)())", - "detail": "int", - "kind": 3, - "sortText": "40a7b70batexit" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "atof(${1:const char *__nptr})" - }, - "filterText": "atof", - "insertText": "atof(${1:const char *__nptr})", - "label": "atof(const char *__nptr)", - "detail": "double", - "kind": 3, - "sortText": "40a7b70batof" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "atoi(${1:const char *__nptr})" - }, - "filterText": "atoi", - "insertText": "atoi(${1:const char *__nptr})", - "label": "atoi(const char *__nptr)", - "detail": "int", - "kind": 3, - "sortText": "40a7b70batoi" - }, - { - "insertTextFormat": 2, - "textEdit": { - "range": { - "end": { - "character": 5, - "line": 2 - }, - "start": { - "character": 4, - "line": 2 - } - }, - "newText": "atol(${1:const char *__nptr})" - }, - "filterText": "atol", - "insertText": "atol(${1:const char *__nptr})", - "label": "atol(const char *__nptr)", - "detail": "long", - "kind": 3, - "sortText": "40a7b70batol" - } -] diff --git a/tests/pyls_completion_sample.json b/tests/pyls_completion_sample.json deleted file mode 100644 index bcc852232..000000000 --- a/tests/pyls_completion_sample.json +++ /dev/null @@ -1,179 +0,0 @@ -[ - { - "documentation": "", - "label": "abc", - "insertText": "abc", - "detail": "os", - "sortText": "aabc", - "kind": 9 - }, - { - "documentation": "Abort the interpreter immediately. This function 'dumps core' or otherwise fails in the hardest way possible on the hosting operating system. This function never returns.", - "label": "abort()", - "insertText": "abort", - "detail": "os", - "sortText": "aabort", - "kind": 3 - }, - { - "documentation": "Use the real uid/gid to test for access to a path. path Path to be tested; can be string or bytes mode Operating-system mode bitfield. Can be F_OK to test existence, or the inclusive-OR of R_OK, W_OK, and X_OK. dir_fd If not None, it should be a file descriptor open to a directory, and path should be relative; path will then be relative to that directory. effective_ids If True, access will use the effective uid/gid instead of the real uid/gid. follow_symlinks If False, and the last element of the path is a symbolic link, access will examine the symbolic link itself instead of the file the link points to. dir_fd, effective_ids, and follow_symlinks may not be implemented on your platform. If they are unavailable, using them will raise a NotImplementedError. Note that most operations will use the effective uid/gid, therefore this routine can be used in a suid/sgid environment to test if the invoking user has the specified access to the path.", - "label": "access(path, mode, dir_fd, effective_ids, follow_symlinks)", - "insertText": "access(${1:path}, ${2:mode}, ${3:dir_fd}, ${4:effective_ids}, ${5:follow_symlinks})$0", - "detail": "os", - "sortText": "aaccess", - "insertTextFormat": 2, - "kind": 3 - }, - { - "documentation": "", - "label": "altsep", - "insertText": "altsep", - "detail": "os", - "sortText": "aaltsep", - "kind": 9 - }, - { - "documentation": "Change the current working directory to the specified path. path may always be specified as a string. On some platforms, path may also be specified as an open file descriptor. If this functionality is unavailable, using it raises an exception.", - "label": "chdir(path)", - "insertText": "chdir(${1:path})$0", - "detail": "os", - "sortText": "achdir", - "insertTextFormat": 2, - "kind": 3 - }, - { - "documentation": "Change the access permissions of a file. path Path to be modified. May always be specified as a str or bytes. On some platforms, path may also be specified as an open file descriptor. If this functionality is unavailable, using it raises an exception. mode Operating-system mode bitfield. dir_fd If not None, it should be a file descriptor open to a directory, and path should be relative; path will then be relative to that directory. follow_symlinks If False, and the last element of the path is a symbolic link, chmod will modify the symbolic link itself instead of the file the link points to. It is an error to use dir_fd or follow_symlinks when specifying path as an open file descriptor. dir_fd and follow_symlinks may not be implemented on your platform. If they are unavailable, using them will raise a NotImplementedError.", - "label": "chmod(path, mode, dir_fd, follow_symlinks)", - "insertText": "chmod(${1:path}, ${2:mode}, ${3:dir_fd}, ${4:follow_symlinks})$0", - "detail": "os", - "sortText": "achmod", - "insertTextFormat": 2, - "kind": 3 - }, - { - "documentation": "Change the owner and group id of path to the numeric uid and gid. path Path to be examined; can be string, bytes, or open-file-descriptor int. dir_fd If not None, it should be a file descriptor open to a directory, and path should be relative; path will then be relative to that directory. follow_symlinks If False, and the last element of the path is a symbolic link, stat will examine the symbolic link itself instead of the file the link points to. path may always be specified as a string. On some platforms, path may also be specified as an open file descriptor. If this functionality is unavailable, using it raises an exception. If dir_fd is not None, it should be a file descriptor open to a directory, and path should be relative; path will then be relative to that directory. If follow_symlinks is False, and the last element of the path is a symbolic link, chown will modify the symbolic link itself instead of the file the link points to. It is an error to use dir_fd or follow_symlinks when specifying path as an open file descriptor. dir_fd and follow_symlinks may not be implemented on your platform. If they are unavailable, using them will raise a NotImplementedError.", - "label": "chown(path, uid, gid, dir_fd, follow_symlinks)", - "insertText": "chown(${1:path}, ${2:uid}, ${3:gid}, ${4:dir_fd}, ${5:follow_symlinks})$0", - "detail": "os", - "sortText": "achown", - "insertTextFormat": 2, - "kind": 3 - }, - { - "documentation": "Change root directory to path.", - "label": "chroot(path)", - "insertText": "chroot(${1:path})$0", - "detail": "os", - "sortText": "achroot", - "insertTextFormat": 2, - "kind": 3 - }, - { - "documentation": "int(x=0) -> integer int(x, base=10) -> integer Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating point numbers, this truncates towards zero. If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal. >>> int('0b100', base=0) 4", - "label": "CLD_CONTINUED", - "insertText": "CLD_CONTINUED", - "detail": "os", - "sortText": "aCLD_CONTINUED", - "kind": 18 - }, - { - "documentation": "int(x=0) -> integer int(x, base=10) -> integer Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating point numbers, this truncates towards zero. If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal. >>> int('0b100', base=0) 4", - "label": "CLD_DUMPED", - "insertText": "CLD_DUMPED", - "detail": "os", - "sortText": "aCLD_DUMPED", - "kind": 18 - }, - { - "documentation": "int(x=0) -> integer int(x, base=10) -> integer Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating point numbers, this truncates towards zero. If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal. >>> int('0b100', base=0) 4", - "label": "CLD_EXITED", - "insertText": "CLD_EXITED", - "detail": "os", - "sortText": "aCLD_EXITED", - "kind": 18 - }, - { - "documentation": "int(x=0) -> integer int(x, base=10) -> integer Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating point numbers, this truncates towards zero. If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal. >>> int('0b100', base=0) 4", - "label": "CLD_TRAPPED", - "insertText": "CLD_TRAPPED", - "detail": "os", - "sortText": "aCLD_TRAPPED", - "kind": 18 - }, - { - "documentation": "Close a file descriptor.", - "label": "close(fd)", - "insertText": "close(${1:fd})$0", - "detail": "os", - "sortText": "aclose", - "insertTextFormat": 2, - "kind": 3 - }, - { - "documentation": "Closes all file descriptors in [fd_low, fd_high), ignoring errors.", - "label": "closerange(fd_low, fd_high)", - "insertText": "closerange(${1:fd_low}, ${2:fd_high})$0", - "detail": "os", - "sortText": "acloserange", - "insertTextFormat": 2, - "kind": 3 - }, - { - "documentation": "Return a string-valued system configuration variable.", - "label": "confstr(name)", - "insertText": "confstr(${1:name})$0", - "detail": "os", - "sortText": "aconfstr", - "insertTextFormat": 2, - "kind": 3 - }, - { - "documentation": "dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)", - "label": "confstr_names", - "insertText": "confstr_names", - "detail": "os", - "sortText": "aconfstr_names", - "kind": 18 - }, - { - "documentation": "Return the number of CPUs in the system; return None if indeterminable. This number is not equivalent to the number of CPUs the current process can use. The number of usable CPUs can be obtained with ``len(os.sched_getaffinity(0))``", - "label": "cpu_count()", - "insertText": "cpu_count", - "detail": "os", - "sortText": "acpu_count", - "kind": 3 - }, - { - "documentation": "Return the name of the controlling terminal for this process.", - "label": "ctermid()", - "insertText": "ctermid", - "detail": "os", - "sortText": "actermid", - "kind": 3 - }, - { - "documentation": "", - "label": "curdir", - "insertText": "curdir", - "detail": "os", - "sortText": "acurdir", - "kind": 9 - }, - { - "documentation": "", - "label": "defpath", - "insertText": "defpath", - "detail": "os", - "sortText": "adefpath", - "kind": 9 - }, - { - "documentation": "Return a string describing the encoding of a terminal's file descriptor. The file descriptor must be attached to a terminal. If the device is not a terminal, return None.", - "label": "device_encoding(fd)", - "insertText": "device_encoding(${1:fd})$0", - "detail": "os", - "sortText": "adevice_encoding", - "insertTextFormat": 2, - "kind": 3 - } -] diff --git a/tests/test_completion_core.py b/tests/test_completion_core.py index ec470c571..4e8e5558f 100644 --- a/tests/test_completion_core.py +++ b/tests/test_completion_core.py @@ -9,15 +9,6 @@ pass -def load_completion_sample(name: str) -> 'Dict': - return json.load(open(path.join(path.dirname(__file__), name + ".json"))) - - -pyls_completion_sample = load_completion_sample("pyls_completion_sample") -clangd_completion_sample = load_completion_sample("clangd_completion_sample") -intelephense_completion_sample = load_completion_sample("intelephense_completion_sample") - - class CompletionResponseParsingTests(unittest.TestCase): def test_no_response(self): From 1ccb7abf890725611575cf1e45b692653906aa21 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 22 Feb 2020 13:33:17 +0100 Subject: [PATCH 50/62] Fix sublime removing $ from the prefix --- plugin/completion.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugin/completion.py b/plugin/completion.py index ed458e709..35488210e 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -147,12 +147,34 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su if not self.enabled: return None + prefix = self.include_special_chars(prefix, locations[0]) completion_list = sublime.CompletionList() self.do_request(completion_list, prefix, locations) return completion_list + def include_special_chars(self, prefix: str, point: int) -> str: + """ + Sanatize the prefix, sublime trims of special characters from the prefix + """ + special_chars = ['$'] + + # prev char under cursor is a special char, $| + prev_char = self.view.substr(point - 1) + if prev_char in special_chars: + return prev_char + + # char before the word is a special char, $hello| + word_region = self.view.word(point) + char_before_word = self.view.substr(word_region.begin() - 1) + + if char_before_word in special_chars: + return char_before_word + prefix + + # no special chars, hello| + return prefix + def do_request(self, completion_list: sublime.CompletionList, prefix: str, locations: List[int]) -> None: # don't store client so we can handle restarts client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) From 036e432c9af34823f6c58e04c299a34c89ba2315 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 22 Feb 2020 13:34:01 +0100 Subject: [PATCH 51/62] Revert "erase regoion for snippet, replace region for plain text" This reverts commit 44a3b2a36a87436a34a82b9c2fa2ee9c48492b83. --- plugin/completion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 35488210e..8d82331e1 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -30,11 +30,11 @@ def run(self, edit: Any, **item: Any) -> None: range = Range.from_lsp(text_edit['range']) edit_region = range_to_region(range, self.view) + self.view.erase(edit, edit_region) if insert_text_format == InsertTextFormat.Snippet: - self.view.erase(edit, edit_region) self.view.run_command("insert_snippet", {"contents": new_text}) else: - self.view.replace(edit, edit_region, new_text) + self.view.insert(edit, edit_region.begin(), new_text) else: completion = item.get('insertText') or item.get('label') or "" if insert_text_format == InsertTextFormat.Snippet: From d974c229701a06c864a9dda1152dbeeae1299afe Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 23 Mar 2020 19:59:56 +0100 Subject: [PATCH 52/62] Workaround for TextEdit range not being valid when selection a completion item The workaround works like this. First a fact: "The specs says that a TextEdit must span only on one line". when we request completion items. and get a response. We save each line where a cursor is. When we select a command completion item. The text is deleted. But becuse we saved the line contents before, we just restore it. so we have the state as before. This workaround have one big downside, I guess it wouldn't work performant for lines that are really really really long. --- plugin/completion.py | 44 ++++++++++-------------------------- plugin/core/restore_lines.py | 36 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 plugin/core/restore_lines.py diff --git a/plugin/completion.py b/plugin/completion.py index 8d82331e1..8066f384f 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -12,6 +12,7 @@ from .core.views import range_to_region from .core.typing import Any, List, Dict, Tuple, Optional, Union from .core.views import text_document_position_params +from .core.restore_lines import RestoreLines class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): @@ -20,10 +21,9 @@ def run(self, edit: Any, **item: Any) -> None: text_edit = item.get('textEdit') if text_edit: - # insert the removed command completion item prefix + # restore the lines # so we don't have to calculate the offset for the textEdit range - for sel in self.view.sel(): - self.view.insert(edit, sel.begin(), CompletionHandler.last_prefix) + RestoreLines.restore_lines(edit, self.view) new_text = text_edit.get('newText') @@ -86,8 +86,6 @@ def run(self, edit: sublime.Edit, range: Optional[Tuple[int, int]] = None) -> No class CompletionHandler(LSPViewEventListener): - last_prefix = "" - def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False @@ -147,35 +145,15 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su if not self.enabled: return None - prefix = self.include_special_chars(prefix, locations[0]) completion_list = sublime.CompletionList() - self.do_request(completion_list, prefix, locations) + self.do_request(completion_list, locations) return completion_list - def include_special_chars(self, prefix: str, point: int) -> str: - """ - Sanatize the prefix, sublime trims of special characters from the prefix - """ - special_chars = ['$'] - - # prev char under cursor is a special char, $| - prev_char = self.view.substr(point - 1) - if prev_char in special_chars: - return prev_char - - # char before the word is a special char, $hello| - word_region = self.view.word(point) - char_before_word = self.view.substr(word_region.begin() - 1) - - if char_before_word in special_chars: - return char_before_word + prefix + def do_request(self, completion_list: sublime.CompletionList, locations: List[int]) -> None: + RestoreLines.clear() # delete saved lines - # no special chars, hello| - return prefix - - def do_request(self, completion_list: sublime.CompletionList, prefix: str, locations: List[int]) -> None: # don't store client so we can handle restarts client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) if not client: @@ -185,11 +163,11 @@ def do_request(self, completion_list: sublime.CompletionList, prefix: str, locat document_position = text_document_position_params(self.view, locations[0]) client.send_request( Request.complete(document_position), - lambda res: self.handle_response(res, completion_list, prefix), + lambda res: self.handle_response(res, completion_list, locations), self.handle_error) def handle_response(self, response: Optional[Union[dict, List]], - completion_list: sublime.CompletionList, prefix: str) -> None: + completion_list: sublime.CompletionList, locations: List[int]) -> None: response_items, response_incomplete = parse_completion_response(response) items = list(format_completion(item) for item in response_items) @@ -200,9 +178,11 @@ def handle_response(self, response: Optional[Union[dict, List]], if response_incomplete: flags |= sublime.DYNAMIC_COMPLETIONS - completion_list.set_completions(items, flags) - CompletionHandler.last_prefix = prefix + + for point in locations: + # save the line to restore later (only when selecting a completion item with a TextEdit) + RestoreLines.save_line(point, self.view) def handle_error(self, error: dict) -> None: sublime.status_message('Completion error: ' + str(error.get('message'))) diff --git a/plugin/core/restore_lines.py b/plugin/core/restore_lines.py new file mode 100644 index 000000000..a34ce433f --- /dev/null +++ b/plugin/core/restore_lines.py @@ -0,0 +1,36 @@ +import sublime + +class RestoreLines: + saved_lines = [] + + def save_line(point, view): + text = view.substr(view.line(point)) + row, _col = view.rowcol(point) + + RestoreLines.saved_lines.append({ + "row": row, + "text": text, + # cursor will be use retore the cursor the te exact position + "cursor": point + }) + + def restore_lines(edit: sublime.Edit, view: sublime.View): + # insert back lines from the bottom to top + RestoreLines.saved_lines.reverse() + + # restore lines contents + for saved_line in RestoreLines.saved_lines: + current_view_point = view.text_point(saved_line['row'], 0) + current_line_region = view.line(current_view_point) + view.replace(edit, current_line_region, saved_line['text']) + + # restore old cursor position + view.sel().clear() + for saved_line in RestoreLines.saved_lines: + view.sel().add(saved_line["cursor"]) + + # the lines are restored, clear them + RestoreLines.clear() + + def clear(): + RestoreLines.saved_lines = [] \ No newline at end of file From e3f580c328af6158f5574ecbdfc03042e056f202 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 23 Mar 2020 20:17:23 +0100 Subject: [PATCH 53/62] Better handle multicursor text edits --- plugin/completion.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugin/completion.py b/plugin/completion.py index 8066f384f..e2431fd89 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -30,11 +30,28 @@ def run(self, edit: Any, **item: Any) -> None: range = Range.from_lsp(text_edit['range']) edit_region = range_to_region(range, self.view) + # diff will be used to see what text has been inserted to the first selection + # and apply the new text to other selections + diff_region = sublime.Region(-1, -1) + diff_region.a = self.view.sel()[0].begin() + self.view.erase(edit, edit_region) if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", {"contents": new_text}) else: self.view.insert(edit, edit_region.begin(), new_text) + + diff_region.b = self.view.sel()[0].begin() + diff_text = self.view.substr(diff_region) + + # we already inserted text to the first selection, + # we ignore the first selection + # and apply the diff text to other selections + _first_selection, *other_sections = self.view.sel() + other_sections.reverse() # apply the edits from bottom to top + for sel in other_sections: + self.view.insert(edit, sel.end(), diff_text) + else: completion = item.get('insertText') or item.get('label') or "" if insert_text_format == InsertTextFormat.Snippet: From f207bf2683dae88058915471d2db330615fe1ca3 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 23 Mar 2020 22:30:35 +0100 Subject: [PATCH 54/62] Move save lines to on_query_completions, and clean up the a little --- plugin/completion.py | 13 +++++-------- plugin/core/restore_lines.py | 26 ++++++++++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index e2431fd89..159259cf4 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -169,22 +169,23 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su return completion_list def do_request(self, completion_list: sublime.CompletionList, locations: List[int]) -> None: - RestoreLines.clear() # delete saved lines - # don't store client so we can handle restarts client = client_from_session(session_for_view(self.view, 'completionProvider', locations[0])) if not client: return + # save lines to restore them later (only when selecting a completion item with a TextEdit) + RestoreLines.save_lines(locations, self.view) + self.manager.documents.purge_changes(self.view) document_position = text_document_position_params(self.view, locations[0]) client.send_request( Request.complete(document_position), - lambda res: self.handle_response(res, completion_list, locations), + lambda res: self.handle_response(res, completion_list), self.handle_error) def handle_response(self, response: Optional[Union[dict, List]], - completion_list: sublime.CompletionList, locations: List[int]) -> None: + completion_list: sublime.CompletionList) -> None: response_items, response_incomplete = parse_completion_response(response) items = list(format_completion(item) for item in response_items) @@ -197,9 +198,5 @@ def handle_response(self, response: Optional[Union[dict, List]], flags |= sublime.DYNAMIC_COMPLETIONS completion_list.set_completions(items, flags) - for point in locations: - # save the line to restore later (only when selecting a completion item with a TextEdit) - RestoreLines.save_line(point, self.view) - def handle_error(self, error: dict) -> None: sublime.status_message('Completion error: ' + str(error.get('message'))) diff --git a/plugin/core/restore_lines.py b/plugin/core/restore_lines.py index a34ce433f..629019c8f 100644 --- a/plugin/core/restore_lines.py +++ b/plugin/core/restore_lines.py @@ -1,18 +1,24 @@ import sublime +from .typing import List + class RestoreLines: saved_lines = [] - def save_line(point, view): - text = view.substr(view.line(point)) - row, _col = view.rowcol(point) + def save_lines(locations: List[int], view: sublime.View): + # Clear previously saved lines + RestoreLines.clear() + + for point in locations: + text = view.substr(view.line(point)) + row, _col = view.rowcol(point) - RestoreLines.saved_lines.append({ - "row": row, - "text": text, - # cursor will be use retore the cursor the te exact position - "cursor": point - }) + RestoreLines.saved_lines.append({ + "row": row, + "text": text, + # cursor will be use retore the cursor the te exact position + "cursor": point + }) def restore_lines(edit: sublime.Edit, view: sublime.View): # insert back lines from the bottom to top @@ -33,4 +39,4 @@ def restore_lines(edit: sublime.Edit, view: sublime.View): RestoreLines.clear() def clear(): - RestoreLines.saved_lines = [] \ No newline at end of file + RestoreLines.saved_lines = [] From 4468f8a5f6e5afff4ee7807a9bfe13de454822f7 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 23 Mar 2020 22:52:58 +0100 Subject: [PATCH 55/62] make flake8 and mypy happy --- plugin/core/restore_lines.py | 11 +++++++---- tests/intelephense_completion_sample.py | 2 +- tests/test_completion.py | 6 +++++- tests/test_completion_core.py | 2 -- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/plugin/core/restore_lines.py b/plugin/core/restore_lines.py index 629019c8f..db2c00a59 100644 --- a/plugin/core/restore_lines.py +++ b/plugin/core/restore_lines.py @@ -3,9 +3,10 @@ class RestoreLines: - saved_lines = [] + saved_lines = [] # type: List[dict] - def save_lines(locations: List[int], view: sublime.View): + @staticmethod + def save_lines(locations: List[int], view: sublime.View) -> None: # Clear previously saved lines RestoreLines.clear() @@ -20,7 +21,8 @@ def save_lines(locations: List[int], view: sublime.View): "cursor": point }) - def restore_lines(edit: sublime.Edit, view: sublime.View): + @staticmethod + def restore_lines(edit: sublime.Edit, view: sublime.View) -> None: # insert back lines from the bottom to top RestoreLines.saved_lines.reverse() @@ -38,5 +40,6 @@ def restore_lines(edit: sublime.Edit, view: sublime.View): # the lines are restored, clear them RestoreLines.clear() - def clear(): + @staticmethod + def clear() -> None: RestoreLines.saved_lines = [] diff --git a/tests/intelephense_completion_sample.py b/tests/intelephense_completion_sample.py index 1597b7b88..9c03e16b2 100644 --- a/tests/intelephense_completion_sample.py +++ b/tests/intelephense_completion_sample.py @@ -10,4 +10,4 @@ ?> """ -intelephense_response = {"items": [{"label": "$hello", "textEdit": {"newText": "$hello", "range": {"end": {"line": 2, "character": 3}, "start": {"line": 2, "character": 0}}}, "data": 2369386987913238, "detail": "int", "kind": 6, "sortText": "$hello"}], "isIncomplete": False} +intelephense_response = {"items": [{"label": "$hello", "textEdit": {"newText": "$hello", "range": {"end": {"line": 2, "character": 3}, "start": {"line": 2, "character": 0}}}, "data": 2369386987913238, "detail": "int", "kind": 6, "sortText": "$hello"}], "isIncomplete": False} # noqa: E501 diff --git a/tests/test_completion.py b/tests/test_completion.py index 2cfae412c..e96626455 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -4,7 +4,11 @@ from unittesting import DeferrableTestCase import sublime -from LSP.tests.intelephense_completion_sample import intelephense_before_state, intelephense_expected_state, intelephense_response +from LSP.tests.intelephense_completion_sample import ( + intelephense_before_state, + intelephense_expected_state, + intelephense_response +) try: from typing import Dict, Optional, List, Generator diff --git a/tests/test_completion_core.py b/tests/test_completion_core.py index 4e8e5558f..ba3fcc801 100644 --- a/tests/test_completion_core.py +++ b/tests/test_completion_core.py @@ -1,6 +1,4 @@ import unittest -from os import path -import json from LSP.plugin.core.completion import parse_completion_response try: from typing import Optional, Dict From bc687e56743fb9d163f1218b11f5eac1448a37e2 Mon Sep 17 00:00:00 2001 From: Predrag Date: Mon, 23 Mar 2020 23:10:44 +0100 Subject: [PATCH 56/62] fix typo --- plugin/completion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 159259cf4..af0ac7c8c 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -47,9 +47,9 @@ def run(self, edit: Any, **item: Any) -> None: # we already inserted text to the first selection, # we ignore the first selection # and apply the diff text to other selections - _first_selection, *other_sections = self.view.sel() - other_sections.reverse() # apply the edits from bottom to top - for sel in other_sections: + _first_selection, *other_selections = self.view.sel() + other_selections.reverse() # apply the edits from bottom to top + for sel in other_selections: self.view.insert(edit, sel.end(), diff_text) else: From 07f7824b0d80680b2e9a236fe6561e8a161fb04c Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 26 Mar 2020 11:55:21 +0100 Subject: [PATCH 57/62] Dont use static class variables for saving the Line contents, because each request to the server will override the previous state. instead create instances so each request has its own independent Line state, and the request cannot override the previous state. --- plugin/completion.py | 14 ++++++++------ plugin/core/completion.py | 8 ++++++-- plugin/core/restore_lines.py | 35 ++++++++++++++++++++++------------- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index af0ac7c8c..879e2f67b 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -16,14 +16,15 @@ class LspSelectCompletionItemCommand(sublime_plugin.TextCommand): - def run(self, edit: Any, **item: Any) -> None: + def run(self, edit: Any, item: Any, restore_lines_dict: dict) -> None: insert_text_format = item.get("insertTextFormat") text_edit = item.get('textEdit') if text_edit: # restore the lines # so we don't have to calculate the offset for the textEdit range - RestoreLines.restore_lines(edit, self.view) + restore_lines = RestoreLines.from_dict(restore_lines_dict) + restore_lines.restore_lines(edit, self.view) new_text = text_edit.get('newText') @@ -175,19 +176,20 @@ def do_request(self, completion_list: sublime.CompletionList, locations: List[in return # save lines to restore them later (only when selecting a completion item with a TextEdit) - RestoreLines.save_lines(locations, self.view) + restore_lines = RestoreLines() + restore_lines.save_lines(locations, self.view) self.manager.documents.purge_changes(self.view) document_position = text_document_position_params(self.view, locations[0]) client.send_request( Request.complete(document_position), - lambda res: self.handle_response(res, completion_list), + lambda res: self.handle_response(res, completion_list, restore_lines), self.handle_error) def handle_response(self, response: Optional[Union[dict, List]], - completion_list: sublime.CompletionList) -> None: + completion_list: sublime.CompletionList, restore_lines: RestoreLines) -> None: response_items, response_incomplete = parse_completion_response(response) - items = list(format_completion(item) for item in response_items) + items = list(format_completion(item, restore_lines) for item in response_items) flags = 0 if settings.only_show_lsp_completions: diff --git a/plugin/core/completion.py b/plugin/core/completion.py index f199bd404..32b396c6a 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -1,4 +1,5 @@ import sublime +from .restore_lines import RestoreLines from .typing import Tuple, Optional, Dict, List, Union @@ -31,7 +32,7 @@ } -def format_completion(item: dict) -> sublime.CompletionItem: +def format_completion(item: dict, restore_lines: RestoreLines) -> sublime.CompletionItem: trigger = item.get('label') or "" annotation = item.get('detail') or "" kind = sublime.KIND_AMBIGUOUS @@ -50,7 +51,10 @@ def format_completion(item: dict) -> sublime.CompletionItem: return sublime.CompletionItem.command_completion( trigger, command="lsp_select_completion_item", - args=item, + args={ + "item": item, + "restore_lines_dict": restore_lines.to_dict() + }, annotation=annotation, kind=kind ) diff --git a/plugin/core/restore_lines.py b/plugin/core/restore_lines.py index db2c00a59..019c6ff88 100644 --- a/plugin/core/restore_lines.py +++ b/plugin/core/restore_lines.py @@ -3,43 +3,52 @@ class RestoreLines: - saved_lines = [] # type: List[dict] + def __init__(self): + self.saved_lines = [] # type: List[dict] - @staticmethod - def save_lines(locations: List[int], view: sublime.View) -> None: + def save_lines(self, locations: List[int], view: sublime.View) -> None: # Clear previously saved lines - RestoreLines.clear() + self.clear() for point in locations: text = view.substr(view.line(point)) row, _col = view.rowcol(point) - RestoreLines.saved_lines.append({ + self.saved_lines.append({ "row": row, "text": text, # cursor will be use retore the cursor the te exact position "cursor": point }) + def to_dict(self): + return { + "saved_lines": self.saved_lines + } + @staticmethod - def restore_lines(edit: sublime.Edit, view: sublime.View) -> None: + def from_dict(dictionary): + restore_lines = RestoreLines() + restore_lines.saved_lines = dictionary["saved_lines"] + return restore_lines + + def restore_lines(self, edit: sublime.Edit, view: sublime.View) -> None: # insert back lines from the bottom to top - RestoreLines.saved_lines.reverse() + self.saved_lines.reverse() # restore lines contents - for saved_line in RestoreLines.saved_lines: + for saved_line in self.saved_lines: current_view_point = view.text_point(saved_line['row'], 0) current_line_region = view.line(current_view_point) view.replace(edit, current_line_region, saved_line['text']) # restore old cursor position view.sel().clear() - for saved_line in RestoreLines.saved_lines: + for saved_line in self.saved_lines: view.sel().add(saved_line["cursor"]) # the lines are restored, clear them - RestoreLines.clear() + self.clear() - @staticmethod - def clear() -> None: - RestoreLines.saved_lines = [] + def clear(self) -> None: + self.saved_lines = [] From 51bbd4d35e3cdee4bfea8ef761fa79f139a6aecd Mon Sep 17 00:00:00 2001 From: Predrag Date: Thu, 26 Mar 2020 12:08:26 +0100 Subject: [PATCH 58/62] fix for commit_completion re-triggers the completion panel "forever" --- plugin/completion.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugin/completion.py b/plugin/completion.py index 879e2f67b..56f53b2a2 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -109,6 +109,14 @@ def __init__(self, view: sublime.View) -> None: self.initialized = False self.enabled = False + def on_text_command(self, command_name, args): + if command_name == 'commit_completion': + # if the previous char is a trigger char + # commit_completion re-triggers the completion panel "forever" + # hide the autocomplete on commit_completion + # https://github.com/sublimelsp/LSP/pull/866#issuecomment-603466761 + self.view.run_command("hide_auto_complete") + @classmethod def is_applicable(cls, view_settings: dict) -> bool: if 'completion' in settings.disabled_capabilities: From b5c0de6ce9710bbcb21ad992bec7a2e682a3de1f Mon Sep 17 00:00:00 2001 From: Predrag Date: Fri, 27 Mar 2020 18:54:14 +0100 Subject: [PATCH 59/62] Better handle multicursor text edits --- plugin/completion.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 56f53b2a2..1a3b53dad 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -31,28 +31,27 @@ def run(self, edit: Any, item: Any, restore_lines_dict: dict) -> None: range = Range.from_lsp(text_edit['range']) edit_region = range_to_region(range, self.view) - # diff will be used to see what text has been inserted to the first selection - # and apply the new text to other selections - diff_region = sublime.Region(-1, -1) - diff_region.a = self.view.sel()[0].begin() + # calculate offset by comparing cursor position with edit_region.begin. + # by applying the offset to all selections + # the TextEdit becomes valid for all selections + cursor = self.view.sel()[0].begin() # type: int + + offset_start = cursor - edit_region.begin() + offset_length = edit_region.end() - edit_region.begin() + + # erease regions from bottom to top + for sel in reversed(self.view.sel()): + begin = sel.begin() - offset_start + end = begin + offset_length + r = sublime.Region(begin, end) + self.view.erase(edit, r) - self.view.erase(edit, edit_region) if insert_text_format == InsertTextFormat.Snippet: self.view.run_command("insert_snippet", {"contents": new_text}) else: - self.view.insert(edit, edit_region.begin(), new_text) - - diff_region.b = self.view.sel()[0].begin() - diff_text = self.view.substr(diff_region) - - # we already inserted text to the first selection, - # we ignore the first selection - # and apply the diff text to other selections - _first_selection, *other_selections = self.view.sel() - other_selections.reverse() # apply the edits from bottom to top - for sel in other_selections: - self.view.insert(edit, sel.end(), diff_text) - + # insert text from bottom to top + for sel in reversed(self.view.sel()): + self.view.insert(edit, sel.begin(), new_text) else: completion = item.get('insertText') or item.get('label') or "" if insert_text_format == InsertTextFormat.Snippet: From 34ae9e15119736ea0bfd1ee1bb4b27c070ff1c77 Mon Sep 17 00:00:00 2001 From: Predrag Date: Fri, 27 Mar 2020 23:58:28 +0100 Subject: [PATCH 60/62] use transform_region to restore file contents --- plugin/core/restore_lines.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/plugin/core/restore_lines.py b/plugin/core/restore_lines.py index 019c6ff88..12aadeda3 100644 --- a/plugin/core/restore_lines.py +++ b/plugin/core/restore_lines.py @@ -7,15 +7,16 @@ def __init__(self): self.saved_lines = [] # type: List[dict] def save_lines(self, locations: List[int], view: sublime.View) -> None: - # Clear previously saved lines - self.clear() + change_id = view.change_id() for point in locations: - text = view.substr(view.line(point)) - row, _col = view.rowcol(point) + line = view.line(point) + change_region = (line.begin(), line.end()) + text = view.substr(line) self.saved_lines.append({ - "row": row, + "change_id": change_id, + "change_region": change_region, "text": text, # cursor will be use retore the cursor the te exact position "cursor": point @@ -33,22 +34,18 @@ def from_dict(dictionary): return restore_lines def restore_lines(self, edit: sublime.Edit, view: sublime.View) -> None: + # restore lines contents # insert back lines from the bottom to top - self.saved_lines.reverse() + for saved_line in reversed(self.saved_lines): + change_id = saved_line['change_id'] + begin, end = saved_line['change_region'] + change_region = sublime.Region(begin, end) - # restore lines contents - for saved_line in self.saved_lines: - current_view_point = view.text_point(saved_line['row'], 0) - current_line_region = view.line(current_view_point) - view.replace(edit, current_line_region, saved_line['text']) + transform_region = view.transform_region_from(change_region, change_id) + view.erase(edit, transform_region) + view.insert(edit, transform_region.begin(), saved_line['text']) # restore old cursor position view.sel().clear() for saved_line in self.saved_lines: - view.sel().add(saved_line["cursor"]) - - # the lines are restored, clear them - self.clear() - - def clear(self) -> None: - self.saved_lines = [] + view.sel().add(saved_line["cursor"]) \ No newline at end of file From 18c47e3826fa642c6d864b5f4a255fe67d726668 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 28 Mar 2020 20:14:20 +0100 Subject: [PATCH 61/62] Resolve the completion promise in the handle_error --- plugin/completion.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 1a3b53dad..1e49c9d78 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -191,7 +191,7 @@ def do_request(self, completion_list: sublime.CompletionList, locations: List[in client.send_request( Request.complete(document_position), lambda res: self.handle_response(res, completion_list, restore_lines), - self.handle_error) + lambda res: self.handle_error(res, completion_list)) def handle_response(self, response: Optional[Union[dict, List]], completion_list: sublime.CompletionList, restore_lines: RestoreLines) -> None: @@ -207,5 +207,6 @@ def handle_response(self, response: Optional[Union[dict, List]], flags |= sublime.DYNAMIC_COMPLETIONS completion_list.set_completions(items, flags) - def handle_error(self, error: dict) -> None: + def handle_error(self, error: dict, completion_list: sublime.CompletionList) -> None: + completion_list.set_completions([]) sublime.status_message('Completion error: ' + str(error.get('message'))) From 6e596e4f61c72aa05c05ca1bf9e0b236f647ce56 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 28 Mar 2020 21:01:29 +0100 Subject: [PATCH 62/62] remove hiding auto_complete gloably after commit_completions --- plugin/completion.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 1e49c9d78..a890c0ebb 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -107,14 +107,6 @@ def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False self.enabled = False - - def on_text_command(self, command_name, args): - if command_name == 'commit_completion': - # if the previous char is a trigger char - # commit_completion re-triggers the completion panel "forever" - # hide the autocomplete on commit_completion - # https://github.com/sublimelsp/LSP/pull/866#issuecomment-603466761 - self.view.run_command("hide_auto_complete") @classmethod def is_applicable(cls, view_settings: dict) -> bool: