From 268bd5dccbca813de97b9911e3d73a43b77d3371 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 30 Aug 2022 23:19:32 +0200 Subject: [PATCH 1/4] Cut 1.18.0 --- VERSION | 2 +- messages.json | 1 + messages/1.18.0.txt | 16 ++++++++++++++++ plugin/core/version.py | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 messages/1.18.0.txt diff --git a/VERSION b/VERSION index 73d74673c..744068368 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.17.0 \ No newline at end of file +1.18.0 \ No newline at end of file diff --git a/messages.json b/messages.json index 1eb61b8e6..f73ef68e1 100644 --- a/messages.json +++ b/messages.json @@ -12,6 +12,7 @@ "1.16.2": "messages/1.16.2.txt", "1.16.3": "messages/1.16.3.txt", "1.17.0": "messages/1.17.0.txt", + "1.18.0": "messages/1.18.0.txt", "1.2.7": "messages/1.2.7.txt", "1.2.8": "messages/1.2.8.txt", "1.2.9": "messages/1.2.9.txt", diff --git a/messages/1.18.0.txt b/messages/1.18.0.txt new file mode 100644 index 000000000..6680360d4 --- /dev/null +++ b/messages/1.18.0.txt @@ -0,0 +1,16 @@ +=> 1.18.0 + +# Features and Fixes + +- Implement inlay hints (#2018) (predragnikolic & jwortmann) (documentation: https://lsp.sublimetext.io/features/#inlay-hints) +- Add option to highlight hover range (#2030) (jwortmann) +- Optionally fallback to goto_reference in lsp_symbol_references (#2029) (timfjord) +- Delay re-calculation of code lenses and inlay hints for currently not visible views (#2025) (jwortmann) +- Improve strategy for semantic highlighting requests (#2020) (jwortmann) +- Follow global settings more accurately whether to show snippet completions (#2017) (jwortmann) +- docs: Add ruby steep language server (#2019) (jalkoby) +- docs: Update F# guidance (#2011) (baronfel) + +# API changes + +- Define overridable methods in LspExecuteCommand (#2024) (rchl) diff --git a/plugin/core/version.py b/plugin/core/version.py index 0d9ab3688..214022177 100644 --- a/plugin/core/version.py +++ b/plugin/core/version.py @@ -1 +1 @@ -__version__ = (1, 17, 0) +__version__ = (1, 18, 0) From fe36b2d2c05126e9469fa3882dbff6731ce4a6b8 Mon Sep 17 00:00:00 2001 From: jwortmann Date: Sun, 4 Sep 2022 23:31:16 +0200 Subject: [PATCH 2/4] Add icons and isPreferred support for code actions (#2040) Co-authored-by: Rafal Chlodnicki --- plugin/code_actions.py | 17 +++++++++++------ plugin/core/protocol.py | 22 +++++++++++----------- plugin/core/sessions.py | 1 + plugin/core/views.py | 26 ++++++++++++++++++++++++++ plugin/documents.py | 13 +++++++++---- plugin/hover.py | 13 +++++++++---- 6 files changed, 67 insertions(+), 25 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index bdb56c223..ba6d92ef8 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -11,6 +11,7 @@ from .core.typing import Any, List, Dict, Callable, Optional, Tuple, Union, Sequence from .core.views import entire_content_region from .core.views import first_selection_region +from .core.views import format_code_actions_for_quick_panel from .core.views import text_document_code_action_params from .save_command import LspSaveCommand, SaveTask import sublime @@ -274,7 +275,7 @@ def run( only_kinds: Optional[List[str]] = None, commands_by_config: Optional[CodeActionsByConfigName] = None ) -> None: - self.commands = [] # type: List[Tuple[str, str, CodeActionOrCommand]] + self.commands = [] # type: List[Tuple[str, CodeActionOrCommand]] self.commands_by_config = {} # type: CodeActionsByConfigName if commands_by_config: self.handle_responses_async(commands_by_config, run_first=True) @@ -299,19 +300,23 @@ def handle_responses_async(self, responses: CodeActionsByConfigName, run_first: else: self.show_code_actions() - def combine_commands(self) -> 'List[Tuple[str, str, CodeActionOrCommand]]': + def combine_commands(self) -> 'List[Tuple[str, CodeActionOrCommand]]': results = [] for config, commands in self.commands_by_config.items(): for command in commands: - results.append((config, command['title'], command)) + results.append((config, command)) return results def show_code_actions(self) -> None: if len(self.commands) > 0: - items = [command[1] for command in self.commands] window = self.view.window() if window: - window.show_quick_panel(items, self.handle_select, placeholder="Code action") + items, selected_index = format_code_actions_for_quick_panel([command[1] for command in self.commands]) + window.show_quick_panel( + items, + self.handle_select, + selected_index=selected_index, + placeholder="Code action") else: self.view.show_popup('No actions available', sublime.HIDE_ON_MOUSE_MOVE_AWAY) @@ -323,7 +328,7 @@ def run_async() -> None: session = self.session_by_name(selected[0]) if session: name = session.config.name - session.run_code_action_async(selected[2], progress=True).then( + session.run_code_action_async(selected[1], progress=True).then( lambda resp: self.handle_response_async(name, resp)) sublime.set_timeout_async(run_async) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 984ed77e4..7ff5990c0 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -199,17 +199,6 @@ class SemanticTokenModifiers: }, total=True) -CodeAction = TypedDict('CodeAction', { - 'title': str, - 'kind': Optional[str], - 'diagnostics': Optional[List[Any]], - 'isPreferred': Optional[bool], - 'disabled': Optional[CodeActionDisabledInformation], - 'edit': Optional[dict], - 'command': Optional[Command], -}, total=True) - - CodeLens = TypedDict('CodeLens', { 'range': RangeLsp, 'command': Optional[Command], @@ -296,6 +285,17 @@ class SemanticTokenModifiers: 'relatedInformation': List[DiagnosticRelatedInformation] }, total=False) +CodeAction = TypedDict('CodeAction', { + 'title': str, + 'kind': str, # NotRequired + 'diagnostics': List[Diagnostic], # NotRequired + 'isPreferred': bool, # NotRequired + 'disabled': CodeActionDisabledInformation, # NotRequired + 'edit': dict, # NotRequired + 'command': Command, # NotRequired + 'data': Any # NotRequired +}, total=False) + TextEdit = TypedDict('TextEdit', { 'newText': str, 'range': RangeLsp diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 4c5ee436e..93d8323b5 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -321,6 +321,7 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor }, "dataSupport": True, "disabledSupport": True, + "isPreferredSupport": True, "resolveSupport": { "properties": [ "edit" diff --git a/plugin/core/views.py b/plugin/core/views.py index 77cc09b4f..ce86f6ee9 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1,4 +1,6 @@ from .css import css as lsp_css +from .protocol import CodeAction +from .protocol import Command from .protocol import CompletionItem from .protocol import CompletionItemKind from .protocol import CompletionItemTag @@ -87,6 +89,10 @@ KIND_VALUE = (sublime.KIND_ID_VARIABLE, "v", "Value") KIND_VARIABLE = (sublime.KIND_ID_VARIABLE, "v", "Variable") +KIND_QUICKFIX = (sublime.KIND_ID_COLOR_YELLOWISH, "f", "QuickFix") +KIND_REFACTOR = (sublime.KIND_ID_COLOR_CYANISH, "r", "Refactor") +KIND_SOURCE = (sublime.KIND_ID_COLOR_PURPLISH, "s", "Source") + KIND_UNSPECIFIED = (sublime.KIND_ID_AMBIGUOUS, "?", "???") COMPLETION_KINDS = { @@ -146,6 +152,12 @@ SymbolKind.TypeParameter: KIND_TYPEPARAMETER } +CODE_ACTION_KINDS = { + "quickfix": KIND_QUICKFIX, + "refactor": KIND_REFACTOR, + "source": KIND_SOURCE +} + SYMBOL_KIND_SCOPES = { SymbolKind.File: "string", SymbolKind.Module: "entity.name.namespace", @@ -988,3 +1000,17 @@ def format_completion( if item.get('textEdit'): completion.flags = sublime.COMPLETION_FLAG_KEEP_PREFIX return completion + + +def format_code_actions_for_quick_panel( + code_actions: List[Union[CodeAction, Command]] +) -> Tuple[List[sublime.QuickPanelItem], int]: + items = [] # type: List[sublime.QuickPanelItem] + selected_index = -1 + for idx, code_action in enumerate(code_actions): + lsp_kind = str(code_action.get("kind", "")) + kind = CODE_ACTION_KINDS.get(lsp_kind.split(".")[0], sublime.KIND_AMBIGUOUS) + items.append(sublime.QuickPanelItem(code_action["title"], kind=kind)) + if code_action.get('isPreferred', False): + selected_index = idx + return items, selected_index diff --git a/plugin/documents.py b/plugin/documents.py index 784ef4523..fc2d7ba08 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -33,6 +33,7 @@ from .core.views import DOCUMENT_HIGHLIGHT_KIND_SCOPES from .core.views import DOCUMENT_HIGHLIGHT_KINDS from .core.views import first_selection_region +from .core.views import format_code_actions_for_quick_panel from .core.views import format_completion from .core.views import make_command_link from .core.views import MarkdownLangMap @@ -598,12 +599,16 @@ def _clear_code_actions_annotation(self) -> None: def _on_navigate(self, href: str, point: int) -> None: if href.startswith('code-actions:'): _, config_name = href.split(":") - titles = [command["title"] for command in self._actions_by_config[config_name]] - if len(titles) > 1: + actions = self._actions_by_config[config_name] + if len(actions) > 1: window = self.view.window() if window: - window.show_quick_panel(titles, lambda i: self.handle_code_action_select(config_name, i), - placeholder="Code actions") + items, selected_index = format_code_actions_for_quick_panel(actions) + window.show_quick_panel( + items, + lambda i: self.handle_code_action_select(config_name, i), + selected_index=selected_index, + placeholder="Code actions") else: self.handle_code_action_select(config_name, 0) diff --git a/plugin/hover.py b/plugin/hover.py index c3bbb462a..85e2884ca 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -21,6 +21,7 @@ from .core.typing import List, Optional, Dict, Tuple, Sequence, Union from .core.views import diagnostic_severity from .core.views import first_selection_region +from .core.views import format_code_actions_for_quick_panel from .core.views import format_diagnostic_for_html from .core.views import FORMAT_MARKED_STRING from .core.views import FORMAT_MARKUP_CONTENT @@ -338,13 +339,17 @@ def _on_navigate(self, href: str, point: int) -> None: window.open_file(fn, flags=sublime.ENCODED_POSITION) elif href.startswith('code-actions:'): _, config_name = href.split(":") - titles = [command["title"] for command in self._actions_by_config[config_name]] + actions = self._actions_by_config[config_name] self.view.run_command("lsp_selection_set", {"regions": [(point, point)]}) - if len(titles) > 1: + if len(actions) > 1: window = self.view.window() if window: - window.show_quick_panel(titles, lambda i: self.handle_code_action_select(config_name, i), - placeholder="Code actions") + items, selected_index = format_code_actions_for_quick_panel(actions) + window.show_quick_panel( + items, + lambda i: self.handle_code_action_select(config_name, i), + selected_index=selected_index, + placeholder="Code actions") else: self.handle_code_action_select(config_name, 0) elif is_location_href(href): From 59dbd905d22770e66c3ceb17555cdc9315dca4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Wed, 7 Sep 2022 20:41:00 +0200 Subject: [PATCH 3/4] Custom context menu in log panel and "Clear log panel" item (#2045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Chłodnicki --- Context LSP Log Panel.sublime-menu | 18 +++++++ LSP.sublime-settings | 2 +- boot.py | 3 +- plugin/core/panels.py | 75 +++++++++++++++++------------ plugin/core/windows.py | 4 +- plugin/panels.py | 6 +-- sublime-package.json | 2 +- tests/test_server_panel_circular.py | 8 +-- 8 files changed, 77 insertions(+), 41 deletions(-) create mode 100644 Context LSP Log Panel.sublime-menu diff --git a/Context LSP Log Panel.sublime-menu b/Context LSP Log Panel.sublime-menu new file mode 100644 index 000000000..3cad99e26 --- /dev/null +++ b/Context LSP Log Panel.sublime-menu @@ -0,0 +1,18 @@ +[ + { + "command": "lsp_clear_log_panel", + "caption": "Clear Log Panel" + }, + { + "caption": "-" + }, + { + "command": "copy" + }, + { + "caption": "-" + }, + { + "command": "select_all" + }, +] diff --git a/LSP.sublime-settings b/LSP.sublime-settings index e58e6c743..72968ba68 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -177,7 +177,7 @@ // Log communication from and to language servers. // Set to an array of values: - // - "panel" - log to the LSP Language Servers output panel + // - "panel" - log to the LSP Log Panel // - "remote" - start a local websocket server on port 9981. Can be connected to with // a websocket client to receive the log messages in real time. // For backward-compatibility, when set to "true", enables the "panel" logger and when diff --git a/boot.py b/boot.py index 8a85cd502..7fac66db4 100644 --- a/boot.py +++ b/boot.py @@ -16,9 +16,10 @@ from .plugin.core.logging import exception_log from .plugin.core.open import opening_files from .plugin.core.panels import destroy_output_panels +from .plugin.core.panels import LspClearLogPanelCommand from .plugin.core.panels import LspClearPanelCommand from .plugin.core.panels import LspUpdatePanelCommand -from .plugin.core.panels import LspUpdateServerPanelCommand +from .plugin.core.panels import LspUpdateLogPanelCommand from .plugin.core.panels import WindowPanelListener from .plugin.core.protocol import Error from .plugin.core.protocol import Location diff --git a/plugin/core/panels.py b/plugin/core/panels.py index 242a7275f..29c0741c9 100644 --- a/plugin/core/panels.py +++ b/plugin/core/panels.py @@ -5,7 +5,7 @@ # about 80 chars per line implies maintaining a buffer of about 40kb per window -SERVER_PANEL_MAX_LINES = 500 +LOG_PANEL_MAX_LINES = 500 OUTPUT_PANEL_SETTINGS = { "auto_indent": False, @@ -32,7 +32,7 @@ class PanelName: Diagnostics = "diagnostics" References = "references" Rename = "rename" - LanguageServers = "language servers" + Log = "LSP Log Panel" @contextmanager @@ -66,13 +66,13 @@ def on_pre_close_window(self, window: sublime.Window) -> None: def on_window_command(self, window: sublime.Window, command_name: str, args: Dict) -> None: if command_name in ('show_panel', 'hide_panel'): - sublime.set_timeout(lambda: self.maybe_update_server_panel(window)) + sublime.set_timeout(lambda: self.maybe_update_log_panel(window)) - def maybe_update_server_panel(self, window: sublime.Window) -> None: - if is_server_panel_open(window): - panel = ensure_server_panel(window) + def maybe_update_log_panel(self, window: sublime.Window) -> None: + if is_log_panel_open(window): + panel = ensure_log_panel(window) if panel: - update_server_panel(panel, window.id()) + update_log_panel(panel, window.id()) def create_output_panel(window: sublime.Window, name: str) -> Optional[sublime.View]: @@ -93,27 +93,31 @@ def destroy_output_panels(window: sublime.Window) -> None: def create_panel(window: sublime.Window, name: str, result_file_regex: str, result_line_regex: str, - syntax: str) -> Optional[sublime.View]: + syntax: str, context_menu: Optional[str] = None) -> Optional[sublime.View]: panel = create_output_panel(window, name) if not panel: return None + settings = panel.settings() if result_file_regex: - panel.settings().set("result_file_regex", result_file_regex) + settings.set("result_file_regex", result_file_regex) if result_line_regex: - panel.settings().set("result_line_regex", result_line_regex) + settings.set("result_line_regex", result_line_regex) + if context_menu: + settings.set("context_menu", context_menu) panel.assign_syntax(syntax) - # Call create_output_panel a second time after assigning the above - # settings, so that it'll be picked up as a result buffer - # see: Packages/Default/exec.py#L228-L230 + # Call create_output_panel a second time after assigning the above settings, so that it'll be picked up + # as a result buffer. See: Packages/Default/exec.py#L228-L230 panel = window.create_output_panel(name) - # All our panels are read-only - panel.set_read_only(True) + if panel: + # All our panels are read-only + panel.set_read_only(True) return panel def ensure_panel(window: sublime.Window, name: str, result_file_regex: str, result_line_regex: str, - syntax: str) -> Optional[sublime.View]: - return window.find_output_panel(name) or create_panel(window, name, result_file_regex, result_line_regex, syntax) + syntax: str, context_menu: Optional[str] = None) -> Optional[sublime.View]: + return window.find_output_panel(name) or \ + create_panel(window, name, result_file_regex, result_line_regex, syntax, context_menu) class LspClearPanelCommand(sublime_plugin.TextCommand): @@ -143,12 +147,13 @@ def run(self, edit: sublime.Edit, characters: Optional[str] = "") -> None: clear_undo_stack(self.view) -def ensure_server_panel(window: sublime.Window) -> Optional[sublime.View]: - return ensure_panel(window, PanelName.LanguageServers, "", "", "Packages/LSP/Syntaxes/ServerLog.sublime-syntax") +def ensure_log_panel(window: sublime.Window) -> Optional[sublime.View]: + return ensure_panel(window, PanelName.Log, "", "", "Packages/LSP/Syntaxes/ServerLog.sublime-syntax", + "Context LSP Log Panel.sublime-menu") -def is_server_panel_open(window: sublime.Window) -> bool: - return window.is_valid() and window.active_panel() == "output.{}".format(PanelName.LanguageServers) +def is_log_panel_open(window: sublime.Window) -> bool: + return window.is_valid() and window.active_panel() == "output.{}".format(PanelName.Log) def log_server_message(window: sublime.Window, prefix: str, message: str) -> None: @@ -157,19 +162,19 @@ def log_server_message(window: sublime.Window, prefix: str, message: str) -> Non return WindowPanelListener.server_log_map[window_id].append((prefix, message)) list_len = len(WindowPanelListener.server_log_map[window_id]) - if list_len >= SERVER_PANEL_MAX_LINES: + if list_len >= LOG_PANEL_MAX_LINES: # Trim leading items in the list, leaving only the max allowed count. - del WindowPanelListener.server_log_map[window_id][:list_len - SERVER_PANEL_MAX_LINES] - panel = ensure_server_panel(window) - if is_server_panel_open(window) and panel: - update_server_panel(panel, window_id) + del WindowPanelListener.server_log_map[window_id][:list_len - LOG_PANEL_MAX_LINES] + panel = ensure_log_panel(window) + if is_log_panel_open(window) and panel: + update_log_panel(panel, window_id) -def update_server_panel(panel: sublime.View, window_id: int) -> None: - panel.run_command("lsp_update_server_panel", {"window_id": window_id}) +def update_log_panel(panel: sublime.View, window_id: int) -> None: + panel.run_command("lsp_update_log_panel", {"window_id": window_id}) -class LspUpdateServerPanelCommand(sublime_plugin.TextCommand): +class LspUpdateLogPanelCommand(sublime_plugin.TextCommand): def run(self, edit: sublime.Edit, window_id: int) -> None: to_process = WindowPanelListener.server_log_map.get(window_id) or [] @@ -183,7 +188,7 @@ def run(self, edit: sublime.Edit, window_id: int) -> None: self.view.insert(edit, self.view.size(), ''.join(new_lines)) last_region_end = 0 # Starting from point 0 in the panel ... total_lines, _ = self.view.rowcol(self.view.size()) - for _ in range(0, max(0, total_lines - SERVER_PANEL_MAX_LINES)): + for _ in range(0, max(0, total_lines - LOG_PANEL_MAX_LINES)): # ... collect all regions that span an entire line ... region = self.view.full_line(last_region_end) last_region_end = region.b @@ -191,3 +196,13 @@ def run(self, edit: sublime.Edit, window_id: int) -> None: if not erase_region.empty(): self.view.erase(edit, erase_region) clear_undo_stack(self.view) + + +class LspClearLogPanelCommand(sublime_plugin.TextCommand): + def run(self, edit: sublime.Edit) -> None: + window = self.view.window() + if not window: + return + panel = ensure_log_panel(window) + if panel: + panel.run_command("lsp_clear_panel") diff --git a/plugin/core/windows.py b/plugin/core/windows.py index f6463719d..0c029f73d 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -6,6 +6,7 @@ from .logging import debug from .logging import exception_log from .message_request_handler import MessageRequestHandler +from .panels import ensure_log_panel from .panels import log_server_message from .promise import Promise from .protocol import DocumentUri @@ -28,8 +29,8 @@ from .views import make_link from .workspace import ProjectFolders from .workspace import sorted_workspace_folders -from collections import OrderedDict from collections import deque +from collections import OrderedDict from subprocess import CalledProcessError from time import time from weakref import ref @@ -82,6 +83,7 @@ def __init__( self.total_error_count = 0 self.total_warning_count = 0 sublime.set_timeout(functools.partial(self._update_panel_main_thread, _NO_DIAGNOSTICS_PLACEHOLDER, [])) + ensure_log_panel(window) def get_config_manager(self) -> WindowConfigManager: return self._configs diff --git a/plugin/panels.py b/plugin/panels.py index 56028ea2f..e30019fd0 100644 --- a/plugin/panels.py +++ b/plugin/panels.py @@ -1,5 +1,5 @@ from .core.diagnostics import ensure_diagnostics_panel -from .core.panels import ensure_server_panel +from .core.panels import ensure_log_panel from .core.panels import PanelName from sublime import Window from sublime_plugin import WindowCommand @@ -13,8 +13,8 @@ def toggle_output_panel(window: Window, panel_type: str) -> None: class LspToggleServerPanelCommand(WindowCommand): def run(self) -> None: - ensure_server_panel(self.window) - toggle_output_panel(self.window, PanelName.LanguageServers) + ensure_log_panel(self.window) + toggle_output_panel(self.window, PanelName.Log) class LspShowDiagnosticsPanelCommand(WindowCommand): diff --git a/sublime-package.json b/sublime-package.json index 4fd4b2b70..8e86f0467 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -499,7 +499,7 @@ "deprecationMessage": "Use an array instead." } ], - "markdownDescription": "Log communication from and to language servers. Possible flags:\n\n- `\"panel\"`: log to the LSP Language Servers output panel\n- `\"remote\"`: start a local websocket server on port 9981. Can be connected to with a websocket client to receive the log messages in real time.\n\nFor backward-compatibility, when set to `true`, enables the `\"panel\"` logger and when set to `false` disables logging. This output panel can be toggled from the command palette with the command **LSP: Toggle Log Panel**." + "markdownDescription": "Log communication from and to language servers. Possible flags:\n\n- `\"panel\"`: log to the LSP Log Panel\n- `\"remote\"`: start a local websocket server on port 9981. Can be connected to with a websocket client to receive the log messages in real time.\n\nFor backward-compatibility, when set to `true`, enables the `\"panel\"` logger and when set to `false` disables logging. This output panel can be toggled from the command palette with the command **LSP: Toggle Log Panel**." }, "log_max_size": { "type": "integer", diff --git a/tests/test_server_panel_circular.py b/tests/test_server_panel_circular.py index 6b1d8d18b..a59ef40c1 100644 --- a/tests/test_server_panel_circular.py +++ b/tests/test_server_panel_circular.py @@ -1,5 +1,5 @@ -from LSP.plugin.core.panels import ensure_server_panel -from LSP.plugin.core.panels import SERVER_PANEL_MAX_LINES +from LSP.plugin.core.panels import ensure_log_panel +from LSP.plugin.core.panels import LOG_PANEL_MAX_LINES from LSP.plugin.core.panels import log_server_message from unittesting import DeferrableTestCase import sublime @@ -14,7 +14,7 @@ def setUp(self): self.skipTest("window is None!") return self.view = self.window.active_view() - panel = ensure_server_panel(self.window) + panel = ensure_log_panel(self.window) if panel is None: self.skipTest("panel is None!") return @@ -29,7 +29,7 @@ def update_panel(self, msg: str) -> None: log_server_message(self.window, "test", msg) def test_server_panel_circular_behavior(self): - n = SERVER_PANEL_MAX_LINES + n = LOG_PANEL_MAX_LINES for i in range(0, n + 1): self.update_panel(str(i)) self.update_panel("overflow") From d8bfe6269f97616d27bcc795c18ea2a9655b067e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ch=C5=82odnicki?= Date: Wed, 7 Sep 2022 20:41:19 +0200 Subject: [PATCH 4/4] Add support for triggerKind in code action requests (#2042) --- plugin/code_actions.py | 28 +++++++++++++++------------- plugin/core/protocol.py | 17 +++++++++++++++++ plugin/core/sessions.py | 10 +++++----- plugin/core/views.py | 19 ++++++++++++------- plugin/documents.py | 14 ++++++++------ plugin/hover.py | 2 +- 6 files changed, 58 insertions(+), 32 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index ba6d92ef8..74a301e56 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -8,7 +8,7 @@ from .core.registry import windows from .core.sessions import SessionBufferProtocol from .core.settings import userprefs -from .core.typing import Any, List, Dict, Callable, Optional, Tuple, Union, Sequence +from .core.typing import Any, List, Dict, Callable, Optional, Tuple, Union from .core.views import entire_content_region from .core.views import first_selection_region from .core.views import format_code_actions_for_quick_panel @@ -82,15 +82,16 @@ def request_for_region_async( self, view: sublime.View, region: sublime.Region, - session_buffer_diagnostics: Sequence[Tuple[SessionBufferProtocol, Sequence[Diagnostic]]], + session_buffer_diagnostics: List[Tuple[SessionBufferProtocol, List[Diagnostic]]], actions_handler: Callable[[CodeActionsByConfigName], None], - only_kinds: Optional[Dict[str, bool]] = None + only_kinds: Optional[Dict[str, bool]] = None, + manual: bool = False, ) -> None: """ Requests code actions with provided diagnostics and specified region. If there are no diagnostics for given session, the request will be made with empty diagnostics list. """ - self._request_async(view, region, session_buffer_diagnostics, False, actions_handler, only_kinds) + self._request_async(view, region, session_buffer_diagnostics, False, actions_handler, only_kinds, manual) def request_on_save( self, @@ -106,19 +107,21 @@ def request_on_save( return region = entire_content_region(view) session_buffer_diagnostics, _ = listener.diagnostics_intersecting_region_async(region) - self._request_async(view, region, session_buffer_diagnostics, False, actions_handler, on_save_actions) + self._request_async( + view, region, session_buffer_diagnostics, False, actions_handler, on_save_actions, manual=False) def _request_async( self, view: sublime.View, region: sublime.Region, - session_buffer_diagnostics: Sequence[Tuple[SessionBufferProtocol, Sequence[Diagnostic]]], + session_buffer_diagnostics: List[Tuple[SessionBufferProtocol, List[Diagnostic]]], only_with_diagnostics: bool, actions_handler: Callable[[CodeActionsByConfigName], None], - on_save_actions: Optional[Dict[str, bool]] = None + on_save_actions: Optional[Dict[str, bool]] = None, + manual: bool = False, ) -> None: location_cache_key = None - use_cache = on_save_actions is None + use_cache = on_save_actions is None and not manual if use_cache: location_cache_key = "{}#{}:{}:{}".format( view.buffer_id(), view.change_count(), region, only_with_diagnostics) @@ -129,13 +132,12 @@ def _request_async( return else: self._response_cache = None - collector = CodeActionsCollector(actions_handler) with collector: listener = windows.listener_for_view(view) if listener: for session in listener.sessions_async('codeActionProvider'): - diagnostics = [] # type: Sequence[Diagnostic] + diagnostics = [] # type: List[Diagnostic] for sb, diags in session_buffer_diagnostics: if sb.session == session: diagnostics = diags @@ -144,14 +146,14 @@ def _request_async( supported_kinds = session.get_capability('codeActionProvider.codeActionKinds') matching_kinds = get_matching_kinds(on_save_actions, supported_kinds or []) if matching_kinds: - params = text_document_code_action_params(view, region, diagnostics, matching_kinds) + params = text_document_code_action_params(view, region, diagnostics, matching_kinds, manual) request = Request.codeAction(params, view) session.send_request_async( request, *filtering_collector(session.config.name, matching_kinds, collector)) else: if only_with_diagnostics and not diagnostics: continue - params = text_document_code_action_params(view, region, diagnostics) + params = text_document_code_action_params(view, region, diagnostics, None, manual) request = Request.codeAction(params, view) session.send_request_async(request, collector.create_collector(session.config.name)) if location_cache_key: @@ -290,7 +292,7 @@ def run( session_buffer_diagnostics, covering = listener.diagnostics_intersecting_async(region) dict_kinds = {kind: True for kind in only_kinds} if only_kinds else None actions_manager.request_for_region_async( - view, covering, session_buffer_diagnostics, self.handle_responses_async, dict_kinds) + view, covering, session_buffer_diagnostics, self.handle_responses_async, dict_kinds, manual=True) def handle_responses_async(self, responses: CodeActionsByConfigName, run_first: bool = False) -> None: self.commands_by_config = responses diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 7ff5990c0..623a5c5da 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -23,6 +23,11 @@ class DiagnosticTag: Deprecated = 2 +class CodeActionTriggerKind: + Invoked = 1 + Automatic = 2 + + class CompletionItemKind: Text = 1 Method = 2 @@ -296,6 +301,18 @@ class SemanticTokenModifiers: 'data': Any # NotRequired }, total=False) +CodeActionContext = TypedDict('CodeActionContext', { + 'diagnostics': List[Diagnostic], + 'only': List[str], # NotRequired + 'triggerKind': int, # NotRequired +}, total=False) + +CodeActionParams = TypedDict('CodeActionParams', { + 'textDocument': TextDocumentIdentifier, + 'range': RangeLsp, + 'context': CodeActionContext, +}, total=True) + TextEdit = TypedDict('TextEdit', { 'newText': str, 'range': RangeLsp diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 93d8323b5..2330dd009 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -53,7 +53,7 @@ from .types import method_to_capability from .types import SettingsRegistration from .types import sublime_pattern_to_glob -from .typing import Callable, cast, Dict, Any, Optional, List, Tuple, Generator, Iterable, Type, Protocol, Sequence, Mapping, Union # noqa: E501 +from .typing import Callable, cast, Dict, Any, Optional, List, Tuple, Generator, Iterable, Type, Protocol, Mapping, Union # noqa: E501 from .url import filename_to_uri from .url import parse_uri from .version import __version__ @@ -594,14 +594,14 @@ def on_session_shutdown_async(self, session: 'Session') -> None: @abstractmethod def diagnostics_async( self - ) -> Iterable[Tuple['SessionBufferProtocol', Sequence[Tuple[Diagnostic, sublime.Region]]]]: + ) -> Iterable[Tuple['SessionBufferProtocol', List[Tuple[Diagnostic, sublime.Region]]]]: raise NotImplementedError() @abstractmethod def diagnostics_intersecting_region_async( self, region: sublime.Region - ) -> Tuple[Sequence[Tuple['SessionBufferProtocol', Sequence[Diagnostic]]], sublime.Region]: + ) -> Tuple[List[Tuple['SessionBufferProtocol', List[Diagnostic]]], sublime.Region]: raise NotImplementedError() @abstractmethod @@ -609,13 +609,13 @@ def diagnostics_touching_point_async( self, pt: int, max_diagnostic_severity_level: int = DiagnosticSeverity.Hint - ) -> Tuple[Sequence[Tuple['SessionBufferProtocol', Sequence[Diagnostic]]], sublime.Region]: + ) -> Tuple[List[Tuple['SessionBufferProtocol', List[Diagnostic]]], sublime.Region]: raise NotImplementedError() def diagnostics_intersecting_async( self, region_or_point: Union[sublime.Region, int] - ) -> Tuple[Sequence[Tuple['SessionBufferProtocol', Sequence[Diagnostic]]], sublime.Region]: + ) -> Tuple[List[Tuple['SessionBufferProtocol', List[Diagnostic]]], sublime.Region]: if isinstance(region_or_point, int): return self.diagnostics_touching_point_async(region_or_point) elif region_or_point.empty(): diff --git a/plugin/core/views.py b/plugin/core/views.py index ce86f6ee9..9966e954a 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1,5 +1,8 @@ from .css import css as lsp_css from .protocol import CodeAction +from .protocol import CodeActionContext +from .protocol import CodeActionParams +from .protocol import CodeActionTriggerKind from .protocol import Command from .protocol import CompletionItem from .protocol import CompletionItemKind @@ -25,7 +28,7 @@ from .protocol import TextDocumentPositionParams from .settings import userprefs from .types import ClientConfig -from .typing import Callable, Optional, Dict, Any, Iterable, List, Union, Tuple, Sequence, cast +from .typing import Callable, Optional, Dict, Any, Iterable, List, Union, Tuple, cast from .url import parse_uri from .workspace import is_subpath_of import html @@ -544,14 +547,16 @@ def selection_range_params(view: sublime.View) -> Dict[str, Any]: def text_document_code_action_params( view: sublime.View, region: sublime.Region, - diagnostics: Sequence[Diagnostic], - on_save_actions: Optional[Sequence[str]] = None -) -> Dict[str, Any]: + diagnostics: List[Diagnostic], + on_save_actions: Optional[List[str]] = None, + manual: bool = False +) -> CodeActionParams: context = { - "diagnostics": diagnostics - } # type: Dict[str, Any] + "diagnostics": diagnostics, + "triggerKind": CodeActionTriggerKind.Invoked if manual else CodeActionTriggerKind.Automatic, + } # type: CodeActionContext if on_save_actions: - context['only'] = on_save_actions + context["only"] = on_save_actions return { "textDocument": text_document_identifier(view), "range": region_to_range(view, region).to_lsp(), diff --git a/plugin/documents.py b/plugin/documents.py index fc2d7ba08..9e7f6f1de 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -20,6 +20,7 @@ from .core.registry import windows from .core.sessions import AbstractViewListener from .core.sessions import Session +from .core.sessions import SessionBufferProtocol from .core.settings import userprefs from .core.signature_help import SigHelp from .core.types import basescope2languageid @@ -233,7 +234,7 @@ def on_session_shutdown_async(self, session: Session) -> None: def diagnostics_async( self - ) -> Generator[Tuple[SessionBuffer, List[Tuple[Diagnostic, sublime.Region]]], None, None]: + ) -> Generator[Tuple[SessionBufferProtocol, List[Tuple[Diagnostic, sublime.Region]]], None, None]: change_count = self.view.change_count() for sb in self.session_buffers_async(): # do not provide stale diagnostics @@ -243,9 +244,9 @@ def diagnostics_async( def diagnostics_intersecting_region_async( self, region: sublime.Region - ) -> Tuple[List[Tuple[SessionBuffer, List[Diagnostic]]], sublime.Region]: + ) -> Tuple[List[Tuple[SessionBufferProtocol, List[Diagnostic]]], sublime.Region]: covering = sublime.Region(region.begin(), region.end()) - result = [] # type: List[Tuple[SessionBuffer, List[Diagnostic]]] + result = [] # type: List[Tuple[SessionBufferProtocol, List[Diagnostic]]] for sb, diagnostics in self.diagnostics_async(): intersections = [] # type: List[Diagnostic] for diagnostic, candidate in diagnostics: @@ -262,9 +263,9 @@ def diagnostics_touching_point_async( self, pt: int, max_diagnostic_severity_level: int = DiagnosticSeverity.Hint - ) -> Tuple[List[Tuple[SessionBuffer, List[Diagnostic]]], sublime.Region]: + ) -> Tuple[List[Tuple[SessionBufferProtocol, List[Diagnostic]]], sublime.Region]: covering = sublime.Region(pt, pt) - result = [] # type: List[Tuple[SessionBuffer, List[Diagnostic]]] + result = [] # type: List[Tuple[SessionBufferProtocol, List[Diagnostic]]] for sb, diagnostics in self.diagnostics_async(): intersections = [] # type: List[Diagnostic] for diagnostic, candidate in diagnostics: @@ -564,7 +565,8 @@ def _on_sighelp_navigate(self, href: str) -> None: def _do_code_actions(self) -> None: diagnostics_by_config, covering = self.diagnostics_intersecting_async(self._stored_region) - actions_manager.request_for_region_async(self.view, covering, diagnostics_by_config, self._on_code_actions) + actions_manager.request_for_region_async( + self.view, covering, diagnostics_by_config, self._on_code_actions, manual=False) def _on_code_actions(self, responses: CodeActionsByConfigName) -> None: action_count = sum(map(len, responses.values())) diff --git a/plugin/hover.py b/plugin/hover.py index 85e2884ca..b4411ca89 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -145,7 +145,7 @@ def run_async() -> None: if not only_diagnostics and userprefs().show_code_actions_in_hover: actions_manager.request_for_region_async( self.view, covering, self._diagnostics_by_config, - functools.partial(self.handle_code_actions, listener, hover_point)) + functools.partial(self.handle_code_actions, listener, hover_point), manual=False) sublime.set_timeout_async(run_async)