LSP
LSP copied to clipboard
Use timestamps on the logging messages
On:
- https://github.com/palantir/python-language-server/issues/257 Took 2 minutes to respond to on_hover(), from 15:56:14 until 15:58:38
I had reported the LSP log in completion to the python-language-server log. Howerver LSP logs have no timestamps, then make impossible to correlated between the two logs.
I wrote a extension to the built-in logging module https://github.com/evandrocoan/PythonDebugTools and I am going to add it on my fork of LSP. Then I will open pull request and you can see if you like it.
Agree that timing is useful to have in LSP!
For performance investigations, developers can make the necessary change to 'logging.py'.
Closing until more endusers report a preference for more logging output.
Something like this?
:: --> LSP-pylsp initialize (1 | 12:18:01): {'initializationOptions': {}, 'capabilities': {'general': {'markdown': {'version': '3.2.2', 'parser': 'Python-Markdown'}, 'regularExpressions': {'engine': 'ECMAScript'}}, 'window': {'showDocument': {'support': True}, 'showMessage': {'messageActionItem': {'additionalPropertiesSupport': True}}, 'workDoneProgress': True}, 'workspace': {'configuration': True, 'symbol': {'tagSupport': {'valueSet': [1]}, 'symbolKind': {'valueSet': [15, 7, 2, 5, 18, 19, 25, 23, 1, 14, 21, 9, 4, 11, 6, 8, 22, 16, 3, 20, 26, 13, 17, 24, 12, 10]}, 'dynamicRegistration': True}, 'executeCommand': {}, 'applyEdit': True, 'semanticTokens': {'refreshSupport': True}, 'inlayHint': {'refreshSupport': True}, 'workspaceFolders': True, 'didChangeWatchedFiles': {'dynamicRegistration': True}, 'workspaceEdit': {'failureHandling': 'abort', 'documentChanges': True}, 'didChangeConfiguration': {'dynamicRegistration': True}, 'codeLens': {'refreshSupport': True}}, 'textDocument': {'completion': {'completionItem': {'tagSupport': {'valueSet': [1]}, 'labelDetailsSupport': True, 'insertReplaceSupport': True, 'documentationFormat': ['markdown', 'plaintext'], 'deprecatedSupport': True, 'insertTextModeSupport': {'valueSet': [2]}, 'resolveSupport': {'properties': ['detail', 'documentation', 'additionalTextEdits']}, 'snippetSupport': True}, 'insertTextMode': 2, 'dynamicRegistration': True, 'completionItemKind': {'valueSet': [12, 10, 9, 7, 24, 22, 17, 21, 14, 11, 4, 16, 15, 8, 5, 20, 18, 2, 25, 19, 6, 23, 3, 1, 13]}}, 'rename': {'prepareSupport': True, 'dynamicRegistration': True}, 'documentLink': {'tooltipSupport': True, 'dynamicRegistration': True}, 'signatureHelp': {'contextSupport': True, 'signatureInformation': {'documentationFormat': ['markdown', 'plaintext'], 'activeParameterSupport': True, 'parameterInformation': {'labelOffsetSupport': True}}, 'dynamicRegistration': True}, 'inlayHint': {'resolveSupport': {'properties': ['textEdits', 'label.command']}, 'dynamicRegistration': True}, 'codeAction': {'disabledSupport': True, 'resolveSupport': {'properties': ['edit']}, 'dynamicRegistration': True, 'isPreferredSupport': True, 'codeActionLiteralSupport': {'codeActionKind': {'valueSet': ['quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source.organizeImports']}}, 'dataSupport': True}, 'formatting': {'dynamicRegistration': True}, 'hover': {'dynamicRegistration': True, 'contentFormat': ['markdown', 'plaintext']}, 'references': {'dynamicRegistration': True}, 'semanticTokens': {'overlappingTokenSupport': False, 'tokenModifiers': ['static', 'abstract', 'async', 'declaration', 'modification', 'deprecated', 'definition', 'defaultLibrary', 'readonly', 'documentation'], 'requests': {'range': True, 'full': {'delta': True}}, 'tokenTypes': ['string', 'property', 'keyword', 'class', 'type', 'operator', 'struct', 'typeParameter', 'regexp', 'parameter', 'macro', 'comment', 'interface', 'method', 'enumMember', 'number', 'namespace', 'modifier', 'decorator', 'variable', 'event', 'function', 'enum'], 'dynamicRegistration': True, 'augmentsSyntaxTokens': True, 'multilineTokenSupport': True, 'formats': ['relative']}, 'selectionRange': {'dynamicRegistration': True}, 'implementation': {'linkSupport': True, 'dynamicRegistration': True}, 'synchronization': {'willSaveWaitUntil': True, 'willSave': True, 'dynamicRegistration': True, 'didSave': True}, 'declaration': {'linkSupport': True, 'dynamicRegistration': True}, 'rangeFormatting': {'dynamicRegistration': True}, 'documentSymbol': {'hierarchicalDocumentSymbolSupport': True, 'tagSupport': {'valueSet': [1]}, 'symbolKind': {'valueSet': [15, 7, 2, 5, 18, 19, 25, 23, 1, 14, 21, 9, 4, 11, 6, 8, 22, 16, 3, 20, 26, 13, 17, 24, 12, 10]}, 'dynamicRegistration': True}, 'definition': {'linkSupport': True, 'dynamicRegistration': True}, 'typeDefinition': {'linkSupport': True, 'dynamicRegistration': True}, 'publishDiagnostics': {'versionSupport': True, 'codeDescriptionSupport': True, 'tagSupport': {'valueSet': [2, 1]}, 'dataSupport': True, 'relatedInformation': True}, 'documentHighlight': {'dynamicRegistration': True}, 'colorProvider': {'dynamicRegistration': True}, 'codeLens': {'dynamicRegistration': True}}}, 'rootPath': '/home/predragnikolic/.config/sublime-text/Packages/LSP', 'clientInfo': {'name': 'Sublime Text LSP', 'version': '1.18.0'}, 'workspaceFolders': [{'name': 'LSP', 'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP'}], 'processId': 183133, 'rootUri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP'}
:: <<< LSP-pylsp (1 | 12:18:02 - 1687ms): {'capabilities': {'renameProvider': True, 'completionProvider': {'resolveProvider': True, 'triggerCharacters': ['.']}, 'codeLensProvider': {'resolveProvider': False}, 'documentRangeFormattingProvider': True, 'executeCommandProvider': {'commands': []}, 'documentSymbolProvider': True, 'signatureHelpProvider': {'triggerCharacters': ['(', ',', '=']}, 'definitionProvider': True, 'codeActionProvider': True, 'documentHighlightProvider': True, 'textDocumentSync': {'didClose': {}, 'didOpen': {}, 'save': {'includeText': True}, 'change': {'syncKind': 2}}, 'experimental': {}, 'workspace': {'workspaceFolders': {'supported': True, 'changeNotifications': True}}, 'documentFormattingProvider': True, 'foldingRangeProvider': True, 'hoverProvider': True, 'referencesProvider': True}, 'serverInfo': {'name': 'pylsp', 'version': '1.5.0'}}
:: -> LSP-pylsp initialized (12:18:02): {}
:: -> LSP-pylsp workspace/didChangeConfiguration (12:18:02): {'settings': {'pylsp': {'rope': {'extensionModules': None, 'ropeFolder': None}, 'plugins': {'yapf': {'enabled': False}, 'rope_completion': {'enabled': False, 'eager': False}, 'jedi_completion': {'resolve_at_most': 25, 'cache_for': ['pandas', 'numpy', 'tensorflow', 'matplotlib'], 'include_params': True, 'eager': False, 'enabled': True, 'fuzzy': True, 'include_class_objects': True}, 'pylsp_black': {'enabled': False, 'cache_config': False, 'line_length': 88, 'preview': False}, 'pylsp_mypy': {'dmypy': False, 'strict': False, 'enabled': False, 'live_mode': True}, 'pylint': {'enabled': False, 'args': None, 'executable': ''}, 'jedi_definition': {'enabled': True, 'follow_imports': True, 'follow_builtin_imports': True}, 'mccabe': {'enabled': True, 'threshold': 15}, 'pydocstyle': {'ignore': None, 'select': None, 'addIgnore': None, 'addSelect': None, 'matchDir': '[^\\.].*', 'enabled': False, 'convention': None, 'match': '(?!test_).*\\.py'}, 'flake8': {'ignore': None, 'select': None, 'perFileIgnores': None, 'filename': None, 'config': None, 'enabled': False, 'maxLineLength': None, 'hangClosing': None, 'executable': 'flake8', 'exclude': None}, 'autopep8': {'enabled': True}, 'jedi_signature_help': {'enabled': True}, 'jedi': {'environment': None, 'extra_paths': ['/opt/sublime_text/Lib/python33', '/home/predragnikolic/.config/sublime-text/Packages'], 'env_vars': None}, 'jedi_hover': {'enabled': True}, 'pyls_isort': {'enabled': False}, 'jedi_symbols': {'enabled': True, 'include_import_symbols': True, 'all_scopes': True}, 'pyflakes': {'enabled': False}, 'pycodestyle': {'ignore': None, 'select': None, 'filename': None, 'aggressive': None, 'maxLineLength': None, 'hangClosing': None, 'enabled': True, 'exclude': None}, 'jedi_references': {'enabled': True}, 'preload': {'enabled': True, 'modules': None}}, 'configurationSources': ['pycodestyle']}}}
:: -> LSP-pylsp textDocument/didOpen (12:18:03): {'textDocument': {'text': 'from ...third_party import WebsocketServer # type: ignore\nfrom .configurations import ConfigManager\nfrom .configurations import WindowConfigManager\nfrom .diagnostics import ensure_diagnostics_panel\nfrom .diagnostics_storage import is_severity_included\nfrom .logging import debug\nfrom .logging import exception_log\nfrom .message_request_handler import MessageRequestHandler\nfrom .panels import ensure_log_panel\nfrom .panels import is_panel_open\nfrom .panels import log_server_message\nfrom .panels import PanelName\nfrom .promise import Promise\nfrom .protocol import DocumentUri\nfrom .protocol import Error\nfrom .protocol import Location\nfrom .protocol import LocationLink\nfrom .sessions import AbstractViewListener\nfrom .sessions import get_plugin\nfrom .sessions import Logger\nfrom .sessions import Manager\nfrom .sessions import Session\nfrom .settings import userprefs\nfrom .transports import create_transport\nfrom .types import ClientConfig\nfrom .types import matches_pattern\nfrom .typing import Optional, Any, Dict, Deque, List, Generator, Tuple, Union\nfrom .url import parse_uri\nfrom .views import extract_variables\nfrom .views import format_diagnostic_for_panel\nfrom .views import make_link\nfrom .workspace import ProjectFolders\nfrom .workspace import sorted_workspace_folders\nfrom collections import deque\nfrom collections import OrderedDict\nfrom subprocess import CalledProcessError\nfrom time import time, strftime\nfrom weakref import ref\nfrom weakref import WeakSet\nimport functools\nimport json\nimport sublime\nimport threading\nimport urllib.parse\n\n\n_NO_DIAGNOSTICS_PLACEHOLDER = " No diagnostics. Well done!"\n\n\ndef extract_message(params: Any) -> str:\n return params.get("message", "???") if isinstance(params, dict) else "???"\n\n\ndef set_diagnostics_count(view: sublime.View, errors: int, warnings: int) -> None:\n try:\n key = AbstractViewListener.TOTAL_ERRORS_AND_WARNINGS_STATUS_KEY\n if userprefs().show_diagnostics_count_in_view_status:\n view.set_status(key, "E: {}, W: {}".format(errors, warnings))\n else:\n view.erase_status(key)\n except Exception:\n pass\n\n\nclass WindowManager(Manager):\n\n DIAGNOSTIC_PHANTOM_KEY = "lsp_diagnostic_phantom"\n\n def __init__(\n self,\n window: sublime.Window,\n workspace: ProjectFolders,\n configs: WindowConfigManager,\n ) -> None:\n self._window = window\n self._configs = configs\n self._sessions = WeakSet() # type: WeakSet[Session]\n self._workspace = workspace\n self._pending_listeners = deque() # type: Deque[AbstractViewListener]\n self._listeners = WeakSet() # type: WeakSet[AbstractViewListener]\n self._new_listener = None # type: Optional[AbstractViewListener]\n self._new_session = None # type: Optional[Session]\n self._panel_code_phantoms = None # type: Optional[sublime.PhantomSet]\n self.total_error_count = 0\n self.total_warning_count = 0\n sublime.set_timeout(functools.partial(self._update_panel_main_thread, _NO_DIAGNOSTICS_PLACEHOLDER, []))\n ensure_log_panel(window)\n\n def get_config_manager(self) -> WindowConfigManager:\n return self._configs\n\n def get_sessions(self) -> Generator[Session, None, None]:\n yield from self._sessions\n\n def on_load_project_async(self) -> None:\n self.update_workspace_folders_async()\n self._configs.update()\n\n def on_post_save_project_async(self) -> None:\n self.on_load_project_async()\n\n def update_workspace_folders_async(self) -> None:\n if self._workspace.update():\n workspace_folders = self._workspace.get_workspace_folders()\n for session in self._sessions:\n session.update_folders(workspace_folders)\n\n def enable_config_async(self, config_name: str) -> None:\n self._configs.enable_config(config_name)\n\n def disable_config_async(self, config_name: str) -> None:\n self._configs.disable_config(config_name)\n\n def open_location_async(\n self,\n location: Union[Location, LocationLink],\n session_name: Optional[str],\n view: sublime.View,\n flags: int = 0,\n group: int = -1\n ) -> Promise[Optional[sublime.View]]:\n for session in self.sessions(view):\n if session_name is None or session_name == session.config.name:\n return session.open_location_async(location, flags, group)\n return Promise.resolve(None)\n\n def register_listener_async(self, listener: AbstractViewListener) -> None:\n set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count)\n # Update workspace folders in case the user have changed those since window was created.\n # There is no currently no notification in ST that would notify about folder changes.\n self.update_workspace_folders_async()\n self._pending_listeners.appendleft(listener)\n if self._new_listener is None:\n self._dequeue_listener_async()\n\n def unregister_listener_async(self, listener: AbstractViewListener) -> None:\n self._listeners.discard(listener)\n\n def listeners(self) -> Generator[AbstractViewListener, None, None]:\n yield from self._listeners\n\n def listener_for_view(self, view: sublime.View) -> Optional[AbstractViewListener]:\n for listener in self.listeners():\n if listener.view == view:\n return listener\n return None\n\n def _dequeue_listener_async(self) -> None:\n listener = None # type: Optional[AbstractViewListener]\n if self._new_listener is not None:\n listener = self._new_listener\n # debug("re-checking listener", listener)\n self._new_listener = None\n else:\n try:\n listener = self._pending_listeners.pop()\n if not listener.view.is_valid():\n # debug("listener", listener, "is no longer valid")\n return self._dequeue_listener_async()\n # debug("adding new pending listener", listener)\n self._listeners.add(listener)\n except IndexError:\n # We have handled all pending listeners.\n self._new_session = None\n return\n if self._new_session:\n self._sessions.add(self._new_session)\n self._publish_sessions_to_listener_async(listener)\n if self._new_session:\n if not any(self._new_session.session_views_async()):\n self._sessions.discard(self._new_session)\n self._new_session.end_async()\n self._new_session = None\n config = self._needed_config(listener.view)\n if config:\n # debug("found new config for listener", listener)\n self._new_listener = listener\n self.start_async(config, listener.view)\n else:\n # debug("no new config found for listener", listener)\n self._new_listener = None\n self._dequeue_listener_async()\n\n def _publish_sessions_to_listener_async(self, listener: AbstractViewListener) -> None:\n inside_workspace = self._workspace.contains(listener.view)\n scheme = urllib.parse.urlparse(listener.get_uri()).scheme\n for session in self._sessions:\n if session.can_handle(listener.view, scheme, capability=None, inside_workspace=inside_workspace):\n # debug("registering session", session.config.name, "to listener", listener)\n try:\n listener.on_session_initialized_async(session)\n except Exception as ex:\n message = "failed to register session {} to listener {}".format(session.config.name, listener)\n exception_log(message, ex)\n\n def window(self) -> sublime.Window:\n return self._window\n\n def sessions(self, view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]:\n inside_workspace = self._workspace.contains(view)\n sessions = list(self._sessions)\n uri = view.settings().get("lsp_uri")\n if not isinstance(uri, str):\n return\n scheme = urllib.parse.urlparse(uri).scheme\n for session in sessions:\n if session.can_handle(view, scheme, capability, inside_workspace):\n yield session\n\n def get_session(self, config_name: str, file_path: str) -> Optional[Session]:\n return self._find_session(config_name, file_path)\n\n def _can_start_config(self, config_name: str, file_path: str) -> bool:\n return not bool(self._find_session(config_name, file_path))\n\n def _find_session(self, config_name: str, file_path: str) -> Optional[Session]:\n inside = self._workspace.contains(file_path)\n for session in self._sessions:\n if session.config.name == config_name and session.handles_path(file_path, inside):\n return session\n return None\n\n def _needed_config(self, view: sublime.View) -> Optional[ClientConfig]:\n configs = self._configs.match_view(view)\n handled = False\n file_name = view.file_name()\n inside = self._workspace.contains(view)\n for config in configs:\n handled = False\n for session in self._sessions:\n if config.name == session.config.name and session.handles_path(file_name, inside):\n handled = True\n break\n if not handled:\n return config\n return None\n\n def start_async(self, config: ClientConfig, initiating_view: sublime.View) -> None:\n config = ClientConfig.from_config(config, {})\n file_path = initiating_view.file_name() or \'\'\n if not self._can_start_config(config.name, file_path):\n # debug(\'Already starting on this window:\', config.name)\n return\n try:\n workspace_folders = sorted_workspace_folders(self._workspace.folders, file_path)\n plugin_class = get_plugin(config.name)\n variables = extract_variables(self._window)\n cwd = None # type: Optional[str]\n if plugin_class is not None:\n if plugin_class.needs_update_or_installation():\n config.set_view_status(initiating_view, "installing...")\n plugin_class.install_or_update()\n additional_variables = plugin_class.additional_variables()\n if isinstance(additional_variables, dict):\n variables.update(additional_variables)\n cannot_start_reason = plugin_class.can_start(self._window, initiating_view, workspace_folders, config)\n if cannot_start_reason:\n config.erase_view_status(initiating_view)\n message = "cannot start {}: {}".format(config.name, cannot_start_reason)\n self._configs.disable_config(config.name, only_for_session=True)\n # Continue with handling pending listeners\n self._new_session = None\n sublime.set_timeout_async(self._dequeue_listener_async)\n return self._window.status_message(message)\n cwd = plugin_class.on_pre_start(self._window, initiating_view, workspace_folders, config)\n config.set_view_status(initiating_view, "starting...")\n session = Session(self, self._create_logger(config.name), workspace_folders, config, plugin_class)\n if cwd:\n transport_cwd = cwd # type: Optional[str]\n else:\n transport_cwd = workspace_folders[0].path if workspace_folders else None\n transport_config = config.resolve_transport_config(variables)\n transport = create_transport(transport_config, transport_cwd, session)\n if plugin_class:\n plugin_class.on_post_start(self._window, initiating_view, workspace_folders, config)\n config.set_view_status(initiating_view, "initialize")\n session.initialize_async(\n variables=variables,\n transport=transport,\n working_directory=cwd,\n init_callback=functools.partial(self._on_post_session_initialize, initiating_view)\n )\n self._new_session = session\n except Exception as e:\n message = "".join((\n "Failed to start {0} - disabling for this window for the duration of the current session.\\n",\n "Re-enable by running \\"LSP: Enable Language Server In Project\\" from the Command Palette.",\n "\\n\\n--- Error: ---\\n{1}"\n )).format(config.name, str(e))\n exception_log("Unable to start subprocess for {}".format(config.name), e)\n if isinstance(e, CalledProcessError):\n print("Server output:\\n{}".format(e.output.decode(\'utf-8\', \'replace\')))\n self._configs.disable_config(config.name, only_for_session=True)\n config.erase_view_status(initiating_view)\n sublime.message_dialog(message)\n # Continue with handling pending listeners\n self._new_session = None\n sublime.set_timeout_async(self._dequeue_listener_async)\n\n def _on_post_session_initialize(\n self, initiating_view: sublime.View, session: Session, is_error: bool = False\n ) -> None:\n if is_error:\n session.config.erase_view_status(initiating_view)\n self._new_listener = None\n self._new_session = None\n else:\n sublime.set_timeout_async(self._dequeue_listener_async)\n\n def _create_logger(self, config_name: str) -> Logger:\n logger_map = {\n "panel": PanelLogger,\n "remote": RemoteLogger,\n }\n loggers = []\n for logger_type in userprefs().log_server:\n if logger_type not in logger_map:\n debug("Invalid logger type ({}) specified for log_server settings".format(logger_type))\n continue\n loggers.append(logger_map[logger_type])\n if len(loggers) == 0:\n return RouterLogger() # logs nothing\n elif len(loggers) == 1:\n return loggers[0](self, config_name)\n else:\n router_logger = RouterLogger()\n for logger in loggers:\n router_logger.append(logger(self, config_name))\n return router_logger\n\n def handle_message_request(self, session: Session, params: Any, request_id: Any) -> None:\n view = self._window.active_view()\n if view:\n MessageRequestHandler(view, session, request_id, params, session.config.name).show()\n\n def restart_sessions_async(self, config_name: Optional[str] = None) -> None:\n self._end_sessions_async(config_name)\n listeners = list(self._listeners)\n self._listeners.clear()\n for listener in listeners:\n self.register_listener_async(listener)\n\n def _end_sessions_async(self, config_name: Optional[str] = None) -> None:\n sessions = list(self._sessions)\n for session in sessions:\n if config_name is None or config_name == session.config.name:\n session.end_async()\n self._sessions.discard(session)\n\n def get_project_path(self, file_path: str) -> Optional[str]:\n candidate = None # type: Optional[str]\n for folder in self._workspace.folders:\n if file_path.startswith(folder):\n if candidate is None or len(folder) > len(candidate):\n candidate = folder\n return candidate\n\n def should_present_diagnostics(self, uri: DocumentUri) -> Optional[str]:\n scheme, path = parse_uri(uri)\n if scheme != "file":\n return None\n if not self._workspace.contains(path):\n return "not inside window folders"\n view = self._window.active_view()\n if not view:\n return None\n settings = view.settings()\n if matches_pattern(path, settings.get("binary_file_patterns")):\n return "matches a pattern in binary_file_patterns"\n if matches_pattern(path, settings.get("file_exclude_patterns")):\n return "matches a pattern in file_exclude_patterns"\n if matches_pattern(path, settings.get("folder_exclude_patterns")):\n return "matches a pattern in folder_exclude_patterns"\n return None\n\n def on_post_exit_async(self, session: Session, exit_code: int, exception: Optional[Exception]) -> None:\n self._sessions.discard(session)\n for listener in self._listeners:\n listener.on_session_shutdown_async(session)\n if exit_code != 0 or exception:\n config = session.config\n msg = "".join((\n "{0} exited with status code {1}. ",\n "Do you want to restart it? If you choose Cancel, it will be disabled for this window for the ",\n "duration of the current session. ",\n "Re-enable by running \\"LSP: Enable Language Server In Project\\" from the Command Palette."\n )).format(config.name, exit_code)\n if exception:\n msg += "\\n\\n--- Error: ---\\n{}".format(str(exception))\n if sublime.ok_cancel_dialog(msg, "Restart {}".format(config.name)):\n for listener in self._listeners:\n self.register_listener_async(listener)\n else:\n self._configs.disable_config(config.name, only_for_session=True)\n\n def plugin_unloaded(self) -> None:\n """\n This is called **from the main thread** when the plugin unloads. In that case we must destroy all sessions\n from the main thread. That could lead to some dict/list being mutated while iterated over, so be careful\n """\n self._end_sessions_async()\n\n def handle_server_message(self, server_name: str, message: str) -> None:\n sublime.set_timeout(lambda: log_server_message(self._window, server_name, message))\n\n def handle_log_message(self, session: Session, params: Any) -> None:\n self.handle_server_message(session.config.name, extract_message(params))\n\n def handle_stderr_log(self, session: Session, message: str) -> None:\n self.handle_server_message(session.config.name, message)\n\n def handle_show_message(self, session: Session, params: Any) -> None:\n sublime.status_message("{}: {}".format(session.config.name, extract_message(params)))\n\n def on_diagnostics_updated(self) -> None:\n self.total_error_count = 0\n self.total_warning_count = 0\n for session in self._sessions:\n local_errors, local_warnings = session.diagnostics.sum_total_errors_and_warnings_async()\n self.total_error_count += local_errors\n self.total_warning_count += local_warnings\n for listener in list(self._listeners):\n set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count)\n if is_panel_open(self._window, PanelName.Diagnostics):\n self.update_diagnostics_panel_async()\n\n def update_diagnostics_panel_async(self) -> None:\n to_render = [] # type: List[str]\n prephantoms = [] # type: List[Tuple[int, int, str, str]]\n row = 0\n max_severity = userprefs().diagnostics_panel_include_severity_level\n contributions = OrderedDict(\n ) # type: OrderedDict[str, List[Tuple[str, Optional[int], Optional[str], Optional[str]]]]\n for session in self._sessions:\n for (_, path), contribution in session.diagnostics.filter_map_diagnostics_async(\n is_severity_included(max_severity), lambda _, diagnostic: format_diagnostic_for_panel(diagnostic)):\n seen = path in contributions\n contributions.setdefault(path, []).extend(contribution)\n if not seen:\n contributions.move_to_end(path)\n for path, contribution in contributions.items():\n to_render.append("{}:".format(path))\n row += 1\n for content, offset, code, href in contribution:\n to_render.append(content)\n if offset is not None and code is not None and href is not None:\n prephantoms.append((row, offset, code, href))\n row += content.count("\\n") + 1\n to_render.append("") # add spacing between filenames\n row += 1\n characters = "\\n".join(to_render)\n if not characters:\n characters = _NO_DIAGNOSTICS_PLACEHOLDER\n sublime.set_timeout(functools.partial(self._update_panel_main_thread, characters, prephantoms))\n\n def _update_panel_main_thread(self, characters: str, prephantoms: List[Tuple[int, int, str, str]]) -> None:\n panel = ensure_diagnostics_panel(self._window)\n if not panel or not panel.is_valid():\n return\n panel.run_command("lsp_update_panel", {"characters": characters})\n if self._panel_code_phantoms is None:\n self._panel_code_phantoms = sublime.PhantomSet(panel, "hrefs")\n phantoms = [] # type: List[sublime.Phantom]\n for row, col, code, href in prephantoms:\n point = panel.text_point(row, col)\n region = sublime.Region(point, point)\n phantoms.append(sublime.Phantom(region, make_link(href, code), sublime.LAYOUT_INLINE))\n self._panel_code_phantoms.update(phantoms)\n\n def show_diagnostics_panel_async(self) -> None:\n if self._window.active_panel() is None:\n self._window.run_command("show_panel", {"panel": "output.diagnostics"})\n\n def hide_diagnostics_panel_async(self) -> None:\n if is_panel_open(self._window, PanelName.Diagnostics):\n self._window.run_command("hide_panel", {"panel": "output.diagnostics"})\n\n\nclass WindowRegistry(object):\n def __init__(self, configs: ConfigManager) -> None:\n self._windows = {} # type: Dict[int, WindowManager]\n self._configs = configs\n\n def lookup(self, window: sublime.Window) -> WindowManager:\n wm = self._windows.get(window.id())\n if wm:\n return wm\n workspace = ProjectFolders(window)\n window_configs = self._configs.for_window(window)\n state = WindowManager(window=window, workspace=workspace, configs=window_configs)\n self._windows[window.id()] = state\n return state\n\n def listener_for_view(self, view: sublime.View) -> Optional[AbstractViewListener]:\n w = view.window()\n if not w:\n return None\n return self.lookup(w).listener_for_view(view)\n\n def discard(self, window: sublime.Window) -> None:\n self._windows.pop(window.id(), None)\n\n\nclass TrackTime:\n def __init__(self, start: Optional[int] = None, end: Optional[int] = None) -> None:\n self.start = start\n self.end = end\n\n def duration(self) -> int:\n if not self.start and not self.end:\n return 0\n return self.end - self.start\n\n\nclass RequestTimeTracker:\n def __init__(self) -> None:\n self._request_duration = {} # type: Dict[int, TrackTime]\n\n def start_tracking(self, request_id: int) -> None:\n self._request_duration[request_id] = TrackTime(start=time())\n\n def end_tracking(self, request_id) -> None:\n track_time = self._request_duration[request_id]\n if track_time:\n track_time.end = time()\n\n def duration_in_ms(self, request_id: int) -> int:\n duration_in_ms = 0\n track_time = self._request_duration[request_id]\n if track_time:\n duration_in_ms = track_time.duration()\n return int(duration_in_ms * 1000)\n\n def clear(self, request_id: int) -> None:\n self._request_duration.pop(request_id, None)\n\n\nclass PanelLogger(Logger):\n\n def __init__(self, manager: WindowManager, server_name: str) -> None:\n self._manager = ref(manager)\n self._server_name = server_name\n self.request_time_tracker = RequestTimeTracker()\n\n def stderr_message(self, message: str) -> None:\n """\n Not handled here as stderr messages are handled by WindowManager regardless\n if this logger is enabled.\n """\n pass\n\n def log(self, message: str, params: Any) -> None:\n\n def run_on_async_worker_thread() -> None:\n nonlocal message\n params_str = repr(params)\n if 0 < userprefs().log_max_size <= len(params_str):\n params_str = \'<params with {} characters>\'.format(len(params_str))\n message = "{}: {}".format(message, params_str)\n manager = self._manager()\n if manager is not None:\n manager.handle_server_message(":", message)\n\n sublime.set_timeout_async(run_on_async_worker_thread)\n\n def outgoing_response(self, request_id: Any, params: Any) -> None:\n self.request_time_tracker.end_tracking(request_id)\n if not userprefs().log_server:\n return\n self.log(self._format_response(">>>", request_id), params)\n self.request_time_tracker.clear(request_id)\n\n def outgoing_error_response(self, request_id: Any, error: Error) -> None:\n self.request_time_tracker.end_tracking(request_id)\n if not userprefs().log_server:\n return\n self.log(self._format_response("~~>", request_id), error.to_lsp())\n self.request_time_tracker.clear(request_id)\n\n def outgoing_request(self, request_id: int, method: str, params: Any) -> None:\n self.request_time_tracker.start_tracking(request_id)\n if not userprefs().log_server:\n return\n self.log(self._format_request("-->", method, request_id), params)\n print(\'pedja\', self.request_time_tracker._request_duration)\n\n def outgoing_notification(self, method: str, params: Any) -> None:\n if not userprefs().log_server:\n return\n self.log(self._format_notification(" ->", method), params)\n\n def incoming_response(self, request_id: int, params: Any, is_error: bool) -> None:\n self.request_time_tracker.end_tracking(request_id)\n if not userprefs().log_server:\n return\n direction = "<~~" if is_error else "<<<"\n self.log(self._format_response(direction, request_id), params)\n self.request_time_tracker.clear(request_id)\n\n def incoming_request(self, request_id: Any, method: str, params: Any) -> None:\n self.request_time_tracker.start_tracking(request_id)\n if not userprefs().log_server:\n return\n self.log(self._format_request("<--", method, request_id), params)\n\n def incoming_notification(self, method: str, params: Any, unhandled: bool) -> None:\n if not userprefs().log_server:\n return\n direction = "<? " if unhandled else "<- "\n self.log(self._format_notification(direction, method), params)\n\n def _format_response(self, direction: str, request_id: Any) -> str:\n duration_in_ms = self.request_time_tracker.duration_in_ms(request_id)\n return "{} {} ({} | {} - {}ms)".format(direction, self._server_name, request_id, strftime("%H:%M:%S"), duration_in_ms)\n\n def _format_request(self, direction: str, method: str, request_id: Any) -> str:\n return "{} {} {} ({} | {})".format(direction, self._server_name, method, request_id, strftime("%H:%M:%S"))\n\n def _format_notification(self, direction: str, method: str) -> str:\n return "{} {} {} ({})".format(direction, self._server_name, method, strftime("%H:%M:%S"))\n\n\nclass RemoteLogger(Logger):\n PORT = 9981\n DIRECTION_OUTGOING = 1\n DIRECTION_INCOMING = 2\n _ws_server = None # type: Optional[WebsocketServer]\n _ws_server_thread = None # type: Optional[threading.Thread]\n _last_id = 0\n\n def __init__(self, manager: WindowManager, server_name: str) -> None:\n RemoteLogger._last_id += 1\n self._server_name = \'{} ({})\'.format(server_name, RemoteLogger._last_id)\n if not RemoteLogger._ws_server:\n try:\n RemoteLogger._ws_server = WebsocketServer(self.PORT)\n RemoteLogger._ws_server.set_fn_new_client(self._on_new_client)\n RemoteLogger._ws_server.set_fn_client_left(self._on_client_left)\n RemoteLogger._ws_server.set_fn_message_received(self._on_message_received)\n self._start_server()\n except OSError as ex:\n if ex.errno == 48: # Address already in use\n debug(\'WebsocketServer not started - address already in use\')\n RemoteLogger._ws_server = None\n else:\n raise ex\n\n def _start_server(self) -> None:\n def start_async() -> None:\n if RemoteLogger._ws_server:\n RemoteLogger._ws_server.run_forever()\n RemoteLogger._ws_server_thread = threading.Thread(target=start_async)\n RemoteLogger._ws_server_thread.start()\n\n def _stop_server(self) -> None:\n if RemoteLogger._ws_server:\n RemoteLogger._ws_server.shutdown()\n RemoteLogger._ws_server = None\n if RemoteLogger._ws_server_thread:\n RemoteLogger._ws_server_thread.join()\n RemoteLogger._ws_server_thread = None\n\n def _on_new_client(self, client: Dict, server: WebsocketServer) -> None:\n """Called for every client connecting (after handshake)."""\n debug("New client connected and was given id %d" % client[\'id\'])\n # server.send_message_to_all("Hey all, a new client has joined us")\n\n def _on_client_left(self, client: Dict, server: WebsocketServer) -> None:\n """Called for every client disconnecting."""\n debug("Client(%d) disconnected" % client[\'id\'])\n\n def _on_message_received(self, client: Dict, server: WebsocketServer, message: str) -> None:\n """Called when a client sends a message."""\n debug("Client(%d) said: %s" % (client[\'id\'], message))\n\n def stderr_message(self, message: str) -> None:\n self._broadcast_json({\n \'server\': self._server_name,\n \'time\': round(time() * 1000),\n \'method\': \'stderr\',\n \'params\': message,\n \'isError\': True,\n \'direction\': self.DIRECTION_INCOMING,\n })\n\n def outgoing_request(self, request_id: int, method: str, params: Any) -> None:\n self._broadcast_json({\n \'server\': self._server_name,\n \'id\': request_id,\n \'time\': round(time() * 1000),\n \'method\': method,\n \'params\': params,\n \'direction\': self.DIRECTION_OUTGOING,\n })\n\n def incoming_response(self, request_id: int, params: Any, is_error: bool) -> None:\n self._broadcast_json({\n \'server\': self._server_name,\n \'id\': request_id,\n \'time\': round(time() * 1000),\n \'params\': params,\n \'direction\': self.DIRECTION_INCOMING,\n \'isError\': is_error,\n })\n\n def incoming_request(self, request_id: Any, method: str, params: Any) -> None:\n self._broadcast_json({\n \'server\': self._server_name,\n \'id\': request_id,\n \'time\': round(time() * 1000),\n \'method\': method,\n \'params\': params,\n \'direction\': self.DIRECTION_INCOMING,\n })\n\n def outgoing_response(self, request_id: Any, params: Any) -> None:\n self._broadcast_json({\n \'server\': self._server_name,\n \'id\': request_id,\n \'time\': round(time() * 1000),\n \'params\': params,\n \'direction\': self.DIRECTION_OUTGOING,\n })\n\n def outgoing_error_response(self, request_id: Any, error: Error) -> None:\n self._broadcast_json({\n \'server\': self._server_name,\n \'id\': request_id,\n \'isError\': True,\n \'params\': error.to_lsp(),\n \'time\': round(time() * 1000),\n \'direction\': self.DIRECTION_OUTGOING,\n })\n\n def outgoing_notification(self, method: str, params: Any) -> None:\n self._broadcast_json({\n \'server\': self._server_name,\n \'time\': round(time() * 1000),\n \'method\': method,\n \'params\': params,\n \'direction\': self.DIRECTION_OUTGOING,\n })\n\n def incoming_notification(self, method: str, params: Any, unhandled: bool) -> None:\n self._broadcast_json({\n \'server\': self._server_name,\n \'time\': round(time() * 1000),\n \'error\': \'Unhandled notification!\' if unhandled else None,\n \'method\': method,\n \'params\': params,\n \'direction\': self.DIRECTION_INCOMING,\n })\n\n def _broadcast_json(self, data: Dict[str, Any]) -> None:\n if RemoteLogger._ws_server:\n json_data = json.dumps(data, sort_keys=True, check_circular=False, separators=(\',\', \':\'))\n RemoteLogger._ws_server.send_message_to_all(json_data)\n\n\nclass RouterLogger(Logger):\n def __init__(self) -> None:\n self._loggers = [] # type: List[Logger]\n\n def append(self, logger: Logger) -> None:\n self._loggers.append(logger)\n\n def stderr_message(self, *args: Any, **kwargs: Any) -> None:\n self._foreach("stderr_message", *args, **kwargs)\n\n def outgoing_response(self, *args: Any, **kwargs: Any) -> None:\n self._foreach("outgoing_response", *args, **kwargs)\n\n def outgoing_error_response(self, *args: Any, **kwargs: Any) -> None:\n self._foreach("outgoing_error_response", *args, **kwargs)\n\n def outgoing_request(self, *args: Any, **kwargs: Any) -> None:\n self._foreach("outgoing_request", *args, **kwargs)\n\n def outgoing_notification(self, *args: Any, **kwargs: Any) -> None:\n self._foreach("outgoing_notification", *args, **kwargs)\n\n def incoming_response(self, *args: Any, **kwargs: Any) -> None:\n self._foreach("incoming_response", *args, **kwargs)\n\n def incoming_request(self, *args: Any, **kwargs: Any) -> None:\n self._foreach("incoming_request", *args, **kwargs)\n\n def incoming_notification(self, *args: Any, **kwargs: Any) -> None:\n self._foreach("incoming_notification", *args, **kwargs)\n\n def _foreach(self, method: str, *args: Any, **kwargs: Any) -> None:\n for logger in self._loggers:\n getattr(logger, method)(*args, **kwargs)\n', 'languageId': 'python', 'version': 0, 'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: --> LSP-pylsp textDocument/codeLens (2 | 12:18:03): {'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: <<< LSP-pylsp (2 | 12:18:03 - 26ms): []
:: --> LSP-pylsp textDocument/hover (3 | 12:18:03): {'position': {'line': 561, 'character': 20}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: --> LSP-pylsp textDocument/codeAction (4 | 12:18:03): {'range': {'start': {'line': 561, 'character': 20}, 'end': {'line': 561, 'character': 20}}, 'context': {'diagnostics': [], 'triggerKind': 2}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: --> LSP-pylsp textDocument/hover (5 | 12:18:04): {'position': {'line': 556, 'character': 23}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: --> LSP-pylsp textDocument/codeAction (6 | 12:18:04): {'range': {'start': {'line': 556, 'character': 23}, 'end': {'line': 556, 'character': 23}}, 'context': {'diagnostics': [], 'triggerKind': 2}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: <- LSP-pylsp textDocument/publishDiagnostics (12:18:04): {'diagnostics': [{'range': {'start': {'line': 613, 'character': 120}, 'end': {'line': 613, 'character': 127}}, 'source': 'pycodestyle', 'message': 'E501 line too long (126 > 120 characters)', 'severity': 2, 'code': 'E501'}], 'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}
:: --> LSP-pylsp textDocument/codeAction (7 | 12:18:04): {'range': {'start': {'line': 564, 'character': 0}, 'end': {'line': 564, 'character': 0}}, 'context': {'diagnostics': [], 'triggerKind': 2}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: <<< LSP-pylsp (3 | 12:18:04 - 1552ms): {'contents': ''}
:: <<< LSP-pylsp (4 | 12:18:04 - 1546ms): []
:: <<< LSP-pylsp (5 | 12:18:04 - 429ms): {'contents': [{'value': 'userprefs() -> Settings', 'language': 'python'}]}
:: <<< LSP-pylsp (6 | 12:18:04 - 422ms): []
:: <<< LSP-pylsp (7 | 12:18:04 - 396ms): []
NOTE: Responses also have a time duration
https://github.com/sublimelsp/LSP/tree/timestamps-in-logs
- I'd put timestamp before server name so that it's aligned and easy to ignore if you are not looking for it
- Should have millisecond precision like
12:18:02.433 - Ideally pad it to certain amount of characters so that all timestamps are aligned.
- The "duration" could be separate