diff --git a/LSP.sublime-settings b/LSP.sublime-settings index 07b38a369..a2e7122a5 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -58,17 +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, - - // 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..9ae95e764 100644 --- a/docs/features.md +++ b/docs/features.md @@ -70,9 +70,7 @@ 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* -* `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/completion.py b/plugin/completion.py index 441e71839..a890c0ebb 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -1,36 +1,100 @@ import sublime import sublime_plugin -from .core.completion import parse_completion_response, format_completion -from .core.configurations import is_supported_syntax -from .core.documents import position_is_word -from .core.edit import parse_text_edit + +from .core.protocol import Request, Range, InsertTextFormat +from .core.settings import settings, client_configs from .core.logging import debug -from .core.protocol import Request +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.sessions import Session -from .core.settings import settings, client_configs +from .core.edit import parse_text_edit +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): + 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 + restore_lines = RestoreLines.from_dict(restore_lines_dict) + restore_lines.restore_lines(edit, self.view) + + new_text = text_edit.get('newText') + + range = Range.from_lsp(text_edit['range']) + edit_region = range_to_region(range, self.view) + + # 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) + + if insert_text_format == InsertTextFormat.Snippet: + self.view.run_command("insert_snippet", {"contents": new_text}) + else: + # 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: + self.view.run_command("insert_snippet", {"contents": completion}) + else: + 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') + if additional_edits: + self.apply_additional_edits(additional_edits) + else: + self.do_resolve(item) + def do_resolve(self, item: dict) -> None: + session = session_for_view(self.view, 'completionProvider', self.view.sel()[0].begin()) + if not session: + return -class CompletionState(object): - IDLE = 0 - REQUESTING = 1 - APPLYING = 2 - CANCELLING = 3 - + client = client_from_session(session) + if not client: + return -last_text_command = None + 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: + if response: + additional_edits = response.get('additionalTextEdits') + if additional_edits: + self.apply_additional_edits(additional_edits) -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 + 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 LspTrimCompletionCommand(sublime_plugin.TextCommand): - def run(self, edit: sublime.Edit, range: Optional[Tuple[int, int]] = None) -> None: if range: start, end = range @@ -43,18 +107,7 @@ def __init__(self, view: sublime.View) -> None: super().__init__(view) self.initialized = False self.enabled = False - self.trigger_chars = [] # type: List[str] - self.auto_complete_selector = "" - self.resolve = False - self.state = CompletionState.IDLE - self.completions = [] # type: List[Any] - self.next_request = None # type: Optional[Tuple[str, List[int]]] - self.last_prefix = "" - self.last_location = -1 - self.committing = False - self.response_items = [] # type: List[dict] - self.response_incomplete = False - + @classmethod def is_applicable(cls, view_settings: dict) -> bool: if 'completion' in settings.disabled_capabilities: @@ -72,274 +125,80 @@ 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( + + trigger_chars = completionProvider.get( 'triggerCharacters') or [] - if self.trigger_chars: - self.register_trigger_chars(session) - self.auto_complete_selector = self.view.settings().get("auto_complete_selector", "") or "" + if trigger_chars: + self.register_trigger_chars(session, trigger_chars) 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 }) 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 - - 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 - - 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 match_selector(self, location: int) -> bool: - return self.view.match_selector(location, self.auto_complete_selector) - - def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[Tuple[List[Tuple[str, str]], int]]: + def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[sublime.CompletionList]: 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.match_selector(locations[0]): - 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 - - return (self.completions, flags) + if not self.enabled: + return None - return None + completion_list = sublime.CompletionList() - def on_text_command(self, command_name: str, args: Optional[Any]) -> None: - self.committing = command_name in ('commit_completion', 'auto_complete') + self.do_request(completion_list, locations) - def do_request(self, prefix: str, locations: List[int]) -> None: - self.next_request = None - view = self.view + return completion_list + def do_request(self, completion_list: sublime.CompletionList, locations: List[int]) -> None: # 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 - if settings.complete_all_chars or self.is_after_trigger_character(locations[0]): - self.manager.documents.purge_changes(self.view) - document_position = text_document_position_params(self.view, locations[0]) - client.send_request( - Request.complete(document_position), - self.handle_response, - self.handle_error) - self.state = CompletionState.REQUESTING + # save lines to restore them later (only when selecting a completion item with a TextEdit) + restore_lines = RestoreLines() + restore_lines.save_lines(locations, self.view) - def do_resolve(self, item: dict) -> None: - view = self.view - - client = client_from_session(session_for_view(view, 'completionProvider', self.last_location)) - if not client: - return - - client.send_request(Request.resolveCompletionItem(item), self.handle_resolve_response) + 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, restore_lines), + lambda res: self.handle_error(res, completion_list)) - 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 handle_response(self, response: Optional[Union[dict, List]], + completion_list: sublime.CompletionList, restore_lines: RestoreLines) -> None: + response_items, response_incomplete = parse_completion_response(response) + items = list(format_completion(item, restore_lines) for item in response_items) - 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') + flags = 0 + if settings.only_show_lsp_completions: + flags |= sublime.INHIBIT_WORD_COMPLETIONS + flags |= sublime.INHIBIT_EXPLICIT_COMPLETIONS - 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)) + if response_incomplete: + 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'))) - 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 13c3ae287..32b396c6a 100644 --- a/plugin/core/completion.py +++ b/plugin/core/completion.py @@ -1,76 +1,63 @@ -from .protocol import CompletionItemKind, Range -from .types import Settings -from .logging import debug +import sublime +from .restore_lines import RestoreLines from .typing import Tuple, Optional, Dict, List, Union -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 - - -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 +completion_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, "c", "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, restore_lines: RestoreLines) -> 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 = completion_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) # type: ignore + + return sublime.CompletionItem.command_completion( + trigger, + command="lsp_select_completion_item", + args={ + "item": item, + "restore_lines_dict": restore_lines.to_dict() + }, + annotation=annotation, + kind=kind + ) def parse_completion_response(response: Optional[Union[Dict, List]]) -> Tuple[List[Dict], bool]: diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index d26a674a9..c7bfa57f0 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -79,6 +79,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/restore_lines.py b/plugin/core/restore_lines.py new file mode 100644 index 000000000..12aadeda3 --- /dev/null +++ b/plugin/core/restore_lines.py @@ -0,0 +1,51 @@ +import sublime +from .typing import List + + +class RestoreLines: + def __init__(self): + self.saved_lines = [] # type: List[dict] + + def save_lines(self, locations: List[int], view: sublime.View) -> None: + change_id = view.change_id() + + for point in locations: + line = view.line(point) + change_region = (line.begin(), line.end()) + text = view.substr(line) + + self.saved_lines.append({ + "change_id": change_id, + "change_region": change_region, + "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 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: + # restore lines contents + # insert back lines from the bottom to top + 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) + + 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"]) \ No newline at end of file diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 8468f6a73..def899d8d 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -32,7 +32,8 @@ def get_initialize_params(workspace_folders: List[WorkspaceFolder], config: Clie }, "completion": { "completionItem": { - "snippetSupport": True + "snippetSupport": True, + "deprecatedSupport": True }, "completionItemKind": { "valueSet": completion_item_kinds diff --git a/plugin/core/settings.py b/plugin/core/settings.py index fc01de66d..cd692580d 100644 --- a/plugin/core/settings.py +++ b/plugin/core/settings.py @@ -75,8 +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.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 0170281e5..e8275ac5b 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -22,8 +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.completion_hint_type = "auto" self.show_references_in_quick_panel = False self.disabled_capabilities = [] # type: List[str] self.log_debug = True diff --git a/stubs/sublime.pyi b/stubs/sublime.pyi index d60ebe8c2..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: @@ -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 @@ -67,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: @@ -238,6 +257,22 @@ 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: + ... + + class Window: window_id = ... # type: int settings_object = ... # type: Settings @@ -478,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: ... @@ -491,7 +532,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/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/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..9c03e16b2 --- /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} # noqa: E501 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/setup.py b/tests/setup.py index 814749d21..80b34259c 100644 --- a/tests/setup.py +++ b/tests/setup.py @@ -171,6 +171,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 55a1fd338..e96626455 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1,10 +1,14 @@ 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 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 @@ -12,111 +16,171 @@ except ImportError: pass -label_completions = [dict(label='asdf'), dict(label='efgh')] +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 = [ - 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': 0 + }, + '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 + } + } + } + } ] @@ -150,7 +214,6 @@ def tearDown(self) -> 'Generator': class QueryCompletionsTests(TextDocumentTestCase): - def init_view_settings(self) -> None: super().init_view_settings() assert self.view @@ -161,63 +224,69 @@ def await_message(self, msg: str) -> 'Generator': yield 500 yield from super().await_message(msg) + 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') + + 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': - self.set_response('textDocument/completion', label_completions) - - handler = self.get_view_event_listener("on_query_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(handler.state, CompletionState.IDLE) - 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.set_response("textDocument/completion", completions_with_label) + + self.type("a") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") + + 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) - 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') - 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())), - insert_text_completions[0]["insertText"]) + self.set_response("textDocument/completion", insert_text_completions) + + 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"]) 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") - 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.set_response("textDocument/completion", var_completion_using_label) + self.type("$") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") + + self.assertEquals(self.read_file(), '$what') def test_var_prefix_added_in_insertText(self) -> 'Generator': """ @@ -225,19 +294,13 @@ 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") - 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') + 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') def test_var_prefix_added_in_label(self) -> 'Generator': """ @@ -245,19 +308,13 @@ def test_var_prefix_added_in_label(self) -> 'Generator': PHP language server: label='$someParam', textEdit='someParam' (https://github.com/sublimelsp/LSP/issues/368) """ - 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') + 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.assertEquals( + self.read_file(), '$what') def test_space_added_in_label(self) -> 'Generator': """ @@ -265,17 +322,12 @@ 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") - 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.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), 'const') + 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') def test_dash_missing_from_label(self) -> 'Generator': """ @@ -283,21 +335,14 @@ 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) + self.type("u") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.set_response('textDocument/completion', dash_missing_from_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 + 2) - self.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), - '-UniqueId') + self.assertEquals( + self.read_file(), + '-UniqueId') def test_edit_before_cursor(self) -> 'Generator': """ @@ -305,23 +350,14 @@ def test_edit_before_cursor(self) -> 'Generator': Metals: label="override def myFunction(): Unit" """ - self.view.run_command('append', {'characters': ' def myF'}) - self.view.run_command('move_to', {'to': 'eol'}) + self.set_response("textDocument/completion", edit_before_cursor) + self.type(' def myF') + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - 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.assertEquals( + self.read_file(), + ' override def myFunction(): Unit = ???') def test_edit_after_nonword(self) -> 'Generator': """ @@ -330,82 +366,71 @@ 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) + self.type("List.") + yield from self.select_completion() + yield from self.await_message("textDocument/completion") - self.set_response('textDocument/completion', edit_after_nonword) - handler = self.get_view_event_listener("on_query_completions") - 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.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), - 'List.apply()') + 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 """ - 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') - 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.assertEquals( - self.view.substr(sublime.Region(0, self.view.size())), - 'def foo: Int = ???\n def boo: Int = ???') + 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 = ???') 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') + 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('textDocument/completion', completions_with_label) 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.view.substr(sublime.Region(0, self.view.size())), - 'import asdf;\nasdf') - handler.resolve = False + 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') + + 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') + + 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) diff --git a/tests/test_completion_core.py b/tests/test_completion_core.py index b204aae4d..ba3fcc801 100644 --- a/tests/test_completion_core.py +++ b/tests/test_completion_core.py @@ -1,8 +1,5 @@ import unittest -from os import path -import json -from LSP.plugin.core.completion import format_completion, parse_completion_response -from LSP.plugin.core.types import Settings +from LSP.plugin.core.completion import parse_completion_response try: from typing import Optional, Dict assert Optional and Dict @@ -10,18 +7,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") - - -settings = Settings() - - class CompletionResponseParsingTests(unittest.TestCase): def test_no_response(self): @@ -35,148 +20,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"}, 0, settings) - 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, settings) - 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, 0, settings) - 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} - } - } - } - last_col = 1 - result = format_completion(item, last_col, settings) - 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] - 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 = "" - last_col = 1 - result = [format_completion(item, last_col, settings) 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): - last_col = 1 - result = [format_completion(item, last_col, settings) 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') - ] - ) diff --git a/tests/test_mocks.py b/tests/test_mocks.py index 3fb98da81..240eb0e01 100644 --- a/tests/test_mocks.py +++ b/tests/test_mocks.py @@ -23,7 +23,7 @@ 'hoverProvider': True, 'completionProvider': { 'triggerCharacters': ['.'], - 'resolveProvider': False + 'resolveProvider': True }, 'textDocumentSync': { "openClose": True,