From ff7b00179a361ce356124c67b8bdf26a42346c04 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 7 Feb 2023 21:07:30 +0100 Subject: [PATCH 1/9] Optimize performance of handling huge completion payloads --- boot.py | 2 +- plugin/completion.py | 14 ++++++++------ plugin/core/views.py | 32 +++++++++----------------------- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/boot.py b/boot.py index 0a23d5e5f..cd9fe15c5 100644 --- a/boot.py +++ b/boot.py @@ -10,7 +10,7 @@ from .plugin.color import LspColorPresentationCommand from .plugin.completion import LspCommitCompletionWithOppositeInsertMode from .plugin.completion import LspResolveDocsCommand -from .plugin.completion import LspSelectCompletionItemCommand +from .plugin.completion import LspSelectCompletionCommand from .plugin.configuration import LspDisableLanguageServerGloballyCommand from .plugin.configuration import LspDisableLanguageServerInProjectCommand from .plugin.configuration import LspEnableLanguageServerGloballyCommand diff --git a/plugin/completion.py b/plugin/completion.py index 68d43468d..6e2528c31 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -90,7 +90,7 @@ def _on_completion_response_async( def _resolve_completions_async(self, responses: List[ResolvedCompletions]) -> None: if self._resolved: return - LspResolveDocsCommand.completions = {} + LspSelectCompletionCommand.completions = {} items = [] # type: List[sublime.CompletionItem] errors = [] # type: List[Error] flags = 0 # int @@ -117,7 +117,7 @@ def _resolve_completions_async(self, responses: List[ResolvedCompletions]) -> No elif isinstance(response, list): response_items = response response_items = sorted(response_items, key=lambda item: item.get("sortText") or item["label"]) - LspResolveDocsCommand.completions[session.config.name] = response_items + LspSelectCompletionCommand.completions[session.config.name] = response_items can_resolve_completion_items = session.has_capability('completionProvider.resolveProvider') config_name = session.config.name items.extend( @@ -150,8 +150,6 @@ def _resolve_task_async(self, completions: List[sublime.CompletionItem], flags: class LspResolveDocsCommand(LspTextCommand): - completions = {} # type: Dict[SessionName, List[CompletionItem]] - def run(self, edit: sublime.Edit, index: int, session_name: str, event: Optional[dict] = None) -> None: def run_async() -> None: @@ -218,8 +216,12 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None: LspCommitCompletionWithOppositeInsertMode.active = False -class LspSelectCompletionItemCommand(LspTextCommand): - def run(self, edit: sublime.Edit, item: CompletionItem, session_name: str) -> None: +class LspSelectCompletionCommand(LspTextCommand): + + completions = {} # type: Dict[SessionName, List[CompletionItem]] + + def run(self, edit: sublime.Edit, index: int, session_name: str) -> None: + item = LspSelectCompletionCommand.completions[session_name][index] text_edit = item.get("textEdit") if text_edit: new_text = text_edit["newText"].replace("\r", "") diff --git a/plugin/core/views.py b/plugin/core/views.py index a98f23dad..32b07d4eb 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1030,22 +1030,18 @@ def format_completion( item: CompletionItem, index: int, can_resolve_completion_items: bool, session_name: str, view_id: int ) -> sublime.CompletionItem: # This is a hot function. Don't do heavy computations or IO in this function. - lsp_label = item['label'] lsp_label_details = item.get('labelDetails') or {} lsp_label_detail = lsp_label_details.get('detail') or "" lsp_label_description = lsp_label_details.get('description') or "" lsp_filter_text = item.get('filterText') or "" lsp_detail = (item.get('detail') or "").replace("\n", " ") - completion_kind = item.get('kind') kind = COMPLETION_KINDS.get(completion_kind, sublime.KIND_AMBIGUOUS) if completion_kind else sublime.KIND_AMBIGUOUS - details = [] # type: List[str] if can_resolve_completion_items or item.get('documentation'): details.append(make_command_link( 'lsp_resolve_docs', "More", {'index': index, 'session_name': session_name}, view_id=view_id)) - if lsp_label_detail and (lsp_label + lsp_label_detail).startswith(lsp_filter_text): trigger = lsp_label + lsp_label_detail annotation = lsp_label_description or lsp_detail @@ -1062,22 +1058,22 @@ def format_completion( details.append(html.escape(lsp_label + lsp_label_detail)) if lsp_label_description: details.append(html.escape(lsp_label_description)) - if item.get('deprecated') or CompletionItemTag.Deprecated in item.get('tags', []): annotation = "DEPRECATED - " + annotation if annotation else "DEPRECATED" - - insert_replace_support_html = get_insert_replace_support_html(item) - if insert_replace_support_html: - details.append(insert_replace_support_html) - + text_edit = item.get('textEdit') + if text_edit and 'insert' in text_edit and 'replace' in text_edit: + insert_mode = userprefs().completion_insert_mode + oposite_insert_mode = 'Replace' if insert_mode == 'insert' else 'Insert' + command_url = sublime.command_url("lsp_commit_completion_with_opposite_insert_mode") + details.append("{}".format(command_url, oposite_insert_mode)) completion = sublime.CompletionItem.command_completion( trigger=trigger, - command='lsp_select_completion_item', - args={"item": item, "session_name": session_name}, + command='lsp_select_completion', + args={"index": index, "session_name": session_name}, annotation=annotation, kind=kind, details=" | ".join(details)) - if item.get('textEdit'): + if text_edit: completion.flags = sublime.COMPLETION_FLAG_KEEP_PREFIX return completion @@ -1095,13 +1091,3 @@ def format_code_actions_for_quick_panel( if code_action.get('isPreferred', False): selected_index = idx return items, selected_index - - -def get_insert_replace_support_html(item: CompletionItem) -> Optional[str]: - text_edit = item.get('textEdit') - if text_edit and 'insert' in text_edit and 'replace' in text_edit: - insert_mode = userprefs().completion_insert_mode - oposite_insert_mode = 'Replace' if insert_mode == 'insert' else 'Insert' - command_url = sublime.command_url("lsp_commit_completion_with_opposite_insert_mode") - return "{}".format(command_url, oposite_insert_mode) - return None From 20baa1bff602df0568a61c48ca3118398ee482b7 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 7 Feb 2023 21:34:10 +0100 Subject: [PATCH 2/9] fix --- plugin/completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/completion.py b/plugin/completion.py index 6e2528c31..4c915b9a7 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -153,7 +153,7 @@ class LspResolveDocsCommand(LspTextCommand): def run(self, edit: sublime.Edit, index: int, session_name: str, event: Optional[dict] = None) -> None: def run_async() -> None: - item = self.completions[session_name][index] + item = LspSelectCompletionCommand.completions[session_name][index] session = self.session_by_name(session_name, 'completionProvider.resolveProvider') if session: request = Request.resolveCompletionItem(item, self.view) From 4b5ae80440cfe47896901d93ea26d1bc8685adbd Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 8 Feb 2023 23:11:09 +0100 Subject: [PATCH 3/9] avoid use of slow json.dumps --- plugin/core/views.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 32b07d4eb..9a49027ec 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -810,6 +810,14 @@ def make_command_link( return make_link(sublime.command_url(cmd, args), text, class_name, tooltip) +def make_documentation_link(text: str, index: int, session_name: str, view_id: int) -> str: + """A version of "make_command_link" optimized for specific use case to avoid slow json.dumps in the hot path.""" + args = '{{"view_id": {}, "command": "lsp_resolve_docs", "args": {{"index": {}, "session_name": "{}"}}}}'.format( + view_id, index, session_name) + href = 'subl:lsp_run_text_command_helper {}'.format(html.escape(args)) + return make_link(href, text) + + class LspRunTextCommandHelperCommand(sublime_plugin.WindowCommand): def run(self, view_id: int, command: str, args: Optional[Dict[str, Any]] = None) -> None: view = sublime.View(view_id) @@ -1040,8 +1048,7 @@ def format_completion( kind = COMPLETION_KINDS.get(completion_kind, sublime.KIND_AMBIGUOUS) if completion_kind else sublime.KIND_AMBIGUOUS details = [] # type: List[str] if can_resolve_completion_items or item.get('documentation'): - details.append(make_command_link( - 'lsp_resolve_docs', "More", {'index': index, 'session_name': session_name}, view_id=view_id)) + details.append(make_documentation_link("More", index, session_name, view_id)) if lsp_label_detail and (lsp_label + lsp_label_detail).startswith(lsp_filter_text): trigger = lsp_label + lsp_label_detail annotation = lsp_label_description or lsp_detail From 49ae9c782f0b1ca91000d67260bf433e6748718a Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 8 Feb 2023 23:15:55 +0100 Subject: [PATCH 4/9] remove use of html.escape --- plugin/core/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 9a49027ec..4d9616031 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -814,7 +814,7 @@ def make_documentation_link(text: str, index: int, session_name: str, view_id: i """A version of "make_command_link" optimized for specific use case to avoid slow json.dumps in the hot path.""" args = '{{"view_id": {}, "command": "lsp_resolve_docs", "args": {{"index": {}, "session_name": "{}"}}}}'.format( view_id, index, session_name) - href = 'subl:lsp_run_text_command_helper {}'.format(html.escape(args)) + href = 'subl:lsp_run_text_command_helper {}'.format(args) return make_link(href, text) @@ -1071,7 +1071,7 @@ def format_completion( if text_edit and 'insert' in text_edit and 'replace' in text_edit: insert_mode = userprefs().completion_insert_mode oposite_insert_mode = 'Replace' if insert_mode == 'insert' else 'Insert' - command_url = sublime.command_url("lsp_commit_completion_with_opposite_insert_mode") + command_url = "subl:lsp_commit_completion_with_opposite_insert_mode" details.append("{}".format(command_url, oposite_insert_mode)) completion = sublime.CompletionItem.command_completion( trigger=trigger, From 8f588458f1620fb7f4c30d127412cdb8ed31e18b Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 8 Feb 2023 23:18:09 +0100 Subject: [PATCH 5/9] no spaces --- plugin/core/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 4d9616031..55d97867d 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -812,7 +812,7 @@ def make_command_link( def make_documentation_link(text: str, index: int, session_name: str, view_id: int) -> str: """A version of "make_command_link" optimized for specific use case to avoid slow json.dumps in the hot path.""" - args = '{{"view_id": {}, "command": "lsp_resolve_docs", "args": {{"index": {}, "session_name": "{}"}}}}'.format( + args = '{{"view_id":{},"command":"lsp_resolve_docs","args":{{"index":{},"session_name":"{}"}}}}'.format( view_id, index, session_name) href = 'subl:lsp_run_text_command_helper {}'.format(args) return make_link(href, text) From 270a2ede0d4ec358f5421df9618d04d113dff4cc Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 8 Feb 2023 23:23:37 +0100 Subject: [PATCH 6/9] no need to pass the text --- plugin/core/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 55d97867d..b1d9c403e 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -810,12 +810,12 @@ def make_command_link( return make_link(sublime.command_url(cmd, args), text, class_name, tooltip) -def make_documentation_link(text: str, index: int, session_name: str, view_id: int) -> str: +def make_documentation_link(index: int, session_name: str, view_id: int) -> str: """A version of "make_command_link" optimized for specific use case to avoid slow json.dumps in the hot path.""" args = '{{"view_id":{},"command":"lsp_resolve_docs","args":{{"index":{},"session_name":"{}"}}}}'.format( view_id, index, session_name) href = 'subl:lsp_run_text_command_helper {}'.format(args) - return make_link(href, text) + return make_link(href, 'More') class LspRunTextCommandHelperCommand(sublime_plugin.WindowCommand): @@ -1048,7 +1048,7 @@ def format_completion( kind = COMPLETION_KINDS.get(completion_kind, sublime.KIND_AMBIGUOUS) if completion_kind else sublime.KIND_AMBIGUOUS details = [] # type: List[str] if can_resolve_completion_items or item.get('documentation'): - details.append(make_documentation_link("More", index, session_name, view_id)) + details.append(make_documentation_link(index, session_name, view_id)) if lsp_label_detail and (lsp_label + lsp_label_detail).startswith(lsp_filter_text): trigger = lsp_label + lsp_label_detail annotation = lsp_label_description or lsp_detail From 846508e9487c2da22871c7d1667675b93351707a Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 8 Feb 2023 23:25:02 +0100 Subject: [PATCH 7/9] inline --- plugin/core/views.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index b1d9c403e..790a30476 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -810,14 +810,6 @@ def make_command_link( return make_link(sublime.command_url(cmd, args), text, class_name, tooltip) -def make_documentation_link(index: int, session_name: str, view_id: int) -> str: - """A version of "make_command_link" optimized for specific use case to avoid slow json.dumps in the hot path.""" - args = '{{"view_id":{},"command":"lsp_resolve_docs","args":{{"index":{},"session_name":"{}"}}}}'.format( - view_id, index, session_name) - href = 'subl:lsp_run_text_command_helper {}'.format(args) - return make_link(href, 'More') - - class LspRunTextCommandHelperCommand(sublime_plugin.WindowCommand): def run(self, view_id: int, command: str, args: Optional[Dict[str, Any]] = None) -> None: view = sublime.View(view_id) @@ -1048,7 +1040,11 @@ def format_completion( kind = COMPLETION_KINDS.get(completion_kind, sublime.KIND_AMBIGUOUS) if completion_kind else sublime.KIND_AMBIGUOUS details = [] # type: List[str] if can_resolve_completion_items or item.get('documentation'): - details.append(make_documentation_link(index, session_name, view_id)) + # Not using "make_command_link" in a hot path to avoid slow json.dumps. + args = '{{"view_id":{},"command":"lsp_resolve_docs","args":{{"index":{},"session_name":"{}"}}}}'.format( + view_id, index, session_name) + href = 'subl:lsp_run_text_command_helper {}'.format(args) + details.append(make_link(href, 'More')) if lsp_label_detail and (lsp_label + lsp_label_detail).startswith(lsp_filter_text): trigger = lsp_label + lsp_label_detail annotation = lsp_label_description or lsp_detail From 6fb684e938ed2ed39c0e86378bc34a745de694a0 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 8 Feb 2023 23:34:23 +0100 Subject: [PATCH 8/9] optimize completion creation --- plugin/core/views.py | 16 +++++++++------- stubs/sublime.pyi | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 790a30476..b43ddf288 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1069,13 +1069,15 @@ def format_completion( oposite_insert_mode = 'Replace' if insert_mode == 'insert' else 'Insert' command_url = "subl:lsp_commit_completion_with_opposite_insert_mode" details.append("{}".format(command_url, oposite_insert_mode)) - completion = sublime.CompletionItem.command_completion( - trigger=trigger, - command='lsp_select_completion', - args={"index": index, "session_name": session_name}, - annotation=annotation, - kind=kind, - details=" | ".join(details)) + completion = sublime.CompletionItem( + trigger, + annotation, + # Not using "make_command_link" in a hot path to avoid slow json.dumps. + 'subl:lsp_select_completion {{"index":{},"session_name":"{}"}}'.format(index, session_name), + sublime.COMPLETION_FORMAT_COMMAND, + kind, + details=" | ".join(details) + ) if text_edit: completion.flags = sublime.COMPLETION_FLAG_KEEP_PREFIX return completion diff --git a/stubs/sublime.pyi b/stubs/sublime.pyi index 2b2a9aee4..9c4e45efd 100644 --- a/stubs/sublime.pyi +++ b/stubs/sublime.pyi @@ -63,6 +63,7 @@ INHIBIT_EXPLICIT_COMPLETIONS = ... # type: int INHIBIT_REORDER = ... # type: int DYNAMIC_COMPLETIONS = ... # type: int COMPLETION_FLAG_KEEP_PREFIX = ... # type: int +COMPLETION_FORMAT_COMMAND = ... # type: int DIALOG_CANCEL = ... # type: int DIALOG_YES = ... # type: int DIALOG_NO = ... # type: int From 2f32ab64c8df5ec35556938c4a9b211151411554 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 8 Feb 2023 23:47:11 +0100 Subject: [PATCH 9/9] fix --- plugin/core/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index b43ddf288..385cccb35 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1072,8 +1072,8 @@ def format_completion( completion = sublime.CompletionItem( trigger, annotation, - # Not using "make_command_link" in a hot path to avoid slow json.dumps. - 'subl:lsp_select_completion {{"index":{},"session_name":"{}"}}'.format(index, session_name), + # Not using "sublime.format_command" in a hot path to avoid slow json.dumps. + 'lsp_select_completion {{"index":{},"session_name":"{}"}}'.format(index, session_name), sublime.COMPLETION_FORMAT_COMMAND, kind, details=" | ".join(details)