diff --git a/LSP.sublime-settings b/LSP.sublime-settings index 6cb35033c..66e72025b 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -163,6 +163,9 @@ // --- Other -------------------------------------------------------------------------- + // Replace error popup windows with a console message and a short status notification. + "suppress_error_dialogs": false, + // Show symbol references in Sublime's quick panel instead of the output panel. "show_references_in_quick_panel": true, diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 2056a866f..9c0eaed4f 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -1,4 +1,5 @@ from __future__ import annotations +from .core.logging_notify import notify_error from .core.promise import Promise from .core.protocol import CodeAction from .core.protocol import CodeActionKind @@ -352,7 +353,8 @@ def run_async() -> None: def _handle_response_async(self, session_name: str, response: Any) -> None: if isinstance(response, Error): - sublime.error_message(f"{session_name}: {str(response)}") + message = f"{session_name}: {str(response)}" + notify_error(self.view.window(), message) # This command must be a WindowCommand in order to reliably hide corresponding menu entries when no view has focus. @@ -414,7 +416,8 @@ def run_async(self, index: int, event: dict | None) -> None: def _handle_response_async(self, session_name: str, response: Any) -> None: if isinstance(response, Error): - sublime.error_message(f"{session_name}: {str(response)}") + message = f"{session_name}: {str(response)}" + notify_error(self.window, message) def _is_cache_valid(self, event: dict | None) -> bool: view = self.view diff --git a/plugin/core/logging_notify.py b/plugin/core/logging_notify.py new file mode 100644 index 000000000..09d26f0b3 --- /dev/null +++ b/plugin/core/logging_notify.py @@ -0,0 +1,18 @@ +from __future__ import annotations +import sublime +from .settings import userprefs + + +def notify_error(window: sublime.Window | None, message: str, status_message: str | None = None) -> None: + """Pick either of the 2 ways to show a user error notification message: + - via a detailed console message and a short status message + - via a blocking error modal dialog""" + if not window: + return + if status_message is None: + status_message = message + if userprefs().suppress_error_dialogs: + window.status_message(status_message) + print("LSP: " + message) + else: + sublime.error_message(message) diff --git a/plugin/core/types.py b/plugin/core/types.py index b5d5b932f..9b4cb1de7 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -232,6 +232,7 @@ class Settings: show_code_actions_in_hover = cast(bool, None) show_diagnostics_annotations_severity_level = cast(int, None) show_diagnostics_count_in_view_status = cast(bool, None) + suppress_error_dialogs = cast(bool, None) show_multiline_diagnostics_highlights = cast(bool, None) show_multiline_document_highlights = cast(bool, None) show_diagnostics_in_view_status = cast(bool, None) @@ -278,6 +279,7 @@ def r(name: str, default: bool | int | str | list | dict) -> None: r("show_code_actions_in_hover", True) r("show_diagnostics_annotations_severity_level", 0) r("show_diagnostics_count_in_view_status", False) + r("suppress_error_dialogs", True) r("show_diagnostics_in_view_status", True) r("show_multiline_diagnostics_highlights", True) r("show_multiline_document_highlights", True) diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 9234724ef..60c13444a 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -7,6 +7,7 @@ from .diagnostics_storage import is_severity_included from .logging import debug from .logging import exception_log +from .logging_notify import notify_error from .message_request_handler import MessageRequestHandler from .panels import LOG_LINES_LIMIT_SETTING_NAME from .panels import MAX_LOG_LINES_LIMIT_OFF @@ -295,12 +296,13 @@ def start_async(self, config: ClientConfig, initiating_view: sublime.View) -> No "Re-enable by running \"LSP: Enable Language Server In Project\" from the Command Palette.", "\n\n--- Error: ---\n{1}" )).format(config.name, str(e)) + status = f"Failed to start {config.name}… See console" exception_log(f"Unable to start subprocess for {config.name}", e) if isinstance(e, CalledProcessError): print("Server output:\n{}".format(e.output.decode('utf-8', 'replace'))) self._config_manager.disable_config(config.name, only_for_session=True) config.erase_view_status(initiating_view) - sublime.message_dialog(message) + notify_error(self._window, message, status) # Continue with handling pending listeners self._new_session = None sublime.set_timeout_async(self._dequeue_listener_async) diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py index e0d2b7a10..8ca1ea865 100644 --- a/plugin/core/workspace.py +++ b/plugin/core/workspace.py @@ -4,6 +4,7 @@ from .types import matches_pattern from .types import sublime_pattern_to_glob from .url import filename_to_uri +from .logging_notify import notify_error from typing import Any import sublime import os @@ -146,8 +147,9 @@ def enable_in_project(window: sublime.Window, config_name: str) -> None: project_client_settings['enabled'] = True window.set_project_data(project_data) else: - sublime.message_dialog( - f"Can't enable {config_name} in the current workspace. Ensure that the project is saved first.") + message = f"Can't enable {config_name} in the current workspace. Ensure that the project is saved first." + status = f"Can't enable {config_name} in this workspace… See console" + notify_error(window, message, status) def disable_in_project(window: sublime.Window, config_name: str) -> None: @@ -159,5 +161,6 @@ def disable_in_project(window: sublime.Window, config_name: str) -> None: project_client_settings['enabled'] = False window.set_project_data(project_data) else: - sublime.message_dialog( - f"Can't disable {config_name} in the current workspace. Ensure that the project is saved first.") + message = f"Can't disable {config_name} in the current workspace. Ensure that the project is saved first." + status = f"Can't enable {config_name} in this workspace… See console" + notify_error(window, message, status) diff --git a/plugin/execute_command.py b/plugin/execute_command.py index 78253402f..d8dbbca8b 100644 --- a/plugin/execute_command.py +++ b/plugin/execute_command.py @@ -1,4 +1,5 @@ from __future__ import annotations +from .core.logging_notify import notify_error from .core.protocol import Error from .core.protocol import ExecuteCommandParams from .core.registry import LspTextCommand @@ -58,7 +59,9 @@ def handle_error_async(self, error: Error, command_name: str) -> None: :param error: The Error object. :param command_name: The name of the command that was executed. """ - sublime.message_dialog(f"command {command_name} failed. Reason: {str(error)}") + message = f"command {command_name} failed. Reason: {str(error)}" + status = f"{command_name} failed… See console" + notify_error(self.view.window(), message, status) def _expand_variables(self, command_args: list[Any]) -> list[Any]: view = self.view diff --git a/plugin/rename.py b/plugin/rename.py index bbbf82ea0..8d784dba3 100644 --- a/plugin/rename.py +++ b/plugin/rename.py @@ -2,6 +2,7 @@ from .core.edit import parse_range from .core.edit import parse_workspace_edit from .core.edit import WorkspaceChanges +from .core.logging_notify import notify_error from .core.protocol import PrepareRenameParams from .core.protocol import PrepareRenameResult from .core.protocol import Range @@ -211,7 +212,8 @@ def _on_rename_result_async(self, session: Session, response: WorkspaceEdit | No def _on_prepare_result(self, pos: int, session_name: str | None, response: PrepareRenameResult | None) -> None: if response is None: - sublime.error_message("The current selection cannot be renamed") + message = "The current selection cannot be renamed" + notify_error(self.view.window(), message) return if is_range_response(response): r = range_to_region(response, self.view) @@ -226,7 +228,8 @@ def _on_prepare_result(self, pos: int, session_name: str | None, response: Prepa self.view.run_command("lsp_symbol_rename", args) def _on_prepare_error(self, error: Any) -> None: - sublime.error_message("Rename error: {}".format(error["message"])) + message = "Rename error: {}".format(error["message"]) + notify_error(self.view.window(), message) def _get_relative_path(self, file_path: str) -> str: wm = windows.lookup(self.view.window()) diff --git a/plugin/tooling.py b/plugin/tooling.py index acfb42c0b..df5e77b9c 100644 --- a/plugin/tooling.py +++ b/plugin/tooling.py @@ -1,6 +1,7 @@ from __future__ import annotations from .core.css import css from .core.logging import debug +from .core.logging_notify import notify_error from .core.registry import windows from .core.sessions import get_plugin from .core.transports import create_transport @@ -130,15 +131,19 @@ def run(self, base_package_name: str) -> None: try: urllib.parse.urlparse(base_url) except Exception: - sublime.error_message("The clipboard content must be a URL to a package.json file.") + message = "The clipboard content must be a URL to a package.json file." + status = "Clipboard must be a URL to package.json" + notify_error(sublime.active_window(), message, status) return if not base_url.endswith("package.json"): - sublime.error_message("URL must end with 'package.json'") + message = "URL must end with 'package.json'" + notify_error(sublime.active_window(), message) return try: package = json.loads(urllib.request.urlopen(base_url).read().decode("utf-8")) except Exception as ex: - sublime.error_message(f'Unable to load "{base_url}": {ex}') + message = f'Unable to load "{base_url}": {ex}' + notify_error(sublime.active_window(), message) return # There might be a translations file as well. @@ -150,11 +155,13 @@ def run(self, base_package_name: str) -> None: contributes = package.get("contributes") if not isinstance(contributes, dict): - sublime.error_message('No "contributes" key found!') + message = 'No "contributes" key found!' + notify_error(sublime.active_window(), message) return configuration = contributes.get("configuration") if not isinstance(configuration, dict) and not isinstance(configuration, list): - sublime.error_message('No "contributes.configuration" key found!') + message = 'No "contributes.configuration" key found!' + notify_error(sublime.active_window(), message) return if isinstance(configuration, dict): properties = configuration.get("properties") @@ -163,7 +170,8 @@ def run(self, base_package_name: str) -> None: for configuration_item in configuration: properties.update(configuration_item.get("properties")) if not isinstance(properties, dict): - sublime.error_message('No "contributes.configuration.properties" key found!') + message = 'No "contributes.configuration.properties" key found!' + notify_error(sublime.active_window(), message) return # Process each key-value pair of the server settings. @@ -303,7 +311,8 @@ def run(self) -> None: return view = wm.window.active_view() if not view: - sublime.message_dialog('Troubleshooting must be run with a file opened') + message = 'Troubleshooting must be run with a file opened' + notify_error(self.window, message) return active_view = view configs = wm.get_config_manager().get_configs() @@ -457,7 +466,9 @@ def run(self, edit: sublime.Edit) -> None: return listener = wm.listener_for_view(self.view) if not listener or not any(listener.session_views_async()): - sublime.error_message("There is no language server running for this view.") + message = "There is no language server running for this view." + status = "No language server for this view" + notify_error(wm.window, message, status) return v = wm.window.new_file() v.set_scratch(True) diff --git a/sublime-package.json b/sublime-package.json index f98b52c50..21b7205da 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -449,6 +449,11 @@ "default": false, "markdownDescription": "Show errors and warnings count in the status bar." }, + "suppress_error_dialogs": { + "type": "boolean", + "default": false, + "markdownDescription": "Replace error popup windows with a console message and a short status notification." + }, "lsp_format_on_paste": { "$ref": "sublime://settings/LSP#/definitions/lsp_format_on_paste" },