diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3cf8821d7..0766edf76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,3 +40,13 @@ Before you submit your pull request, please review the following: I will try to help you get the PR in mergeable shape within a reasonable time, but it may take a few days. It is best if you check your GitHub notifications in the meantime! + +## Releasing a new version (for maintainers) + +* Get a log of commits since the previously released tag with `git log --format="- format:%s (%an)" ..main` +* Filter out non-relevant and non-important commits (it's not relevant to report fixes for bugs that weren't released yet, for example) +* Optionally group changes into Fixes/Features/etc. +* Create a new file in `messages/` with a file name of the yet-to-be-released version and include the changes. +* Run `./scripts/release.py build` which will bump the version and create a new commit with a new messages file included. +* If something doesn't look right in the newly created commit, delete the newly created tag manually and git reset to the previous commit making sure that you don't lose the newly created messages file. +* Run `GITHUB_TOKEN= ./scripts/release.py publish` to push and create a new Github release. diff --git a/Context.sublime-menu b/Context.sublime-menu index 5defb140f..0b240f854 100644 --- a/Context.sublime-menu +++ b/Context.sublime-menu @@ -42,12 +42,7 @@ "caption": "Source Action…" }, { - "command": "lsp_format_document", - "caption": "Format File" - }, - { - "command": "lsp_format_document_range", - "caption": "Format Selection" + "command": "lsp_format" }, { "command": "lsp_expand_selection", diff --git a/Default.sublime-commands b/Default.sublime-commands index 92cfa689b..2a7e3bf29 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -70,28 +70,48 @@ "command": "lsp_restart_server", }, { - "caption": "LSP: Goto Symbol", + "caption": "LSP: Goto Symbol…", "command": "lsp_document_symbols", }, { - "caption": "LSP: Goto Implementation", - "command": "lsp_symbol_implementation", + "caption": "LSP: Goto Symbol In Project…", + "command": "lsp_workspace_symbols" + }, + { + "caption": "LSP: Goto Definition…", + "command": "lsp_symbol_definition" + }, + { + "caption": "LSP: Goto Type Definition…", + "command": "lsp_symbol_type_definition" }, { - "caption": "LSP: Goto Declaration", + "caption": "LSP: Goto Declaration…", "command": "lsp_symbol_declaration", }, { - "caption": "LSP: Goto Diagnostic", + "caption": "LSP: Goto Implementation…", + "command": "lsp_symbol_implementation", + }, + { + "caption": "LSP: Goto Diagnostic…", "command": "lsp_goto_diagnostic", "args": { "uri": "$view_uri" } }, { - "caption": "LSP: Goto Diagnostic in Project", + "caption": "LSP: Goto Diagnostic in Project…", "command": "lsp_goto_diagnostic" }, + { + "caption": "LSP: Find References", + "command": "lsp_symbol_references" + }, + { + "caption": "LSP: Follow Link", + "command": "lsp_open_link" + }, { "caption": "LSP: Toggle Log Panel", "command": "lsp_toggle_server_panel", @@ -100,10 +120,6 @@ "caption": "LSP: Toggle Diagnostics Panel", "command": "lsp_show_diagnostics_panel" }, - { - "caption": "LSP: Goto Symbol In Project", - "command": "lsp_workspace_symbols" - }, { "caption": "LSP: Rename", "command": "lsp_symbol_rename" @@ -134,8 +150,4 @@ "caption": "LSP: Run Code Lens", "command": "lsp_code_lens" }, - { - "caption": "LSP: Find References", - "command": "lsp_symbol_references" - } ] diff --git a/Default.sublime-keymap b/Default.sublime-keymap index 5a1e0c2b5..832dea744 100644 --- a/Default.sublime-keymap +++ b/Default.sublime-keymap @@ -6,447 +6,217 @@ // { // "keys": ["f1"], // "command": "show_overlay", - // "args": { - // "overlay": "command_palette", - // "text": "LSP: ", - // } + // "args": {"overlay": "command_palette", "text": "LSP: "} // }, - // Override native save to handle Code-Actions-On-Save - { - "command": "lsp_save", - "keys": [ - "primary+s" - ], - "context": [ - { - "key": "lsp.session_with_capability", - "operator": "equal", - "operand": "textDocumentSync.willSave | textDocumentSync.willSaveWaitUntil | codeActionProvider.codeActionKinds | documentFormattingProvider | documentRangeFormattingProvider" - } - ] - }, // Insert/Replace Completions { + "keys": ["alt+enter"], "command": "lsp_commit_completion_with_opposite_insert_mode", - "keys": [ - "alt+enter" - ], "context": [ - { - "key": "auto_complete_visible", - "operator": "equal", - "operand": true - }, - { - "key": "lsp.session_with_capability", - "operator": "equal", - "operand": "completionProvider" - } + {"key": "lsp.session_with_capability", "operand": "completionProvider"}, + {"key": "auto_complete_visible"} ] }, // Save all open files with lsp_save // { - // "command": "lsp_save_all", - // "keys": [ - // "UNBOUND" - // ] + // "keys": ["UNBOUND"], + // "command": "lsp_save_all" // }, // Run Code Action // { + // "keys": ["UNBOUND"], // "command": "lsp_code_actions", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "codeActionProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "codeActionProvider"}] // }, // Run Source Actions // { + // "keys": ["UNBOUND"], // "command": "lsp_code_actions", - // "args": { - // "only_kinds": [ - // "source" - // ] - // }, - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "codeActionProvider.codeActionKinds" - // } - // ] + // "args": {"only_kinds": ["source"]}, + // "context": [{"key": "lsp.session_with_capability", "operand": "codeActionProvider.codeActionKinds"}] // }, // Run Code Lens // { + // "keys": ["UNBOUND"], // "command": "lsp_code_lens", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "codeLensProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "codeLensProvider"}] // }, // Toggle Diagnostics Panel { + "keys": ["primary+alt+m"], "command": "lsp_show_diagnostics_panel", - "keys": [ - "primary+alt+m" - ], - "context": [ - { - "key": "setting.lsp_active" - } - ] + "context": [{"key": "setting.lsp_active"}] }, // Toggle Language Server Logs Panel // { + // "keys": ["UNBOUND"], // "command": "lsp_toggle_server_panel", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "setting.lsp_active" - // } - // ] + // "context": [{"key": "setting.lsp_active"}] // }, // Trigger Signature Help { + "keys": ["primary+alt+space"], "command": "lsp_signature_help_show", - "keys": [ - "primary+alt+space" - ], - "context": [ - { - "key": "lsp.signature_help_available", - "operator": "equal", - "operand": true - } - ] - }, - // Move Up/Down in Signature Help - { - "command": "lsp_signature_help_navigate", - "args": { - "forward": false - }, - "keys": [ - "up" - ], - "context": [ - { - "key": "lsp.signature_help_multiple_choices_available", - "operator": "equal", - "operand": true - } - ] - }, - { - "command": "lsp_signature_help_navigate", - "args": { - "forward": true - }, - "keys": [ - "down" - ], - "context": [ - { - "key": "lsp.signature_help_multiple_choices_available", - "operator": "equal", - "operand": true - } - ] + "context": [{"key": "lsp.signature_help_available"}] }, // Find References { + "keys": ["shift+f12"], "command": "lsp_symbol_references", - "args": { - "side_by_side": false, - "fallback": false - }, - "keys": [ - "shift+f12" - ], - "context": [ - { - "key": "lsp.session_with_capability", - "operator": "equal", - "operand": "referencesProvider" - }, - ] + "args": {"side_by_side": false, "fallback": false}, + "context": [{"key": "lsp.session_with_capability", "operand": "referencesProvider"}] }, + // Find References (side-by-side) + // { + // "keys": ["primary+shift+f12"], + // "command": "lsp_symbol_references", + // "args": {"side_by_side": true, "fallback": false}, + // "context": [{"key": "lsp.session_with_capability", "operand": "referencesProvider"}] + // }, // Goto Definition // { + // "keys": ["f12"], // "command": "lsp_symbol_definition", - // "args": { - // "side_by_side": false, - // "force_group": true, - // "fallback": false, - // "group": -1 - // }, - // "keys": [ - // "f12" - // ], + // "args": {"side_by_side": false, "force_group": true, "fallback": false, "group": -1}, // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "definitionProvider" - // }, - // { - // "key": "auto_complete_visible", - // "operator": "equal", - // "operand": false - // } + // {"key": "lsp.session_with_capability", "operand": "definitionProvider"}, + // {"key": "auto_complete_visible", "operand": false} + // ] + // }, + // Goto Definition (side-by-side) + // { + // "keys": ["primary+f12"], + // "command": "lsp_symbol_definition", + // "args": {"side_by_side": true, "force_group": true, "fallback": false, "group": -1}, + // "context": [ + // {"key": "lsp.session_with_capability", "operand": "definitionProvider"}, + // {"key": "auto_complete_visible", "operand": false} // ] // }, // Goto Type Definition // { + // "keys": ["UNBOUND"], // "command": "lsp_symbol_type_definition", - // "args": { - // "side_by_side": false, - // "force_group": true, - // "group": -1 - // }, - // "keys": [ - // "UNBOUND" - // ], + // "args": {"side_by_side": false, "force_group": true, "group": -1}, // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "typeDefinitionProvider" - // }, - // { - // "key": "auto_complete_visible", - // "operator": "equal", - // "operand": false - // } + // {"key": "lsp.session_with_capability", "operand": "typeDefinitionProvider"}, + // {"key": "auto_complete_visible", "operand": false} // ] // }, // Goto Declaration // { + // "keys": ["UNBOUND"], // "command": "lsp_symbol_declaration", - // "args": { - // "side_by_side": false, - // "force_group": true, - // "group": -1 - // }, - // "keys": [ - // "UNBOUND" - // ], + // "args": {"side_by_side": false, "force_group": true, "group": -1}, // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "declarationProvider" - // }, - // { - // "key": "auto_complete_visible", - // "operator": "equal", - // "operand": false - // } + // {"key": "lsp.session_with_capability", "operand": "declarationProvider"}, + // {"key": "auto_complete_visible", "operand": false} // ] // }, // Goto Implementation // { + // "keys": ["UNBOUND"], // "command": "lsp_symbol_implementation", - // "args": { - // "side_by_side": false, - // "force_group": true, - // "group": -1 - // }, - // "keys": [ - // "UNBOUND" - // ], + // "args": {"side_by_side": false, "force_group": true, "group": -1}, // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "implementationProvider" - // }, - // { - // "key": "auto_complete_visible", - // "operator": "equal", - // "operand": false - // } + // {"key": "lsp.session_with_capability", "operand": "implementationProvider"}, + // {"key": "auto_complete_visible", "operand": false} // ] // }, // Goto Diagnostic // { + // "keys": ["f8"], // "command": "lsp_goto_diagnostic", - // "args": { - // "uri": "$view_uri" - // }, - // "keys": [ - // "f8" - // ], + // "args": {"uri": "$view_uri"} // }, // Goto Diagnostic in Project // { - // "command": "lsp_goto_diagnostic", - // "keys": [ - // "shift+f8" - // ], + // "keys": ["shift+f8"], + // "command": "lsp_goto_diagnostic" // }, // Jump to next Diagnostic in Tab // { + // "keys": ["UNBOUND"], // "command": "lsp_next_diagnostic", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "setting.lsp_active" - // } - // ] + // "context": [{"key": "setting.lsp_active"}] // }, // Jump to previous Diagnostic in Tab // { + // "keys": ["UNBOUND"], // "command": "lsp_prev_diagnostic", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "setting.lsp_active" - // } - // ] + // "context": [{"key": "setting.lsp_active"}] // }, // Rename // { + // "keys": ["UNBOUND"], // "command": "lsp_symbol_rename", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "renameProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "renameProvider"}] // }, // Format File // { + // "keys": ["UNBOUND"], // "command": "lsp_format_document", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "documentFormattingProvider | documentRangeFormattingProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "documentFormattingProvider | documentRangeFormattingProvider"}] // }, // Format Selection // { + // "keys": ["UNBOUND"], // "command": "lsp_format_document_range", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "documentRangeFormattingProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "documentRangeFormattingProvider"}] // }, // Document Symbols (a replacement for ST's "Goto Symbol") // { + // "keys": ["primary+r"], // "command": "lsp_document_symbols", - // "keys": [ - // "primary+r" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "documentSymbolProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "documentSymbolProvider"}] // }, // Workspace Symbols (a replacement for ST's "Goto Symbol In Project") // { + // "keys": ["primary+shift+r"], // "command": "lsp_workspace_symbols", - // "keys": [ - // "primary+shift+r" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "workspaceSymbolProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "workspaceSymbolProvider"}] // }, // Hover Popup // { + // "keys": ["UNBOUND"], // "command": "lsp_hover", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "hoverProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "hoverProvider"}] // }, // Follow Link // { + // "keys": ["UNBOUND"], // "command": "lsp_open_link", - // "keys": [ - // "UNBOUND" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "documentLinkProvider" - // } - // ] + // "context": [{"key": "lsp.link_available"}] // }, // Expand Selection (a replacement for ST's "Expand Selection") // { + // "keys": ["primary+shift+a"], // "command": "lsp_expand_selection", - // "keys": [ - // "primary+shift+a" - // ], - // "context": [ - // { - // "key": "lsp.session_with_capability", - // "operator": "equal", - // "operand": "selectionRangeProvider" - // } - // ] + // "context": [{"key": "lsp.session_with_capability", "operand": "selectionRangeProvider"}] // }, - // Internal key-binding + //==== Internal key-bindings ==== { - "keys": [ - "" - ], + "keys": [""], "command": "noop", - "context": [ - { - "key": "setting.lsp_suppress_input", - } - ] + "context": [{"key": "setting.lsp_suppress_input"}] + }, + // Move Up/Down in Signature Help + { + "keys": ["up"], + "command": "lsp_signature_help_navigate", + "args": {"forward": false}, + "context": [{"key": "lsp.signature_help_multiple_choices_available"}] + }, + { + "keys": ["down"], + "command": "lsp_signature_help_navigate", + "args": {"forward": true}, + "context": [{"key": "lsp.signature_help_multiple_choices_available"}] + }, + // Override native save to handle Code-Actions-On-Save + { + "keys": ["primary+s"], + "command": "lsp_save", + "context": [{"key": "lsp.session_with_capability", "operand": "textDocumentSync.willSave | textDocumentSync.willSaveWaitUntil | codeActionProvider.codeActionKinds | documentFormattingProvider | documentRangeFormattingProvider"}] }, ] diff --git a/Main.sublime-menu b/Main.sublime-menu index 49a2c7dd2..f1976a033 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -7,16 +7,115 @@ "caption": "-" }, { - "caption": "LSP: Rename…", - "command": "lsp_symbol_rename" + "command": "lsp_source_action", + "args": {"id": -1} }, { - "caption": "LSP: Format File", - "command": "lsp_format_document" + "caption": "LSP: Source Action", + "children": [ + { + "command": "lsp_source_action", + "args": {"id": 0} + }, + { + "command": "lsp_source_action", + "args": {"id": 1} + }, + { + "command": "lsp_source_action", + "args": {"id": 2} + }, + { + "command": "lsp_source_action", + "args": {"id": 3} + }, + { + "command": "lsp_source_action", + "args": {"id": 4} + }, + { + "command": "lsp_source_action", + "args": {"id": 5} + }, + { + "command": "lsp_source_action", + "args": {"id": 6} + }, + { + "command": "lsp_source_action", + "args": {"id": 7} + }, + { + "command": "lsp_source_action", + "args": {"id": 8} + }, + { + "command": "lsp_source_action", + "args": {"id": 9} + } + ] + }, + { + "caption": "LSP: Refactor", + "children": [ + { + "caption": "Rename…", + "command": "lsp_symbol_rename" + }, + { + "command": "lsp_refactor", + "args": {"id": 0} + }, + { + "command": "lsp_refactor", + "args": {"id": 1} + }, + { + "command": "lsp_refactor", + "args": {"id": 2} + }, + { + "command": "lsp_refactor", + "args": {"id": 3} + }, + { + "command": "lsp_refactor", + "args": {"id": 4} + }, + { + "command": "lsp_refactor", + "args": {"id": 5} + }, + { + "command": "lsp_refactor", + "args": {"id": 6} + }, + { + "command": "lsp_refactor", + "args": {"id": 7} + }, + { + "command": "lsp_refactor", + "args": {"id": 8} + }, + { + "command": "lsp_refactor", + "args": {"id": 9} + } + ] }, { - "caption": "LSP: Format Selection", - "command": "lsp_format_document_range" + "caption": "LSP: Format", + "children": [ + { + "caption": "Format File", + "command": "lsp_format_document" + }, + { + "caption": "Format Selection", + "command": "lsp_format_document_range" + } + ] } ] }, diff --git a/VERSION b/VERSION index f1e7f0650..e4264e984 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.0 \ No newline at end of file +1.21.0 \ No newline at end of file diff --git a/boot.py b/boot.py index ed441e1ce..1e5798b71 100644 --- a/boot.py +++ b/boot.py @@ -4,6 +4,8 @@ # Please keep this list sorted (Edit -> Sort Lines) from .plugin.code_actions import LspCodeActionsCommand +from .plugin.code_actions import LspRefactorCommand +from .plugin.code_actions import LspSourceActionCommand from .plugin.code_lens import LspCodeLensCommand from .plugin.color import LspColorPresentationCommand from .plugin.completion import LspCommitCompletionWithOppositeInsertMode @@ -41,6 +43,7 @@ from .plugin.documents import TextChangeListener from .plugin.edit import LspApplyDocumentEditCommand from .plugin.execute_command import LspExecuteCommand +from .plugin.formatting import LspFormatCommand from .plugin.formatting import LspFormatDocumentCommand from .plugin.formatting import LspFormatDocumentRangeCommand from .plugin.goto import LspSymbolDeclarationCommand diff --git a/docs/src/customization.md b/docs/src/customization.md index b94d62fc7..d7a8fabd9 100644 --- a/docs/src/customization.md +++ b/docs/src/customization.md @@ -15,7 +15,7 @@ See the [mdpopups documentation](http://facelessuser.github.io/sublime-markdown- ## Keyboard shortcuts (key bindings) -LSP's key bindings can be edited from the `Preferences: LSP Key Bindings` command in the Command Palette. Many of the default key bindings (visible in the left view) are disabled to avoid conflicts with default or user key bindings. To enable those, copy them to your user key bindings on the right, un-comment, and pick the key shortcut of your choice. +LSP's key bindings can be edited from the `Preferences: LSP Key Bindings` command in the Command Palette. Many of the default key bindings (visible in the left view) are disabled to avoid conflicts with default or user key bindings. To enable those, copy them to your user key bindings on the right, un-comment, and pick the key shortcut of your choice. Check also the overview of available [Keyboard Shortcuts](keyboard_shortcuts.md). If you want to create a new key binding that is different from the ones that are already included, you might want to make it active only when there is a language server with a specific [LSP capability](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize) (refer to the `ServerCapabilities` structure in that link) running. In that case, you can make use of the `lsp.session_with_capability` context. For example, the following key binding overrides `ctrl+r` to use LSP's symbol provider but only when the current view has a language server with the `documentSymbolProvider` capability and we're in a javascript or a typescript file: diff --git a/docs/src/features.md b/docs/src/features.md index c70630831..f673c23bd 100644 --- a/docs/src/features.md +++ b/docs/src/features.md @@ -134,7 +134,7 @@ Some language servers provide _global_ rename functionality as well. This packag Code Actions are an umbrella term for "Quick Fixes" and "Refactorings". They are actions that change the file (or more than one file) to resolve a diagnostic or apply a standard refactor technique. For instance, extracting a block of code into a separate method is usually called "Extract Method" and is a "Refactoring". Whereas "add a missing semicolon" would resolve a diagnostic that warns about a missing semicolon. -Formatting is different from Code Actions, because Formatting is supposed to _not_ mutate the abstract syntax tree of the file, only move around white space. Any Code Action will mutate the abstract syntax tree. +Formatting is different from Code Actions because Formatting is supposed to _not_ mutate the abstract syntax tree of the file, only move around white space. Any Code Action will mutate the abstract syntax tree. This package presents "Quick Fix" Code Actions as a bluish clickable annotation positioned to the right of the viewport. Alternatively, they can be presented as a light bulb in the Gutter Area. @@ -184,8 +184,8 @@ A language server may itself also expose settings that you can use to customize ## Server Initialization Options -Initialization Options are like [Server Settings](concepts.md#server-settings), except they are static in the sense that they cannot be changed once the language server subprocess has started. +Initialization Options are like [Server Settings](#server-settings), except they are static in the sense that they cannot be changed once the language server subprocess has started. ## Subprocesses -A language server usually runs as a long-lived subprocess of Sublime Text. Once you start Sublime Text and open a view, the syntax of that view is matched against any possible client configurations registered. If a [client configuration](guides/client_configuration.md) matches, a subprocess is started that will then serve you language smartness. +A language server usually runs as a long-lived subprocess of Sublime Text. Once you start Sublime Text and open a view, the syntax of that view is matched against any possible client configurations registered. If a [client configuration](client_configuration.md) matches, a subprocess is started that will then serve you language smartness. diff --git a/docs/src/keyboard_shortcuts.md b/docs/src/keyboard_shortcuts.md index 4e16185f5..1d5d161d7 100644 --- a/docs/src/keyboard_shortcuts.md +++ b/docs/src/keyboard_shortcuts.md @@ -10,10 +10,13 @@ Refer to the [Customization section](customization.md#keyboard-shortcuts-key-bin | Auto Complete | ctrl space (also on macOS) | `auto_complete` | Expand Selection | unbound | `lsp_expand_selection` | Find References | shift f12 | `lsp_symbol_references` +| Follow Link | unbound | `lsp_open_link` | Format File | unbound | `lsp_format_document` | Format Selection | unbound | `lsp_format_document_range` | Goto Declaration | unbound | `lsp_symbol_declaration` | Goto Definition | unbound
suggested: f12 | `lsp_symbol_definition` +| Goto Diagnostic | unbound
suggested: f8 | `lsp_goto_diagnostic` (with args: `{"uri": "$view_uri"}`) +| Goto Diagnostic in Project | unbound
suggested: shift f8 | `lsp_goto_diagnostic` | Goto Implementation | unbound | `lsp_symbol_implementation` | Goto Symbol in Project | unbound
suggested: ctrl shift r | `lsp_workspace_symbols` | Goto Symbol | unbound
suggested: ctrl r | `lsp_document_symbols` diff --git a/docs/src/language_servers.md b/docs/src/language_servers.md index 91a756246..b150ae640 100644 --- a/docs/src/language_servers.md +++ b/docs/src/language_servers.md @@ -4,7 +4,7 @@ Follow the setup steps for a language server to get it up and running. If you encounter problems, consult the [common issues](troubleshooting.md#common-problems) page or search the [LSP issues](https://github.com/sublimelsp/LSP/issues) before opening new ones. -If there are no setup steps for a language server on this page, but a [language server implementation](https://microsoft.github.io/language-server-protocol/implementors/servers/) exist, follow the guide for [creating a client configuration](./guides/client_configuration.md). Pull requests for adding a new client configuration are welcome. +If there are no setup steps for a language server on this page, but a [language server implementation](https://microsoft.github.io/language-server-protocol/implementors/servers/) exist, follow the guide for [creating a client configuration](./client_configuration.md). Pull requests for adding a new client configuration are welcome. !!! tip "We recommend installing [LSP-json](https://packagecontrol.io/packages/LSP-json)." [LSP-json](https://packagecontrol.io/packages/LSP-json) provides completions and diagnostics when editing JSON files that adhere to a JSON schema. @@ -65,17 +65,17 @@ Follow installation instructions on [LSP-css](https://github.com/sublimelsp/LSP- ```json { - "clients": { - "serve-d": { - "enabled": true, - "command": ["C:/Users/MY_NAME_HERE/AppData/Roaming/code-d/bin/serve-d.exe"], - "selector": "source.d", - "settings": { - "d.dcdServerPath": "C:/Users/MY_NAME_HERE/AppData/Roaming/code-d/bin/dcd-server.exe", - "d.dcdClientPath": "C:/Users/MY_NAME_HERE/AppData/Roaming/code-d/bin/dcd-client.exe", - } - } - } + "clients": { + "serve-d": { + "enabled": true, + "command": ["C:/Users/MY_NAME_HERE/AppData/Roaming/code-d/bin/serve-d.exe"], + "selector": "source.d", + "settings": { + "d.dcdServerPath": "C:/Users/MY_NAME_HERE/AppData/Roaming/code-d/bin/dcd-server.exe", + "d.dcdClientPath": "C:/Users/MY_NAME_HERE/AppData/Roaming/code-d/bin/dcd-client.exe", + } + } + } } ``` @@ -129,7 +129,7 @@ Follow installation instructions on [LSP-elm](https://github.com/sublimelsp/LSP- "clients": { "fsautocomplete": { "enabled": true, - "command": ["fsautocomplete", "--background-service-enabled"], + "command": ["fsautocomplete"], "selector": "source.fsharp", "initializationOptions": { "AutomaticWorkspaceInit": true @@ -323,6 +323,27 @@ Follow installation instructions on [LSP-lua](https://github.com/sublimelsp/LSP- Spell check can be provided by [LSP-ltex-ls](https://github.com/LDAP/LSP-ltex-ls). +### markmark + +[Markmark](https://github.com/nikku/markmark) is a language server for Markdown files, supporting go to definition / references [and more](https://github.com/nikku/markmark#features). + +1. [Install Markmark](https://github.com/nikku/markmark#installation) (requires `Node >= 16`) +2. Open `Preferences > Package Settings > LSP > Settings` and add the `"markmark"` client configuration to the `"clients"`: + + + ```json + { + "clients": { + "markmark": { + "enabled": true, + "command": ["markmark-lsp", "--stdio"], + "selector": "text.html.markdown" + } + } + } + ``` + + ## OCaml/Reason 1. Install the [Reason](https://packagecontrol.io/packages/Reason) package from Package Control for syntax highlighting. diff --git a/language-ids.sublime-settings b/language-ids.sublime-settings index c9ddc48f7..95587bbae 100644 --- a/language-ids.sublime-settings +++ b/language-ids.sublime-settings @@ -90,6 +90,7 @@ "source.yaml.sublime.syntax": "yaml", // https://github.com/SublimeText/PackageDev "source.yaml.yarn": "yaml", // https://github.com/SublimeText/AFileIcon "text.advanced_csv": "csv", // https://github.com/SublimeText/AFileIcon + "text.django": "html", // https://github.com/willstott101/django-sublime-syntax "text.html.basic": "html", "text.html.elixir": "html", // https://github.com/elixir-editors/elixir-tmbundle "text.html.markdown.academicmarkdown": "markdown", // https://github.com/mangecoeur/AcademicMarkdown @@ -97,21 +98,21 @@ "text.html.markdown.rmarkdown": "r", // https://github.com/REditorSupport/sublime-ide-r "text.html.ngx": "html", // https://github.com/princemaple/ngx-html-syntax "text.jinja": "html", // https://github.com/Sublime-Instincts/BetterJinja - "text.plain": "txt", - "text.plain.buildpacks": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.eslint": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.fastq": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.license": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.lnk": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.log": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.nodejs": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.pcb": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.ps": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.python": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.readme": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.ruby": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.sketch": "txt", // https://github.com/SublimeText/AFileIcon - "text.plain.visualstudio": "txt", // https://github.com/SublimeText/AFileIcon + "text.plain": "plaintext", + "text.plain.buildpacks": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.eslint": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.fastq": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.license": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.lnk": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.log": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.nodejs": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.pcb": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.ps": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.python": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.readme": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.ruby": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.sketch": "plaintext", // https://github.com/SublimeText/AFileIcon + "text.plain.visualstudio": "plaintext", // https://github.com/SublimeText/AFileIcon "text.plist": "xml", // https://bitbucket.org/fschwehn/sublime_plist "text.xml.plist": "xml", // https://github.com/SublimeText/PackageDev "text.xml.plist.textmate.preferences": "xml", // https://github.com/SublimeText/PackageDev diff --git a/messages.json b/messages.json index cfbebd96b..640e40c62 100644 --- a/messages.json +++ b/messages.json @@ -18,6 +18,7 @@ "1.2.8": "messages/1.2.8.txt", "1.2.9": "messages/1.2.9.txt", "1.20.0": "messages/1.20.0.txt", + "1.21.0": "messages/1.21.0.txt", "1.3.0": "messages/1.3.0.txt", "1.3.1": "messages/1.3.1.txt", "1.4.0": "messages/1.4.0.txt", diff --git a/messages/1.21.0.txt b/messages/1.21.0.txt new file mode 100644 index 000000000..6c9e91dda --- /dev/null +++ b/messages/1.21.0.txt @@ -0,0 +1,34 @@ +=> 1.21.0 + +# Features + +- Add "Source Action" entry to the "Edit" main menu (#2149) (jwortmann) +- Add "Refactor" entry to the "Edit" main menu (#2141) (jwortmann) +- Auto-restart on server crashing, up to 5 times (#2145) (Lucas Alber) + +# Fixes and Improvements + +- Fix inlay hint parts wrapping into multiple lines (#2153) (Rafał Chłodnicki) +- Ensure commands triggered from minihtml run on correct view (#2154) (Rafał Chłodnicki) +- Fix completion documentation being parsed as markdown twice (#2146) (Rafał Chłodnicki) +- When going to definition, scroll to start of the region, not end (#2147) (Rafał Chłodnicki) +- Improve performance of completion & signature request on typing (#2148) (Rafał Chłodnicki) +- Fix code lenses not updating after Undo (#2139) (Rafał Chłodnicki) +- Add missing Goto commands to Command Palette (#2140) (Rafał Chłodnicki) +- docs: add missing keyboard shortcuts (#2143) (Rafał Chłodnicki) +- Pass force_group to LocationPicker (#2134) (Rafał Chłodnicki) +- Don't advertise support for disabled code actions (#2137) (Rafał Chłodnicki) +- Add context for lsp_open_link key binding (#2138) (jwortmann) +- Fix prepareRename support (#2127) (Rafał Chłodnicki) +- docs(language_servers): add markmark language server (for Markdown) (#2129) (Nico Rehwaldt) +- Fix plugin overwrite `on_workspace_configuration` (#2132) (Lucas Alber) +- Hide inactive items in context menu (#2124) (jwortmann) +- Don't show a source if diagnostic doesn't have a source (#2119) (Sainan) +- Combine file and range formatting entries in context menu (#2123) (jwortmann) +- Add language id for Django templates (Jannis Vajen) +- Nicer presentation for "find references/definition" quick panel (#2109) (Rafał Chłodnicki) +- Make Goto Diagnostic overlays consistent (#2115) (jwortmann) +- Don't trigger code action requests for background views (#2108) (Rafał Chłodnicki) +- Ignore diagnostics for files in folder_exclude_patterns (#2113) (jwortmann) +- Fix diagnostics underline color priority (#2106) (jwortmann) +- Fix diagnostics_additional_delay_auto_complete_ms not working after split view (#2107) (jwortmann) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index e1946fdcd..278c07559 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -16,13 +16,17 @@ 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 +from .save_command import LspSaveCommand +from .save_command import SaveTask +from abc import ABCMeta +from abc import abstractmethod from functools import partial import sublime ConfigName = str CodeActionOrCommand = Union[CodeAction, Command] CodeActionsByConfigName = Tuple[ConfigName, List[CodeActionOrCommand]] +MENU_ACTIONS_KINDS = [CodeActionKind.Refactor, CodeActionKind.Source] def is_command(action: CodeActionOrCommand) -> TypeGuard[Command]: @@ -34,6 +38,9 @@ class CodeActionsManager: def __init__(self) -> None: self._response_cache = None # type: Optional[Tuple[str, Promise[List[CodeActionsByConfigName]]]] + self.menu_actions_cache_key = None # type: Optional[str] + self.refactor_actions_cache = [] # type: List[Tuple[str, CodeAction]] + self.source_actions_cache = [] # type: List[Tuple[str, CodeAction]] def request_for_region_async( self, @@ -49,6 +56,7 @@ def request_for_region_async( """ listener = windows.listener_for_view(view) if not listener: + self.menu_actions_cache_key = None return Promise.resolve([]) location_cache_key = None use_cache = not manual @@ -60,6 +68,10 @@ def request_for_region_async( return task else: self._response_cache = None + elif only_kinds == MENU_ACTIONS_KINDS: + self.menu_actions_cache_key = "{}#{}:{}".format(view.buffer_id(), view.change_count(), region) + self.refactor_actions_cache.clear() + self.source_actions_cache.clear() def request_factory(session: Session) -> Optional[Request]: diagnostics = [] # type: List[Diagnostic] @@ -73,13 +85,22 @@ def request_factory(session: Session) -> Optional[Request]: def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> List[CodeActionOrCommand]: # Filter out non "quickfix" code actions unless "only_kinds" is provided. if only_kinds: - return [a for a in actions if not is_command(a) and kinds_include_kind(only_kinds, a.get('kind'))] + code_actions = [cast(CodeAction, a) for a in actions if not is_command(a) and not a.get('disabled')] + if manual and only_kinds == MENU_ACTIONS_KINDS: + for action in code_actions: + kind = action.get('kind') + if kinds_include_kind([CodeActionKind.Refactor], kind): + self.refactor_actions_cache.append((session.config.name, action)) + elif kinds_include_kind([CodeActionKind.Source], kind): + self.source_actions_cache.append((session.config.name, action)) + return [action for action in code_actions if kinds_include_kind(only_kinds, action.get('kind'))] if manual: - return actions + return [a for a in actions if not a.get('disabled')] # On implicit (selection change) request, only return commands and quick fix kinds. return [ a for a in actions - if is_command(a) or not a.get('kind') or kinds_include_kind([CodeActionKind.QuickFix], a.get('kind')) + if is_command(a) or not a.get('disabled') and + kinds_include_kind([CodeActionKind.QuickFix], a.get('kind', CodeActionKind.QuickFix)) ] task = self._collect_code_actions_async(listener, request_factory, response_filter) @@ -115,7 +136,7 @@ def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> Lis # actions that need to be then manually filtered. session_kinds = get_session_kinds(session) matching_kinds = get_matching_on_save_kinds(on_save_actions, session_kinds) - return [a for a in actions if a.get('kind') in matching_kinds] + return [a for a in actions if a.get('kind') in matching_kinds and not a.get('disabled')] return self._collect_code_actions_async(listener, request_factory, response_filter) @@ -129,12 +150,9 @@ def _collect_code_actions_async( def on_response( session: Session, response: Union[Error, Optional[List[CodeActionOrCommand]]] ) -> CodeActionsByConfigName: - if isinstance(response, Error): - actions = [] - else: - actions = [action for action in (response or []) if not action.get('disabled')] - if actions and response_filter: - actions = response_filter(session, actions) + actions = [] + if response and not isinstance(response, Error) and response_filter: + actions = response_filter(session, response) return (session.config.name, actions) tasks = [] # type: List[Promise[CodeActionsByConfigName]] @@ -256,6 +274,16 @@ class LspCodeActionsCommand(LspTextCommand): capability = 'codeActionProvider' + def is_visible( + self, + event: Optional[dict] = None, + point: Optional[int] = None, + only_kinds: Optional[List[CodeActionKind]] = None + ) -> bool: + if self.applies_to_context_menu(event): + return self.is_enabled(event, point) + return True + def run( self, edit: sublime.Edit, @@ -323,3 +351,76 @@ def run_async() -> None: def _handle_response_async(self, session_name: str, response: Any) -> None: if isinstance(response, Error): sublime.error_message("{}: {}".format(session_name, str(response))) + + +class LspMenuActionCommand(LspTextCommand, metaclass=ABCMeta): + """Handles a particular kind of code actions with the purpose to list them as items in a submenu.""" + + capability = 'codeActionProvider' + + @property + @abstractmethod + def actions_cache(self) -> List[Tuple[str, CodeAction]]: + ... + + def is_enabled(self, id: int, event: Optional[dict] = None, point: Optional[int] = None) -> bool: + if not super().is_enabled(event, point): + return False + return -1 < id < len(self.actions_cache) + + def is_visible(self, id: int, event: Optional[dict] = None, point: Optional[int] = None) -> bool: + if id == -1: + if super().is_enabled(event, point): + sublime.set_timeout_async(partial(self._request_menu_actions_async, event)) + return False + return id < len(self.actions_cache) and self._is_cache_valid(event) + + def description(self, id: int, event: Optional[dict] = None, point: Optional[int] = None) -> Optional[str]: + if -1 < id < len(self.actions_cache): + return self.actions_cache[id][1]['title'] + + def run(self, edit: sublime.Edit, id: int, event: Optional[dict] = None, point: Optional[int] = None) -> None: + sublime.set_timeout_async(partial(self.run_async, id, event)) + + def run_async(self, id: int, event: Optional[dict]) -> None: + if self._is_cache_valid(event): + config_name, action = self.actions_cache[id] + session = self.session_by_name(config_name) + if session: + session.run_code_action_async(action, progress=True) \ + .then(lambda response: self._handle_response_async(config_name, response)) + + def _handle_response_async(self, session_name: str, response: Any) -> None: + if isinstance(response, Error): + sublime.error_message("{}: {}".format(session_name, str(response))) + + def _is_cache_valid(self, event: Optional[dict]) -> bool: + region = self._get_region(event) + if region is None: + return False + v = self.view + return actions_manager.menu_actions_cache_key == "{}#{}:{}".format(v.buffer_id(), v.change_count(), region) + + def _get_region(self, event: Optional[dict]) -> Optional[sublime.Region]: + if event is not None and self.applies_to_context_menu(event): + return sublime.Region(self.view.window_to_text((event['x'], event['y']))) + return first_selection_region(self.view) + + def _request_menu_actions_async(self, event: Optional[dict]) -> None: + region = self._get_region(event) + if region is not None: + actions_manager.request_for_region_async(self.view, region, [], MENU_ACTIONS_KINDS, True) + + +class LspRefactorCommand(LspMenuActionCommand): + + @property + def actions_cache(self) -> List[Tuple[str, CodeAction]]: + return actions_manager.refactor_actions_cache + + +class LspSourceActionCommand(LspMenuActionCommand): + + @property + def actions_cache(self) -> List[Tuple[str, CodeAction]]: + return actions_manager.source_actions_cache diff --git a/plugin/code_lens.py b/plugin/code_lens.py index 87fa8eddb..116898a0f 100644 --- a/plugin/code_lens.py +++ b/plugin/code_lens.py @@ -1,4 +1,6 @@ -from .core.protocol import CodeLens, CodeLensExtended, Error +from .core.protocol import CodeLens +from .core.protocol import CodeLensExtended +from .core.protocol import Error from .core.typing import List, Tuple, Dict, Iterable, Generator, Union, cast from .core.registry import LspTextCommand from .core.registry import windows @@ -23,7 +25,7 @@ def __init__(self, data: CodeLens, view: sublime.View, session_name: str) -> Non self.region = range_to_region(data['range'], view) self.session_name = session_name self.annotation = '...' - self.resolve_annotation() + self.resolve_annotation(view.id()) self.is_resolve_error = False def __repr__(self) -> str: @@ -42,7 +44,7 @@ def to_lsp(self) -> CodeLensExtended: def small_html(self) -> str: return '{}'.format(self.annotation) - def resolve_annotation(self) -> None: + def resolve_annotation(self, view_id: int) -> None: command = self.data.get('command') if command is not None: command_name = command.get('command') @@ -51,7 +53,7 @@ def resolve_annotation(self) -> None: 'session_name': self.session_name, 'command_name': command_name, 'command_args': command.get('arguments', []), - }) + }, view_id=view_id) else: self.annotation = html_escape(command['title']) else: @@ -64,7 +66,7 @@ def resolve(self, view: sublime.View, code_lens_or_error: Union[CodeLens, Error] return self.data = code_lens_or_error self.region = range_to_region(code_lens_or_error['range'], view) - self.resolve_annotation() + self.resolve_annotation(view.id()) class CodeLensView: diff --git a/plugin/completion.py b/plugin/completion.py index edd90fbb5..eae376b3b 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -68,13 +68,13 @@ def run_main() -> None: if not self.view.is_valid(): return if self.view.is_popup_visible(): - update_lsp_popup(self.view, minihtml_content, md=True) + update_lsp_popup(self.view, minihtml_content, md=False) else: show_lsp_popup( self.view, minihtml_content, flags=sublime.COOPERATE_WITH_AUTO_COMPLETE, - md=True, + md=False, on_navigate=self._on_navigate) sublime.set_timeout(run_main) diff --git a/plugin/core/active_request.py b/plugin/core/active_request.py new file mode 100644 index 000000000..bdd39c9e7 --- /dev/null +++ b/plugin/core/active_request.py @@ -0,0 +1,73 @@ +from .sessions import SessionViewProtocol +from .progress import ProgressReporter +from .progress import ViewProgressReporter +from .progress import WindowProgressReporter +from .protocol import Request +from .typing import Any, Optional, Dict +from weakref import ref +import sublime + + +class ActiveRequest: + """ + Holds state per request. + """ + + def __init__(self, sv: SessionViewProtocol, request_id: int, request: Request) -> None: + # sv is the parent object; there is no need to keep it alive explicitly. + self.weaksv = ref(sv) + self.request_id = request_id + self.request = request + self.progress = None # type: Optional[ProgressReporter] + # `request.progress` is either a boolean or a string. If it's a boolean, then that signals that the server does + # not support client-initiated progress. However, for some requests we still want to notify some kind of + # progress to the end-user. This is communicated by the boolean value being "True". + # If `request.progress` is a string, then this string is equal to the workDoneProgress token. In that case, the + # server should start reporting progress for this request. However, even if the server supports workDoneProgress + # then we still don't know for sure whether it will actually start reporting progress. So we still want to + # put a line in the status bar if the request takes a while even if the server promises to report progress. + if request.progress: + # Keep a weak reference because we don't want this delayed function to keep this object alive. + weakself = ref(self) + + def show() -> None: + this = weakself() + # If the server supports client-initiated progress, then it should have sent a "progress begin" + # notification. In that case, `this.progress` should not be None. So if `this.progress` is None + # then the server didn't notify in a timely manner and we will start putting a line in the status bar + # about this request taking a long time (>200ms). + if this is not None and this.progress is None: + # If this object is still alive then that means the request hasn't finished yet after 200ms, + # so put a message in the status bar to notify that this request is still in progress. + this.progress = this._start_progress_reporter_async(this.request.method) + + sublime.set_timeout_async(show, 200) + + def _start_progress_reporter_async( + self, + title: str, + message: Optional[str] = None, + percentage: Optional[float] = None + ) -> Optional[ProgressReporter]: + sv = self.weaksv() + if not sv: + return None + if self.request.view is not None: + key = "lspprogressview-{}-{}-{}".format(sv.session.config.name, self.request.view.id(), self.request_id) + return ViewProgressReporter(self.request.view, key, title, message, percentage) + else: + key = "lspprogresswindow-{}-{}-{}".format(sv.session.config.name, sv.session.window.id(), self.request_id) + return WindowProgressReporter(sv.session.window, key, title, message, percentage) + + def update_progress_async(self, params: Dict[str, Any]) -> None: + value = params['value'] + kind = value['kind'] + message = value.get("message") + percentage = value.get("percentage") + if kind == 'begin': + title = value["title"] + # This would potentially overwrite the "manual" progress that activates after 200ms, which is OK. + self.progress = self._start_progress_reporter_async(title, message, percentage) + elif kind == 'report': + if self.progress: + self.progress(message, percentage) diff --git a/plugin/core/configurations.py b/plugin/core/configurations.py index 7bd5cca3a..cc5d55c41 100644 --- a/plugin/core/configurations.py +++ b/plugin/core/configurations.py @@ -1,17 +1,25 @@ from .logging import debug from .logging import exception_log +from .logging import printf from .types import ClientConfig -from .typing import Generator, List, Optional, Set, Dict +from .typing import Generator, List, Optional, Set, Dict, Deque from .workspace import enable_in_project, disable_in_project +from collections import deque +from datetime import datetime, timedelta import sublime import urllib.parse +RETRY_MAX_COUNT = 5 +RETRY_COUNT_TIMEDELTA = timedelta(minutes=3) + + class WindowConfigManager(object): def __init__(self, window: sublime.Window, global_configs: Dict[str, ClientConfig]) -> None: self._window = window self._global_configs = global_configs self._disabled_for_session = set() # type: Set[str] + self._crashes = {} # type: Dict[str, Deque[datetime]] self.all = {} # type: Dict[str, ClientConfig] self.update() @@ -73,6 +81,22 @@ def disable_config(self, config_name: str, only_for_session: bool = False) -> No disable_in_project(self._window, config_name) self.update(config_name) + def record_crash(self, config_name: str, exit_code: int, exception: Optional[Exception]) -> bool: + """ + Signal that a session has crashed. + + Returns True if the session should be restarted automatically. + """ + if config_name not in self._crashes: + self._crashes[config_name] = deque(maxlen=RETRY_MAX_COUNT) + now = datetime.now() + self._crashes[config_name].append(now) + timeout = now - RETRY_COUNT_TIMEDELTA + crash_count = len([crash for crash in self._crashes[config_name] if crash > timeout]) + printf("{} crashed ({} / {} times in the last {} seconds), exit code {}, exception: {}".format( + config_name, crash_count, RETRY_MAX_COUNT, RETRY_COUNT_TIMEDELTA.total_seconds(), exit_code, exception)) + return crash_count < RETRY_MAX_COUNT + def _disable_for_session(self, config_name: str) -> None: self._disabled_for_session.add(config_name) diff --git a/plugin/core/edit.py b/plugin/core/edit.py index 275934dee..d32fc6a49 100644 --- a/plugin/core/edit.py +++ b/plugin/core/edit.py @@ -1,7 +1,10 @@ from .logging import debug from .open import open_file from .promise import Promise -from .protocol import UINT_MAX, TextEdit as LspTextEdit, Position, WorkspaceEdit +from .protocol import Position +from .protocol import TextEdit +from .protocol import UINT_MAX +from .protocol import WorkspaceEdit from .typing import List, Dict, Optional, Tuple from functools import partial import sublime @@ -37,7 +40,7 @@ def parse_range(range: Position) -> Tuple[int, int]: return range['line'], min(UINT_MAX, range['character']) -def parse_text_edit(text_edit: LspTextEdit, version: Optional[int] = None) -> TextEditTuple: +def parse_text_edit(text_edit: TextEdit, version: Optional[int] = None) -> TextEditTuple: return ( parse_range(text_edit['range']['start']), parse_range(text_edit['range']['end']), diff --git a/plugin/core/open.py b/plugin/core/open.py index adb7b6248..3de49c71a 100644 --- a/plugin/core/open.py +++ b/plugin/core/open.py @@ -117,10 +117,10 @@ def center_selection(v: sublime.View, r: Range) -> sublime.View: if window: window.focus_view(v) if int(sublime.version()) >= 4124: - v.show_at_center(selection, animate=False) + v.show_at_center(selection.begin(), animate=False) else: # TODO: remove later when a stable build lands - v.show_at_center(selection) # type: ignore + v.show_at_center(selection.begin()) # type: ignore return v diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 8220070c6..c0325cb17 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -5861,15 +5861,15 @@ def __init__( self.progress = progress # type: Union[bool, str] @classmethod - def initialize(cls, params: Mapping[str, Any]) -> 'Request': + def initialize(cls, params: InitializeParams) -> 'Request': return Request("initialize", params) @classmethod - def complete(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def complete(cls, params: CompletionParams, view: sublime.View) -> 'Request': return Request("textDocument/completion", params, view) @classmethod - def signatureHelp(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def signatureHelp(cls, params: SignatureHelpParams, view: sublime.View) -> 'Request': return Request("textDocument/signatureHelp", params, view) @classmethod @@ -5877,7 +5877,7 @@ def codeAction(cls, params: CodeActionParams, view: sublime.View) -> 'Request': return Request("textDocument/codeAction", params, view) @classmethod - def documentColor(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def documentColor(cls, params: DocumentColorParams, view: sublime.View) -> 'Request': return Request('textDocument/documentColor', params, view) @classmethod @@ -5885,7 +5885,7 @@ def colorPresentation(cls, params: ColorPresentationParams, view: sublime.View) return Request('textDocument/colorPresentation', params, view) @classmethod - def willSaveWaitUntil(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def willSaveWaitUntil(cls, params: WillSaveTextDocumentParams, view: sublime.View) -> 'Request': return Request("textDocument/willSaveWaitUntil", params, view) @classmethod @@ -5893,23 +5893,23 @@ def documentSymbols(cls, params: DocumentSymbolParams, view: sublime.View) -> 'R return Request("textDocument/documentSymbol", params, view, progress=True) @classmethod - def documentHighlight(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def documentHighlight(cls, params: DocumentHighlightParams, view: sublime.View) -> 'Request': return Request("textDocument/documentHighlight", params, view) @classmethod - def documentLink(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def documentLink(cls, params: DocumentLinkParams, view: sublime.View) -> 'Request': return Request("textDocument/documentLink", params, view) @classmethod - def semanticTokensFull(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def semanticTokensFull(cls, params: SemanticTokensParams, view: sublime.View) -> 'Request': return Request("textDocument/semanticTokens/full", params, view) @classmethod - def semanticTokensFullDelta(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def semanticTokensFullDelta(cls, params: SemanticTokensDeltaParams, view: sublime.View) -> 'Request': return Request("textDocument/semanticTokens/full/delta", params, view) @classmethod - def semanticTokensRange(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def semanticTokensRange(cls, params: SemanticTokensRangeParams, view: sublime.View) -> 'Request': return Request("textDocument/semanticTokens/range", params, view) @classmethod @@ -5928,6 +5928,18 @@ def inlayHint(cls, params: InlayHintParams, view: sublime.View) -> 'Request': def resolveInlayHint(cls, params: InlayHint, view: sublime.View) -> 'Request': return Request('inlayHint/resolve', params, view) + @classmethod + def rename(cls, params: RenameParams, view: sublime.View, progress: bool = False) -> 'Request': + return Request('textDocument/rename', params, view, progress) + + @classmethod + def prepareRename(cls, params: PrepareRenameParams, view: sublime.View, progress: bool = False) -> 'Request': + return Request('textDocument/prepareRename', params, view, progress) + + @classmethod + def selectionRange(cls, params: SelectionRangeParams) -> 'Request': + return Request('textDocument/selectionRange', params) + @classmethod def workspaceSymbol(cls, params: WorkspaceSymbolParams) -> 'Request': return Request("workspace/symbol", params, None, progress=True) @@ -6003,35 +6015,35 @@ def initialized(cls) -> 'Notification': return Notification("initialized", {}) @classmethod - def didOpen(cls, params: dict) -> 'Notification': + def didOpen(cls, params: DidOpenTextDocumentParams) -> 'Notification': return Notification("textDocument/didOpen", params) @classmethod - def didChange(cls, params: dict) -> 'Notification': + def didChange(cls, params: DidChangeTextDocumentParams) -> 'Notification': return Notification("textDocument/didChange", params) @classmethod - def willSave(cls, params: dict) -> 'Notification': + def willSave(cls, params: WillSaveTextDocumentParams) -> 'Notification': return Notification("textDocument/willSave", params) @classmethod - def didSave(cls, params: dict) -> 'Notification': + def didSave(cls, params: DidSaveTextDocumentParams) -> 'Notification': return Notification("textDocument/didSave", params) @classmethod - def didClose(cls, params: dict) -> 'Notification': + def didClose(cls, params: DidCloseTextDocumentParams) -> 'Notification': return Notification("textDocument/didClose", params) @classmethod - def didChangeConfiguration(cls, params: dict) -> 'Notification': + def didChangeConfiguration(cls, params: DidChangeConfigurationParams) -> 'Notification': return Notification("workspace/didChangeConfiguration", params) @classmethod - def didChangeWatchedFiles(cls, params: dict) -> 'Notification': + def didChangeWatchedFiles(cls, params: DidChangeWatchedFilesParams) -> 'Notification': return Notification("workspace/didChangeWatchedFiles", params) @classmethod - def didChangeWorkspaceFolders(cls, params: dict) -> 'Notification': + def didChangeWorkspaceFolders(cls, params: DidChangeWorkspaceFoldersParams) -> 'Notification': return Notification("workspace/didChangeWorkspaceFolders", params) @classmethod diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 14063f531..53d9308c7 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -108,6 +108,10 @@ def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) def want_event(self) -> bool: return True + @staticmethod + def applies_to_context_menu(event: Optional[dict]) -> bool: + return event is not None and 'x' in event + def get_listener(self) -> Optional[AbstractViewListener]: return windows.listener_for_view(self.view) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 6f957613c..07fd91df6 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -27,6 +27,7 @@ from .protocol import DiagnosticSeverity from .protocol import DiagnosticTag from .protocol import DidChangeWatchedFilesRegistrationOptions +from .protocol import DidChangeWorkspaceFoldersParams from .protocol import DocumentLink from .protocol import DocumentUri from .protocol import Error @@ -35,12 +36,15 @@ from .protocol import FailureHandlingKind from .protocol import FileEvent from .protocol import GeneralClientCapabilities +from .protocol import InitializeParams from .protocol import InsertTextMode from .protocol import Location from .protocol import LocationLink +from .protocol import LSPAny from .protocol import LSPObject from .protocol import MarkupKind from .protocol import Notification +from .protocol import PrepareSupportDefaultBehavior from .protocol import PublishDiagnosticsParams from .protocol import Range from .protocol import Request @@ -204,7 +208,7 @@ def _enum_like_class_to_list(c: Type[object]) -> List[Union[int, str]]: def get_initialize_params(variables: Dict[str, str], workspace_folders: List[WorkspaceFolder], - config: ClientConfig) -> dict: + config: ClientConfig) -> InitializeParams: completion_kinds = cast(List[CompletionItemKind], _enum_like_class_to_list(CompletionItemKind)) symbol_kinds = cast(List[SymbolKind], _enum_like_class_to_list(SymbolKind)) diagnostic_tag_value_set = cast(List[DiagnosticTag], _enum_like_class_to_list(DiagnosticTag)) @@ -335,7 +339,6 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor } }, "dataSupport": True, - "disabledSupport": True, "isPreferredSupport": True, "resolveSupport": { "properties": [ @@ -345,7 +348,8 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor }, "rename": { "dynamicRegistration": True, - "prepareSupport": True + "prepareSupport": True, + "prepareSupportDefaultBehavior": PrepareSupportDefaultBehavior.Identifier, }, "colorProvider": { "dynamicRegistration": True # exceptional @@ -451,7 +455,7 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor "rootPath": first_folder.path if first_folder else None, "workspaceFolders": [folder.to_lsp() for folder in workspace_folders] if workspace_folders else None, "capabilities": capabilities, - "initializationOptions": config.init_options.get_resolved(variables) + "initializationOptions": cast(LSPAny, config.init_options.get_resolved(variables)) } @@ -600,6 +604,10 @@ def sessions_async(self, capability_path: Optional[str] = None) -> Generator['Se def session_views_async(self) -> Iterable['SessionViewProtocol']: raise NotImplementedError() + @abstractmethod + def purge_changes_async(self) -> None: + raise NotImplementedError() + @abstractmethod def on_session_initialized_async(self, session: 'Session') -> None: raise NotImplementedError() @@ -866,14 +874,16 @@ def on_settings_changed(self, settings: DottedDict) -> None: """ pass - def on_workspace_configuration(self, params: Dict, configuration: Any) -> None: + def on_workspace_configuration(self, params: Dict, configuration: Any) -> Any: """ Override to augment configuration returned for the workspace/configuration request. :param params: A ConfigurationItem for which configuration is requested. - :param configuration: The resolved configuration for given params. + :param configuration: The pre-resolved configuration for given params using the settings object or None. + + :returns: The resolved configuration for given params. """ - pass + return configuration def on_pre_server_command(self, command: Mapping[str, Any], done_callback: Callable[[], None]) -> bool: """ @@ -936,12 +946,16 @@ def on_session_buffer_changed_async(self, session_buffer: SessionBufferProtocol) """ pass - def on_session_end_async(self) -> None: + def on_session_end_async(self, exit_code: Optional[int], exception: Optional[Exception]) -> None: """ Notifies about the session ending (also if the session has crashed). Provides an opportunity to clean up any stored state or delete references to the session or plugin instance that would otherwise prevent the - instance from being garbage-collected. If the plugin hasn't crashed, a shutdown message will be send immediately - after this method returns. + instance from being garbage-collected. + + If the session hasn't crashed, a shutdown message will be send immediately + after this method returns. In this case exit_code and exception are None. + If the session has crashed, the exit_code and an optional exception are provided. + This API is triggered on async thread. """ pass @@ -1112,7 +1126,8 @@ def check_applicable(self, sb: SessionBufferProtocol) -> None: return -_WORK_DONE_PROGRESS_PREFIX = "wd" +# This prefix should disambiguate common string generation techniques like UUID4. +_WORK_DONE_PROGRESS_PREFIX = "$ublime-" class Session(TransportCallbacks): @@ -1337,7 +1352,7 @@ def update_folders(self, folders: List[WorkspaceFolder]) -> None: "added": [a.to_lsp() for a in added], "removed": [r.to_lsp() for r in removed] } - } + } # type: DidChangeWorkspaceFoldersParams self.send_notification(Notification.didChangeWorkspaceFolders(params)) if self._supports_workspace_folders(): self._workspace_folders = folders @@ -1644,8 +1659,9 @@ def m_workspace_configuration(self, params: Dict[str, Any], request_id: Any) -> for requested_item in requested_items: configuration = self.config.settings.copy(requested_item.get('section') or None) if self._plugin: - self._plugin.on_workspace_configuration(requested_item, configuration) - items.append(configuration) + items.append(self._plugin.on_workspace_configuration(requested_item, configuration)) + else: + items.append(configuration) self.send_response(Response(request_id, sublime.expand_variables(items, self._template_variables()))) def m_workspace_applyEdit(self, params: Any, request_id: Any) -> None: @@ -1798,28 +1814,46 @@ def _invoke_views(self, request: Request, method: str, *args: Any) -> None: for sv in self.session_views_async(): getattr(sv, method)(*args) + def _create_window_progress_reporter(self, token: str, value: Dict[str, Any]) -> None: + self._progress[token] = WindowProgressReporter( + window=self.window, + key="lspprogress{}{}".format(self.config.name, token), + title=value["title"], + message=value.get("message") + ) + def m___progress(self, params: Any) -> None: """handles the $/progress notification""" token = params['token'] + value = params['value'] + kind = value['kind'] if token not in self._progress: + # If the token is not in the _progress map then that could mean two things: + # + # 1) The server is reporting on our client-initiated request progress. In that case, the progress token + # should be of the form $_WORK_DONE_PROGRESS_PREFIX$RequestId. We try to parse it, and if it succeeds, + # we can delegate to the appropriate session view instances. + # + # 2) The server is not spec-compliant and reports progress using server-initiated progress but didn't + # call window/workDoneProgress/create before hand. In that case, we check the 'kind' field of the + # progress data. If the 'kind' field is 'begin', we set up a progress reporter anyway. try: request_id = int(token[len(_WORK_DONE_PROGRESS_PREFIX):]) request = self._response_handlers[request_id][0] self._invoke_views(request, "on_request_progress", request_id, params) return except (IndexError, ValueError, KeyError): + # The parse failed so possibility (1) is apparently not applicable. At this point we may still be + # dealing with possibility (2). + if kind == 'begin': + # We are dealing with possibility (2), so create the progress reporter now. + self._create_window_progress_reporter(token, value) + return pass debug('unknown $/progress token: {}'.format(token)) return - value = params['value'] - kind = value['kind'] if kind == 'begin': - self._progress[token] = WindowProgressReporter( - window=self.window, - key="lspprogress{}{}".format(self.config.name, token), - title=value["title"], - message=value.get("message") - ) + self._create_window_progress_reporter(token, value) elif kind == 'report': progress = self._progress[token] assert isinstance(progress, WindowProgressReporter) @@ -1841,7 +1875,7 @@ def end_async(self) -> None: return self.exiting = True if self._plugin: - self._plugin.on_session_end_async() + self._plugin.on_session_end_async(None, None) self._plugin = None for sv in self.session_views_async(): for status_key in self._status_messages.keys(): @@ -1868,7 +1902,7 @@ def on_transport_close(self, exit_code: int, exception: Optional[Exception]) -> self.transport = None self._response_handlers.clear() if self._plugin: - self._plugin.on_session_end_async() + self._plugin.on_session_end_async(exit_code, exception) self._plugin = None if self._initialize_error: # Override potential exit error with a saved one. diff --git a/plugin/core/types.py b/plugin/core/types.py index 658390afc..6857f9a9d 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -6,7 +6,6 @@ from .typing import cast, TypedDict, TypeVar from .url import filename_to_uri from .url import parse_uri -from threading import RLock from wcmatch.glob import BRACE from wcmatch.glob import globmatch from wcmatch.glob import GLOBSTAR @@ -134,41 +133,46 @@ def __del__(self) -> None: self._settings.clear_on_change("LSP") -class Debouncer: +class DebouncerNonThreadSafe: + """ + Debouncer for delaying execution of a function until specified timeout time. + + When calling `debounce()` multiple times, if the time span between calls is shorter than the specified `timeout_ms`, + the callback function will only be called once, after `timeout_ms` since the last call. + + This implementation is not thread safe. You must ensure that `debounce()` is called from the same thread as + was choosen during initialization through the `async_thread` argument. + """ - def __init__(self) -> None: + def __init__(self, async_thread: bool) -> None: + self._async_thread = async_thread self._current_id = -1 self._next_id = 0 - self._current_id_lock = RLock() - def debounce(self, f: Callable[[], None], timeout_ms: int = 0, condition: Callable[[], bool] = lambda: True, - async_thread: bool = False) -> None: + def debounce( + self, f: Callable[[], None], timeout_ms: int = 0, condition: Callable[[], bool] = lambda: True + ) -> None: """ - Possibly run a function at a later point in time, either on the async thread or on the main thread. + Possibly run a function at a later point in time on the thread chosen during initialization. :param f: The function to possibly run :param timeout_ms: The time in milliseconds after which to possibly to run the function :param condition: The condition that must evaluate to True in order to run the funtion - :param async_thread: If true, run the function on the async worker thread, otherwise run - the function on the main thread """ def run(debounce_id: int) -> None: - with self._current_id_lock: - if debounce_id != self._current_id: - return + if debounce_id != self._current_id: + return if condition(): f() - runner = sublime.set_timeout_async if async_thread else sublime.set_timeout - with self._current_id_lock: - current_id = self._current_id = self._next_id + runner = sublime.set_timeout_async if self._async_thread else sublime.set_timeout + current_id = self._current_id = self._next_id self._next_id += 1 runner(lambda: run(current_id), timeout_ms) def cancel_pending(self) -> None: - with self._current_id_lock: - self._current_id = -1 + self._current_id = -1 def read_dict_setting(settings_obj: sublime.Settings, key: str, default: dict) -> dict: diff --git a/plugin/core/version.py b/plugin/core/version.py index df39e2cdb..4c68f1f1b 100644 --- a/plugin/core/version.py +++ b/plugin/core/version.py @@ -1 +1 @@ -__version__ = (1, 20, 0) +__version__ = (1, 21, 0) diff --git a/plugin/core/views.py b/plugin/core/views.py index 552b7c39c..cb8119119 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -13,6 +13,11 @@ from .protocol import Diagnostic from .protocol import DiagnosticRelatedInformation from .protocol import DiagnosticSeverity +from .protocol import DidChangeTextDocumentParams +from .protocol import DidCloseTextDocumentParams +from .protocol import DidOpenTextDocumentParams +from .protocol import DidSaveTextDocumentParams +from .protocol import DocumentColorParams from .protocol import DocumentHighlightKind from .protocol import DocumentUri from .protocol import ExperimentalTextDocumentRangeParams @@ -25,9 +30,15 @@ from .protocol import Position from .protocol import Range from .protocol import Request +from .protocol import SelectionRangeParams from .protocol import SymbolKind +from .protocol import TextDocumentContentChangeEvent from .protocol import TextDocumentIdentifier +from .protocol import TextDocumentItem from .protocol import TextDocumentPositionParams +from .protocol import TextDocumentSaveReason +from .protocol import VersionedTextDocumentIdentifier +from .protocol import WillSaveTextDocumentParams from .settings import userprefs from .types import ClientConfig from .typing import Callable, Optional, Dict, Any, Iterable, List, Union, Tuple, cast @@ -185,34 +196,26 @@ sublime.KIND_VARIABLE: "entity.name.constant | constant.other | support.constant | variable.other | variable.parameter | variable.other.member | variable.other.readwrite.member" # noqa: E501 } # type: Dict[SublimeKind, str] -SYMBOL_KIND_SCOPES = { - SymbolKind.File: "string", - SymbolKind.Module: "entity.name.namespace", - SymbolKind.Namespace: "entity.name.namespace", - SymbolKind.Package: "entity.name.namespace", - SymbolKind.Class: "entity.name.class", - SymbolKind.Method: "entity.name.function", - SymbolKind.Property: "variable.other.member", - SymbolKind.Field: "variable.other.member", - SymbolKind.Constructor: "entity.name.function.constructor", - SymbolKind.Enum: "entity.name.enum", - SymbolKind.Interface: "entity.name.interface", - SymbolKind.Function: "entity.name.function", - SymbolKind.Variable: "variable.other", - SymbolKind.Constant: "variable.other.constant", - SymbolKind.String: "string", - SymbolKind.Number: "constant.numeric", - SymbolKind.Boolean: "constant.language.boolean", - SymbolKind.Array: "meta.sequence", - SymbolKind.Object: "meta.mapping", - SymbolKind.Key: "meta.mapping.key string", - SymbolKind.Null: "constant.language.null", - SymbolKind.EnumMember: "constant.other.enum", - SymbolKind.Struct: "entity.name.struct", - SymbolKind.Event: "entity.name.function", - SymbolKind.Operator: "keyword.operator", - SymbolKind.TypeParameter: "variable.parameter.type" -} # type: Dict[SymbolKind, str] +# Recommended colors to use by themes for each symbol kind, based on the kind_container specialization class described +# at https://www.sublimetext.com/docs/themes.html#quick-panel +SUBLIME_KIND_ID_COLOR_SCOPES = { + sublime.KIND_ID_KEYWORD: "region.pinkish", + sublime.KIND_ID_TYPE: "region.purplish", + sublime.KIND_ID_FUNCTION: "region.redish", + sublime.KIND_ID_NAMESPACE: "region.bluish", + sublime.KIND_ID_NAVIGATION: "region.yellowish", + sublime.KIND_ID_MARKUP: "region.orangish", + sublime.KIND_ID_VARIABLE: "region.cyanish", + sublime.KIND_ID_SNIPPET: "region.greenish", + sublime.KIND_ID_COLOR_REDISH: "region.redish", + sublime.KIND_ID_COLOR_ORANGISH: "region.orangish", + sublime.KIND_ID_COLOR_YELLOWISH: "region.yellowish", + sublime.KIND_ID_COLOR_GREENISH: "region.greenish", + sublime.KIND_ID_COLOR_CYANISH: "region.cyanish", + sublime.KIND_ID_COLOR_BLUISH: "region.bluish", + sublime.KIND_ID_COLOR_PURPLISH: "region.purplish", + sublime.KIND_ID_COLOR_PINKISH: "region.pinkish" +} # type: Dict[int, str] DOCUMENT_HIGHLIGHT_KINDS = { DocumentHighlightKind.Text: "text", @@ -431,6 +434,11 @@ def first_selection_region(view: sublime.View) -> Optional[sublime.Region]: return None +def has_single_nonempty_selection(view: sublime.View) -> bool: + selections = view.sel() + return len(selections) == 1 and not selections[0].empty() + + def entire_content_region(view: sublime.View) -> sublime.Region: return sublime.Region(0, view.size()) @@ -443,7 +451,7 @@ def entire_content_range(view: sublime.View) -> Range: return region_to_range(view, entire_content_region(view)) -def text_document_item(view: sublime.View, language_id: str) -> Dict[str, Any]: +def text_document_item(view: sublime.View, language_id: str) -> TextDocumentItem: return { "uri": uri_from_view(view), "languageId": language_id, @@ -452,7 +460,7 @@ def text_document_item(view: sublime.View, language_id: str) -> Dict[str, Any]: } -def versioned_text_document_identifier(view: sublime.View, version: int) -> Dict[str, Any]: +def versioned_text_document_identifier(view: sublime.View, version: int) -> VersionedTextDocumentIdentifier: return {"uri": uri_from_view(view), "version": version} @@ -469,11 +477,11 @@ def text_document_range_params(view: sublime.View, location: int, } -def did_open_text_document_params(view: sublime.View, language_id: str) -> Dict[str, Any]: +def did_open_text_document_params(view: sublime.View, language_id: str) -> DidOpenTextDocumentParams: return {"textDocument": text_document_item(view, language_id)} -def render_text_change(change: sublime.TextChange) -> Dict[str, Any]: +def render_text_change(change: sublime.TextChange) -> TextDocumentContentChangeEvent: # Note: cannot use protocol.Range because these are "historic" points. return { "range": { @@ -484,10 +492,14 @@ def render_text_change(change: sublime.TextChange) -> Dict[str, Any]: } -def did_change_text_document_params(view: sublime.View, version: int, - changes: Optional[Iterable[sublime.TextChange]] = None) -> Dict[str, Any]: - content_changes = [] # type: List[Dict[str, Any]] - result = {"textDocument": versioned_text_document_identifier(view, version), "contentChanges": content_changes} +def did_change_text_document_params( + view: sublime.View, version: int, changes: Optional[Iterable[sublime.TextChange]] = None +) -> DidChangeTextDocumentParams: + content_changes = [] # type: List[TextDocumentContentChangeEvent] + result = { + "textDocument": versioned_text_document_identifier(view, version), + "contentChanges": content_changes + } # type: DidChangeTextDocumentParams if changes is None: # TextDocumentSyncKind.Full content_changes.append({"text": entire_content(view)}) @@ -498,21 +510,24 @@ def did_change_text_document_params(view: sublime.View, version: int, return result -def will_save_text_document_params(view_or_uri: Union[DocumentUri, sublime.View], reason: int) -> Dict[str, Any]: +def will_save_text_document_params( + view_or_uri: Union[DocumentUri, sublime.View], reason: TextDocumentSaveReason +) -> WillSaveTextDocumentParams: return {"textDocument": text_document_identifier(view_or_uri), "reason": reason} def did_save_text_document_params( view: sublime.View, include_text: bool, uri: Optional[DocumentUri] = None -) -> Dict[str, Any]: - identifier = text_document_identifier(uri if uri is not None else view) - result = {"textDocument": identifier} # type: Dict[str, Any] +) -> DidSaveTextDocumentParams: + result = { + "textDocument": text_document_identifier(uri if uri is not None else view) + } # type: DidSaveTextDocumentParams if include_text: result["text"] = entire_content(view) return result -def did_close_text_document_params(uri: DocumentUri) -> Dict[str, Any]: +def did_close_text_document_params(uri: DocumentUri) -> DidCloseTextDocumentParams: return {"textDocument": text_document_identifier(uri)} @@ -525,11 +540,11 @@ def did_change(view: sublime.View, version: int, return Notification.didChange(did_change_text_document_params(view, version, changes)) -def will_save(uri: DocumentUri, reason: int) -> Notification: +def will_save(uri: DocumentUri, reason: TextDocumentSaveReason) -> Notification: return Notification.willSave(will_save_text_document_params(uri, reason)) -def will_save_wait_until(view: sublime.View, reason: int) -> Request: +def will_save_wait_until(view: sublime.View, reason: TextDocumentSaveReason) -> Request: return Request.willSaveWaitUntil(will_save_text_document_params(view, reason), view) @@ -574,7 +589,7 @@ def text_document_range_formatting(view: sublime.View, region: sublime.Region) - }, view, progress=True) -def selection_range_params(view: sublime.View) -> Dict[str, Any]: +def selection_range_params(view: sublime.View) -> SelectionRangeParams: return { "textDocument": text_document_identifier(view), "positions": [position(view, r.b) for r in view.sel()] @@ -715,12 +730,6 @@ def minihtml( "allow_code_wrap": True, "markdown_extensions": [ "markdown.extensions.admonition", - { - "pymdownx.escapeall": { - "hardbreak": True, - "nbsp": False - } - }, { "pymdownx.magiclink": { # links are displayed without the initial ftp://, http://, https://, or ftps://. @@ -782,10 +791,10 @@ def make_link(href: str, text: Any, class_name: Optional[str] = None) -> str: def make_command_link(command: str, text: str, command_args: Optional[Dict[str, Any]] = None, - class_name: Optional[str] = None, view: Optional[sublime.View] = None) -> str: - if view: + class_name: Optional[str] = None, view_id: Optional[int] = None) -> str: + if view_id is not None: cmd = "lsp_run_text_command_helper" - args = {"view_id": view.id(), "command": command, "args": command_args} # type: Optional[Dict[str, Any]] + args = {"view_id": view_id, "command": command, "args": command_args} # type: Optional[Dict[str, Any]] else: cmd = command args = command_args @@ -840,7 +849,7 @@ def lsp_color_to_phantom(view: sublime.View, color_info: ColorInformation) -> su return sublime.Phantom(region, lsp_color_to_html(color_info), sublime.LAYOUT_INLINE) -def document_color_params(view: sublime.View) -> Dict[str, Any]: +def document_color_params(view: sublime.View) -> DocumentColorParams: return {"textDocument": text_document_identifier(view)} @@ -854,10 +863,6 @@ def diagnostic_severity(diagnostic: Diagnostic) -> DiagnosticSeverity: return diagnostic.get("severity", DiagnosticSeverity.Error) -def diagnostic_source(diagnostic: Diagnostic) -> str: - return diagnostic.get("source", "unknown-source") - - def format_diagnostics_for_annotation( diagnostics: List[Diagnostic], view: sublime.View ) -> Tuple[List[sublime.Region], List[str]]: @@ -890,14 +895,15 @@ def format_diagnostic_for_panel(diagnostic: Diagnostic) -> Tuple[str, Optional[i """ formatted, code, href = diagnostic_source_and_code(diagnostic) lines = diagnostic["message"].splitlines() or [""] - # \u200B is the zero-width space - result = " {:>4}:{:<4}{:<8}{} \u200B{}".format( + result = " {:>4}:{:<4}{:<8}{}".format( diagnostic["range"]["start"]["line"] + 1, diagnostic["range"]["start"]["character"] + 1, format_severity(diagnostic_severity(diagnostic)), - lines[0], - formatted + lines[0] ) + if formatted != "" or code is not None: + # \u200B is the zero-width space + result += " \u200B{}".format(formatted) offset = len(result) if href else None for line in itertools.islice(lines, 1, None): result += "\n" + 18 * " " + line @@ -908,22 +914,21 @@ def format_diagnostic_source_and_code(diagnostic: Diagnostic) -> str: formatted, code, href = diagnostic_source_and_code(diagnostic) if href is None or code is None: return formatted - return formatted + code + return formatted + "({})".format(code) def diagnostic_source_and_code(diagnostic: Diagnostic) -> Tuple[str, Optional[str], Optional[str]]: - formatted = [diagnostic_source(diagnostic)] + formatted = diagnostic.get("source", "") href = None code = diagnostic.get("code") if code is not None: code = str(code) - formatted.append(":") code_description = diagnostic.get("codeDescription") if code_description: href = code_description["href"] else: - formatted.append(code) - return "".join(formatted), code, href + formatted += "({})".format(code) + return formatted, code, href def location_to_human_readable( @@ -993,10 +998,6 @@ def _with_color(text: Any, hexcolor: str) -> str: return '{}'.format(hexcolor, text) -def _with_scope_color(view: sublime.View, text: Any, scope: str) -> str: - return _with_color(text, view.style_for_scope(scope)["foreground"]) - - def format_diagnostic_for_html( view: sublime.View, config: ClientConfig, @@ -1010,16 +1011,22 @@ def format_diagnostic_for_html( text2html(diagnostic["message"]) ] code_description = diagnostic.get("codeDescription") - if code_description: - code = make_link(code_description["href"], diagnostic.get("code")) # type: Optional[str] - elif "code" in diagnostic: - code = _with_color(diagnostic["code"], "color(var(--foreground) alpha(0.6))") + if "code" in diagnostic: + code = [_with_color("(", "color(var(--foreground) alpha(0.6))")] + if code_description: + code.append(make_link(code_description["href"], diagnostic.get("code"))) + else: + code.append(_with_color(diagnostic["code"], "color(var(--foreground) alpha(0.6))")) + code.append(_with_color(")", "color(var(--foreground) alpha(0.6))")) else: code = None - source = diagnostic_source(diagnostic) - formatted.extend((" ", _with_color(source, "color(var(--foreground) alpha(0.6))"))) + source = diagnostic.get("source") + if source or code: + formatted.append(" ") + if source: + formatted.append(_with_color(source, "color(var(--foreground) alpha(0.6))")) if code: - formatted.extend((_with_scope_color(view, ":", "punctuation.separator.lsp"), code)) + formatted.extend(code) related_infos = diagnostic.get("relatedInformation") if related_infos: formatted.append('