diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cfbdba108..f314543a3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.x diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 108338e0f..0ed6bbedc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,9 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - if: ${{ matrix.os == 'macOS-latest' }} + run: brew unlink openssl - uses: SublimeText/UnitTesting/actions/setup@v1 - uses: SublimeText/UnitTesting/actions/run-tests@v1 with: @@ -32,7 +34,7 @@ jobs: Lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: '3.8' diff --git a/Default.sublime-commands b/Default.sublime-commands index 86765bffe..f0e9e026e 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -57,6 +57,11 @@ "caption": "LSP: Format File", "command": "lsp_format_document", }, + { + "caption": "LSP: Format File With", + "command": "lsp_format_document", + "args": {"select": true} + }, { "caption": "LSP: Format Selection", "command": "lsp_format_document_range", @@ -70,38 +75,38 @@ "command": "lsp_restart_server", }, { - "caption": "LSP: Goto Symbol…", + "caption": "LSP: Goto Symbol", "command": "lsp_document_symbols", }, { - "caption": "LSP: Goto Symbol In Project…", + "caption": "LSP: Goto Symbol in Project", "command": "lsp_workspace_symbols" }, { - "caption": "LSP: Goto Definition…", + "caption": "LSP: Goto Definition", "command": "lsp_symbol_definition" }, { - "caption": "LSP: Goto Type 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 Implementation…", + "caption": "LSP: Goto Implementation", "command": "lsp_symbol_implementation", }, { - "caption": "LSP: Goto Diagnostic…", + "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" }, { @@ -125,11 +130,11 @@ "command": "lsp_symbol_rename" }, { - "caption": "LSP: Code Action…", + "caption": "LSP: Code Action", "command": "lsp_code_actions" }, { - "caption": "LSP: Refactor…", + "caption": "LSP: Refactor", "command": "lsp_code_actions", "args": { "only_kinds": [ @@ -138,7 +143,7 @@ }, }, { - "caption": "LSP: Source Action…", + "caption": "LSP: Source Action", "command": "lsp_code_actions", "args": { "only_kinds": [ @@ -162,4 +167,9 @@ "caption": "LSP: Toggle Inlay Hints", "command": "lsp_toggle_inlay_hints", }, + // { + // "caption": "LSP: Fold All Comment Blocks", + // "command": "lsp_fold_all", + // "args": {"kind": "comment"} + // }, ] diff --git a/Default.sublime-keymap b/Default.sublime-keymap index 542001736..d95ffdb9e 100644 --- a/Default.sublime-keymap +++ b/Default.sublime-keymap @@ -63,7 +63,7 @@ { "keys": ["shift+f12"], "command": "lsp_symbol_references", - "args": {"side_by_side": false, "force_group": true, "fallback": false, "group": -1}, + "args": {"side_by_side": false, "force_group": true, "fallback": false, "group": -1, "include_declaration": false}, "context": [{"key": "lsp.session_with_capability", "operand": "referencesProvider"}] }, // Find References (side-by-side) @@ -211,8 +211,18 @@ // { // "keys": ["primary+shift+a"], // "command": "lsp_expand_selection", + // "args": {"fallback": false}, // "context": [{"key": "lsp.session_with_capability", "operand": "selectionRangeProvider"}] // }, + // Fold around caret position - an optional "strict" argument can be used to configure whether + // to fold only when the caret is contained within the folded region (true), or even when it is + // anywhere on the starting line (false). + // { + // "keys": ["UNBOUND"], + // "command": "lsp_fold", + // "args": {"strict": true}, + // "context": [{"key": "lsp.session_with_capability", "operand": "foldingRangeProvider"}] + // }, //==== Internal key-bindings ==== { "keys": [""], diff --git a/LSP.sublime-settings b/LSP.sublime-settings index ac7b08aac..b5692d58d 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -59,6 +59,16 @@ // hint: 4 "show_diagnostics_severity_level": 4, + // Show diagnostics as annotations with level equal to or less than: + // none: 0 (never show) + // error: 1 + // warning: 2 + // info: 3 + // hint: 4 + // When enabled, it's recommended not to use the "annotation" value for the + // `show_code_actions` option as it's impossible to enforce which one gets shown first. + "show_diagnostics_annotations_severity_level": 0, + // Open the diagnostics panel automatically on save when diagnostics level is // equal to or less than: // none: 0 (never open the panel automatically) @@ -202,6 +212,14 @@ // about how to configure your color scheme for semantic highlighting. "semantic_highlighting": false, + // Determines ranges which initially should be folded when a document is opened, + // provided that the language server has support for this. + "initially_folded": [ + // "comment", + // "imports", + // "region", + ], + // --- Debugging ---------------------------------------------------------------------- // Show verbose debug messages in the sublime console. diff --git a/Main.sublime-menu b/Main.sublime-menu index 7ff4e44a5..0b39db9c5 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -2,10 +2,27 @@ { "id": "edit", "children": [ + { + "id": "fold", + "children": [ + { + "id": "lsp", + "caption": "-" + }, + { + "command": "lsp_fold", + "args": {"prefetch": true} + } + ] + }, { "id": "lsp", "caption": "-" }, + { + "command": "lsp_fold", + "args": {"prefetch": true, "hidden": true} + }, { "command": "lsp_source_action", "args": {"id": -1} diff --git a/VERSION b/VERSION index d43704683..bc584045a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.0 \ No newline at end of file +1.26.0 \ No newline at end of file diff --git a/annotations.css b/annotations.css new file mode 100644 index 000000000..e20ed8866 --- /dev/null +++ b/annotations.css @@ -0,0 +1,16 @@ +.lsp_annotation { + margin: 0; + border-width: 0; +} +.lsp_annotation .errors { + color: color(var(--redish) alpha(0.85)); +} +.lsp_annotation .warnings { + color: color(var(--yellowish) alpha(0.85)); +} +.lsp_annotation .info { + color: color(var(--bluish) alpha(0.85)); +} +.lsp_annotation .hints { + color: color(var(--bluish) alpha(0.85)); +} diff --git a/boot.py b/boot.py index 8c8712faa..686215f2f 100644 --- a/boot.py +++ b/boot.py @@ -25,7 +25,6 @@ from .plugin.core.registry import LspNextDiagnosticCommand from .plugin.core.registry import LspOpenLocationCommand from .plugin.core.registry import LspPrevDiagnosticCommand -from .plugin.core.registry import LspRecheckSessionsCommand from .plugin.core.registry import LspRestartServerCommand from .plugin.core.registry import windows from .plugin.core.sessions import AbstractPlugin @@ -43,6 +42,8 @@ from .plugin.documents import TextChangeListener from .plugin.edit import LspApplyDocumentEditCommand from .plugin.execute_command import LspExecuteCommand +from .plugin.folding_range import LspFoldAllCommand +from .plugin.folding_range import LspFoldCommand from .plugin.formatting import LspFormatCommand from .plugin.formatting import LspFormatDocumentCommand from .plugin.formatting import LspFormatDocumentRangeCommand diff --git a/dependencies.json b/dependencies.json index dfe18a876..b7f778153 100644 --- a/dependencies.json +++ b/dependencies.json @@ -1,11 +1,9 @@ { "*": { ">=4096": [ - "backrefs", "bracex", "mdpopups", "pathlib", - "pyyaml", "wcmatch" ] } diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index c554020a1..3c963200f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -25,12 +25,18 @@ theme: extra_css: - stylesheets/extra.css +extra_javascript: + - js/redirect.js + markdown_extensions: # tabs for ST4 and ST3 content - pymdownx.tabbed - # higlight code block - - pymdownx.highlight + # highlight code blocks + - pymdownx.highlight: + extend_pygments_lang: + - name: jsonc + lang: json - pymdownx.superfences # add tip, warning info boxes diff --git a/docs/src/client_configuration.md b/docs/src/client_configuration.md index db471c8e8..c75c5ce9a 100644 --- a/docs/src/client_configuration.md +++ b/docs/src/client_configuration.md @@ -14,7 +14,7 @@ If your language server is missing or not configured correctly, you need to add/ Below is an example of the `LSP.sublime-settings` file with configurations for the [Phpactor](https://phpactor.readthedocs.io/en/master/usage/language-server.html#language-server) server. -```js +```jsonc { // General settings "show_diagnostics_panel_on_save": 0, @@ -65,6 +65,8 @@ The vast majority of language servers can communicate over stdio. To use stdio, Some language servers can also act as a TCP server accepting incoming TCP connections. So: the language server subprocess is started by this package, and the subprocess will then open a TCP listener port. The editor can then connect as a client and initiate the communication. To use this mode, set `tcp_port` to a positive number designating the port to connect to on `localhost`. +Optionally in this case, you can omit the `command` setting if you don't want Sublime LSP to manage the language server process and you'll take care of it yourself. + ### TCP - localhost - editor acts as a TCP server Some _LSP servers_ instead expect the _LSP client_ to act as a _TCP server_. The _LSP server_ will then connect as a _TCP client_, after which the _LSP client_ is expected to initiate the communication. To use this mode, set `tcp_port` to a negative number designating the port to bind to for accepting new TCP connections. @@ -79,7 +81,7 @@ The port number can be inserted into the server's startup `command` in your clie Global LSP settings (which currently are `lsp_format_on_save` and `lsp_code_actions_on_save`) can be overridden per-project in `.sublime-project` file: -```json +```jsonc { "folders": [ @@ -97,7 +99,7 @@ Also global language server settings can be added or overridden per-project by a > **Note**: The `settings` and `initializationOptions` objects for server configurations will be merged with globally defined server configurations so it's possible to override only certain properties from those objects. -```json +```jsonc { "folders": [ diff --git a/docs/src/commands.md b/docs/src/commands.md index e6274fb65..ebb7d0a65 100644 --- a/docs/src/commands.md +++ b/docs/src/commands.md @@ -19,7 +19,7 @@ For LSP servers that can handle [workspace/executeCommand](https://microsoft.git Example: -```js +```jsonc [ // ... { diff --git a/docs/src/customization.md b/docs/src/customization.md index ede4de8ce..e6970328a 100644 --- a/docs/src/customization.md +++ b/docs/src/customization.md @@ -4,7 +4,7 @@ LSP's key bindings can be edited from the `Preferences: LSP Key Bindings` comman 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: -```js +```jsonc { "command": "lsp_document_symbols", "keys": [ @@ -39,7 +39,7 @@ If you want to bind some action to a mouse, open `Preferences / Browser Packages Here is an example of a mouse binding that triggers LSP's "go to symbol definition" command on pressing the ctrl+left click: -```js +```jsonc [ { "button": "button1", @@ -83,6 +83,9 @@ The following tables give an overview of the scope names used by LSP. ### Semantic Highlighting +!!! note + Semantic highlighting is disabled by default. To enable it, set `"semantic_highlighting": true` in your LSP user settings. + !!! info "This feature is only available if the server has the *semanticTokensProvider* capability." Language servers that support semantic highlighting are for example *clangd* and *rust-analyzer*. @@ -90,7 +93,7 @@ In order to support semantic highlighting, the color scheme requires a special r LSP automatically adds such a rule to the built-in color schemes from Sublime Text. If you use a custom color scheme, select `UI: Customize Color Scheme` from the Command Palette and add for example the following code: -```json +```jsonc { "rules": [ { @@ -103,7 +106,7 @@ If you use a custom color scheme, select `UI: Customize Color Scheme` from the C Furthermore, it is possible to adjust the colors for semantic tokens by applying a foreground color to the individual token types: -| scope | [Semantic Token Type](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#semanticTokenTypes) | +| scope | [Semantic Token Type](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#semanticTokenTypes) | | ----- | ------------------ | | `meta.semantic-token.namespace` | namespace | | `meta.semantic-token.type` | type | @@ -133,9 +136,9 @@ By default, LSP will assign scopes based on the [scope naming guideline](https:/ Language servers can also add their custom token types, which are not defined in the protocol. An "LSP-\*" helper package (or user) can provide a `semantic_tokens` mapping in the server configuration for such additional token types, or to override the scopes used for the predefined tokens from the table above. The keys of this mapping should be the token types and values should be the corresponding scopes. -Semantic tokens with exactly one [token modifier](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#semanticTokenModifiers) can be addressed by appending the modifier after a dot. +Semantic tokens with exactly one [token modifier](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#semanticTokenModifiers) can be addressed by appending the modifier after a dot. -```json +```jsonc { "semantic_tokens": { "magicFunction": "support.function.builtin", @@ -198,7 +201,7 @@ Those scopes can be used to, for example, gray out the text color of unused code For example, to add a custom rule for `Mariana` color scheme, select `UI: Customize Color Scheme` from the Command Palette and add the following rule: -```json +```jsonc { "rules": [ { @@ -221,7 +224,8 @@ The color scheme rule only works if the "background" color is (marginally) diffe | `variable.parameter.sighelp.active.lsp` | Function argument which is currently highlighted in the signature help popup | !!! note - If the color scheme utilizes different (foreground) colors for the scopes of active and non-active parameters, the active parameter will not additionally be rendered with bold and underlined font style. + If there is no special rule for the active parameter in the color scheme, it will be rendered with bold and underlined font style. + But if the color scheme defines a different `"foreground"` color for the active parameter, the style follows the `"font_style"` property from the color scheme rule. ### Annotations diff --git a/docs/src/js/redirect.js b/docs/src/js/redirect.js new file mode 100644 index 000000000..90a59b54a --- /dev/null +++ b/docs/src/js/redirect.js @@ -0,0 +1,4 @@ +// Fore info see issue: https://github.com/sublimelsp/LSP/issues/2284#issuecomment-1606014439 +if (window.location.toString() === "https://lsp.readthedocs.io/en/latest/") { + window.location.href = 'https://lsp.sublimetext.io/' +} diff --git a/docs/src/keyboard_shortcuts.md b/docs/src/keyboard_shortcuts.md index d18ccceb5..81c39bfd7 100644 --- a/docs/src/keyboard_shortcuts.md +++ b/docs/src/keyboard_shortcuts.md @@ -9,7 +9,9 @@ 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` +| Find References | shift f12 | `lsp_symbol_references` (supports optional args: `{"include_declaration": true/false}`) +| Fold | unbound | `lsp_fold` (supports optional args: `{"strict": true/false}` - to configure whether to fold only when the caret is contained within the folded region (`true`), or even when it is anywhere on the starting line (`false`)) +| Fold All | unbound | `lsp_fold_all` (supports optional args: `{"kind": "comment" | "imports" | "region"}`) | Follow Link | unbound | `lsp_open_link` | Format File | unbound | `lsp_format_document` | Format Selection | unbound | `lsp_format_document_range` diff --git a/docs/src/language_servers.md b/docs/src/language_servers.md index dc6f8dc9c..329017bb7 100644 --- a/docs/src/language_servers.md +++ b/docs/src/language_servers.md @@ -39,7 +39,7 @@ Follow installation instructions on [LSP-OmniSharp](https://github.com/sublimels 1. Download [clojure-lsp](https://clojure-lsp.io/installation/). 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"clojure-lsp"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "clojure-lsp": { @@ -63,7 +63,7 @@ Follow installation instructions on [LSP-css](https://github.com/sublimelsp/LSP- 1. Install the [D Language Server](https://github.com/Pure-D/serve-d#installation). 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"serve-d"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "serve-d": { @@ -100,7 +100,7 @@ Follow installation instructions on [LSP-elm](https://github.com/sublimelsp/LSP- 1. Install the [Erlang Language Server](https://github.com/erlang-ls/erlang_ls). 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"erlang-ls"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "erlang-ls": { @@ -124,7 +124,7 @@ Follow installation instructions on [LSP-elm](https://github.com/sublimelsp/LSP- 4. Open `Preferences > Package Settings > LSP > Settings` and add the `"fsautocomplete"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "fsautocomplete": { @@ -144,24 +144,23 @@ Follow installation instructions on [LSP-elm](https://github.com/sublimelsp/LSP- ## Fortran -1. Install the [ Fortran](https://packagecontrol.io/packages/Fortran) package from Package Control for syntax highlighting. -2. Install the [Fortran Language Server](https://github.com/hansec/fortran-language-server#installation). +1. Install the [ModernFortran](https://packagecontrol.io/packages/ModernFortran) or the [Fortran](https://packagecontrol.io/packages/Fortran) package from Package Control for syntax highlighting. +2. Install the [fortls](https://fortls.fortran-lang.org/quickstart.html#download) language server. 3. Open `Preferences > Package Settings > LSP > Settings` and add the `"fortls"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "fortls": { "enabled": true, - "command": ["fortls"], - "selector": "source.modern-fortran | source.fixedform-fortran" + "command": ["fortls", "--notify_init"], + "selector": "source.fortran | source.modern-fortran | source.fixedform-fortran" } } } ``` -!!! info "See available [configuration options](https://github.com/hansec/fortran-language-server#language-server-settings)." - For example set `"command": ["fortls", "--lowercase_intrinsics"]` to use lowercase for autocomplete suggestions. +!!! info "See available [configuration options](https://fortls.fortran-lang.org/options.html)." ## Go @@ -176,7 +175,7 @@ Follow installation instructions on [LSP-gopls](https://github.com/sublimelsp/LS 2. Launch the Godot Editor on the project you are working on and leave it running. 3. Open `Preferences > Package Settings > LSP > Settings` and add the `"godot-lsp"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "godot-lsp": { @@ -189,6 +188,8 @@ Follow installation instructions on [LSP-gopls](https://github.com/sublimelsp/LS } ``` +If you encounter high cpu load or any other issues you can try omitting the [command] line, and ensure the godot editor is running while you work in sublime. + ## GraphQL Follow installation instructions on [LSP-graphql](https://github.com/sublimelsp/LSP-graphql). @@ -198,7 +199,7 @@ Follow installation instructions on [LSP-graphql](https://github.com/sublimelsp/ 1. Install [haskell-language-server](https://github.com/haskell/haskell-language-server). 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"haskell-language-server"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "haskell-language-server": { @@ -241,7 +242,7 @@ Follow installation instructions on [LSP-flow](https://github.com/sublimelsp/LSP 1. Install the [quick-lint-js LSP server](https://quick-lint-js.com/install/cli/) for JavaScript. 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"quick-lint-js"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "quick-lint-js": { @@ -253,7 +254,13 @@ Follow installation instructions on [LSP-flow](https://github.com/sublimelsp/LSP } ``` -### TypeScript +### Rome + +> Rome unifies your development stack by combining the functionality of separate tools. Supports linting and formatting. + +Follow installation instructions on [LSP-rome](https://github.com/sublimelsp/LSP-rome). + +### TypeScript Language Server Follow installation instructions on [LSP-typescript](https://github.com/sublimelsp/LSP-typescript). @@ -271,7 +278,7 @@ Follow installation instructions on [LSP-julia](https://github.com/sublimelsp/LS 2. Install the [Kotlin Language Server](https://github.com/fwcd/KotlinLanguageServer) (requires [building](https://github.com/fwcd/KotlinLanguageServer/blob/master/BUILDING.md) first). 3. Open `Preferences > Package Settings > LSP > Settings` and add the `"kotlinls"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "kotlinls": { @@ -303,7 +310,7 @@ Spell check can be provided by [LSP-ltex-ls](https://github.com/sublimelsp/LSP-l 1. Follow [installation instructions for Digestif](https://github.com/astoff/digestif#installation) to install the server, and make sure it is available in your PATH. 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"digestif"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "digestif": { @@ -317,7 +324,7 @@ Spell check can be provided by [LSP-ltex-ls](https://github.com/sublimelsp/LSP-l 3. To enable auto-completions for the relevant situations in LaTeX files, adjust Sublime's `"auto_complete_selector"` setting (`Preferences > Settings`); for example - ```json + ```jsonc { "auto_complete_selector": "meta.tag, source - comment - string.quoted.double.block - string.quoted.single.block - string.unquoted.heredoc, text.tex constant.other.citation, text.tex constant.other.reference, text.tex support.function, text.tex variable.parameter.function", } @@ -328,7 +335,7 @@ Spell check can be provided by [LSP-ltex-ls](https://github.com/sublimelsp/LSP-l 1. Install [cc-lsp](https://github.com/cxxxr/cl-lsp) using Roswell. 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"cc-lsp"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "cc-lsp": { @@ -360,7 +367,7 @@ Spell check can be provided by [LSP-ltex-ls](https://github.com/LDAP/LSP-ltex-ls 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"markmark"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "markmark": { @@ -380,7 +387,7 @@ Spell check can be provided by [LSP-ltex-ls](https://github.com/LDAP/LSP-ltex-ls 3. Open `Preferences > Package Settings > LSP > Settings` and add the `"reason"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "reason": { @@ -392,6 +399,10 @@ Spell check can be provided by [LSP-ltex-ls](https://github.com/LDAP/LSP-ltex-ls } ``` +## Odin + +Follow installation instructions on [ols](https://github.com/DanielGavin/ols/). + ## PromQL Follow installation instructions on [LSP-promql](https://github.com/prometheus-community/sublimelsp-promql). @@ -409,7 +420,7 @@ Follow installation instructions on [LSP-intelephense](https://github.com/sublim 1. Install [Phpactor globally](https://phpactor.readthedocs.io/en/master/usage/standalone.html#installation-global). 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"phpactor"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "phpactor": { @@ -431,12 +442,22 @@ There are multiple options: ### Pyright +> A full-featured, standards-based static type checker for Python. It is designed for high performance and can be used with large Python source bases. + Follow installation instructions on [LSP-pyright](https://github.com/sublimelsp/LSP-pyright). -### Python LSP Server +### Python LSP Server (pylsp) + +> A [Jedi](https://github.com/davidhalter/jedi)-powered language server that also supports running various linters through built-in plugins. Follow installation instructions on [LSP-pylsp](https://github.com/sublimelsp/LSP-pylsp). +### LSP-ruff + +> An extremely fast Python linter and code transformation tool, written in Rust. + +Follow installation instructions on [LSP-ruff](https://github.com/sublimelsp/LSP-ruff). + ## R Follow installation instructions on [R-IDE](https://github.com/REditorSupport/sublime-ide-r#installation). @@ -451,7 +472,7 @@ There are multiple options: 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"ruby"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "ruby": { @@ -479,7 +500,7 @@ There are multiple options: 2. Open `Preferences > Package Settings > LSP > Settings` and add the `"sorbet"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "sorbet": { @@ -512,7 +533,7 @@ Follow installation instructions on [LSP-metals](https://github.com/scalameta/me ``` 3. Open `Preferences > Package Settings > LSP > Settings` and add the `"diagnostic-ls"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "diagnostic-ls": { @@ -570,7 +591,7 @@ Follow installation instructions on [LSP-metals](https://github.com/scalameta/me 3. Open `Preferences > Package Settings > LSP > Settings` and add the `"steep"` client configuration to the `"clients"`: - ```json + ```jsonc { "clients": { "steep": { @@ -607,6 +628,40 @@ Follow installation instructions on [LSP-tailwindcss](https://github.com/sublime Follow installation instructions on [LSP-terraform](https://github.com/sublimelsp/LSP-terraform). +## TypeScript + +See [Javascript/TypeScript](#javascripttypescript). + +## Typst + +1. Install the [Typst](https://packagecontrol.io/packages/Typst) package from Package Control for syntax highlighting. +2. Download the [typst-lsp](https://github.com/nvarner/typst-lsp/releases) language server executable for your platform. +3. Open `Preferences > Package Settings > LSP > Settings` and add the `"typst-lsp"` client configuration to the `"clients"`: + + ```jsonc + { + "clients": { + "typst-lsp": { + "enabled": true, + "command": ["C:\\path\\to\\typst-lsp-win32-x64.exe"], // adjust this path according to your platform/setup + "selector": "text.typst" + } + } + } + ``` +4. Optional: to enable auto-completions for the relevant situations in Typst files, adjust Sublime's `"auto_complete_selector"` and/or `"auto_complete_triggers"` setting (`Preferences > Settings`); for example + + ```jsonc + { + "auto_complete_triggers": + [ + {"selector": "text.html, text.xml", "characters": "<"}, + {"selector": "punctuation.accessor", "rhs_empty": true}, + {"selector": "text.typst", "characters": "#", "rhs_empty": true}, + ], + } + ``` + ## Vue There are multiple options: @@ -625,7 +680,7 @@ Follow installation instructions on [LSP-volar](https://github.com/sublimelsp/LS 2. Install the [Vala Language Server](https://github.com/Prince781/vala-language-server) 3. Add Vala Langauge Server to LSP settings: - ```json + ```jsonc { "clients": { "vala-language-server": { diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md index e29277fe7..e6ceddfe9 100644 --- a/docs/src/troubleshooting.md +++ b/docs/src/troubleshooting.md @@ -1,12 +1,9 @@ ## Self-help instructions -To get more visibility into the inner-workings of the LSP client and the server and be able to diagnose problems, open `Preferences: LSP Settings` from the Command Palette and set the following options: +To see the LSP server and client communication, run `LSP: Toggle Log Panel` from the Command Palette. Logs are useful to diagnose problems. -| Option | Description | -| ----------------------- | -------------------------------------------------------------------- | -| `log_debug: true` | Show verbose debug messages in the Sublime Text console. | - -Once enabled (no restart necessary), the communication log can be seen by running `LSP: Toggle Log Panel` from the Command Palette. It might be a good idea to restart Sublime Text and reproduce the issue again, so that the logs are clean. +!!! note + It might be a good idea to restart Sublime Text and reproduce the issue again so that the logs are clean. If you believe the issue is with this package, please include the output from the Sublime console in your issue report! @@ -27,7 +24,7 @@ Adjusting `PATH` can differ based on the operating system and the default shell macOS - Depending on your default shell, edit: ~/.profile (bash), ~/.zprofile (zsh) or ~/.config/fish/config.fish (fish). + Depending on your default shell (macOS ships with zsh shell by default), edit: ~/.zprofile (zsh), ~/.profile (bash) or ~/.config/fish/config.fish (fish). Linux @@ -51,7 +48,7 @@ Another solution could be (at least on Linux) to update the server `PATH` using - `` is the server name - `` is the directory needed for the server to behave correctly -```json +```jsonc "": { // ... diff --git a/icons/lsp_logo.svg b/icons/lsp_logo.svg new file mode 100644 index 000000000..9dabbf7b3 --- /dev/null +++ b/icons/lsp_logo.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/messages.json b/messages.json index c62949ad2..2302b1327 100644 --- a/messages.json +++ b/messages.json @@ -22,6 +22,8 @@ "1.22.0": "messages/1.22.0.txt", "1.23.0": "messages/1.23.0.txt", "1.24.0": "messages/1.24.0.txt", + "1.25.0": "messages/1.25.0.txt", + "1.26.0": "messages/1.26.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.25.0.txt b/messages/1.25.0.txt new file mode 100644 index 000000000..5031e8c53 --- /dev/null +++ b/messages/1.25.0.txt @@ -0,0 +1,22 @@ +=> 1.25.0 + +⚠️ To ensure that everything works properly after LSP package is updated, it's strongly recommended to restart +Sublime Text once it finishes updating all packages. ⚠️ + +# New features + +- Add option to show diagnostics as annotations (#1702) (Rafał Chłodnicki) +- Add argument "include_declaration" to "lsp_symbol_references" (#2275) (Magnus Karlsson) + +# Fixes + +- Fix rare KeyError (Janos Wortmann) +- fix "Error rewriting command" warning triggered on startup (#2277) (Rafał Chłodnicki) +- fix crash on checking excluded folders with missing project data (#2276) (Rafał Chłodnicki) +- Fix tagged diagnostics flickering on document changes (#2274) (Rafał Chłodnicki) + +# Improvements + +- Show server crashed dialog on unexpected output in server's stdout (Rafal Chlodnicki) +- Only do a single pass on running code actions on save (#2283) (Rafał Chłodnicki) +- Take font style of sighelp active parameter from color scheme (#2279) (jwortmann) diff --git a/messages/1.26.0.txt b/messages/1.26.0.txt new file mode 100644 index 000000000..3e08d36cb --- /dev/null +++ b/messages/1.26.0.txt @@ -0,0 +1,28 @@ +=> 1.26.0 + +⚠️⚠️⚠️ +To ensure that everything works properly after LSP package is updated, it's strongly recommended +to restart Sublime Text once it finishes updating all packages. +⚠️⚠️⚠️ + +# New features + +- Add support for remote images in hover popups (#2341) (jwortmann) +- Add kind filter for Goto Symbol command (#2330) (jwortmann) +- Handle multiple formatters (#2328) (jwortmann) +- Add support for folding range request (#2304) (jwortmann) +- Add support for multi-range formatting (#2299) (jwortmann) + +# Improvements + +- Handle custom URI schemes in hover text links (#2339) (Raoul Wols) +- Sort and select closest result for Find References in quick panel (#2337) (jwortmann) +- Improve signature help performance (#2329) (jwortmann) +- Align "Expand Selection" fallback behavior with "Goto Definition" and "Find References" (Janos Wortmann) +- support client config with `tcp_port` but without `command` (#2300) (Marek Budík) + +# Fixes + +- check `codeAction/resolve` capability against session buffer (#2343) (1900 TD Lemon) +- Minor visual tweaks to ShowMessageRequest popup (#2340) (Rafał Chłodnicki) +- fix "formatting on save" potentially running on outdated document state (Rafal Chlodnicki) diff --git a/notification.css b/notification.css index 0c6ac9bcc..4d2883489 100644 --- a/notification.css +++ b/notification.css @@ -2,14 +2,15 @@ margin: 0.5rem; padding: 1rem; } -.notification .message { - margin-top: 1rem; - margin-bottom: 3rem; + +.notification h2 { + margin-bottom: 1rem; } .notification .actions a { - text-decoration: none; - padding: 0.5rem; border: 2px solid color(var(--foreground) alpha(0.25)); color: var(--foreground); + margin-top: 3rem; + padding: 0.5rem; + text-decoration: none; } diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 69c7c784d..c55877346 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -253,20 +253,15 @@ def _request_code_actions_async(self) -> None: def _handle_response_async(self, responses: List[CodeActionsByConfigName]) -> None: if self._cancelled: return - document_version = self._task_runner.view.change_count() + view = self._task_runner.view tasks = [] # type: List[Promise] for config_name, code_actions in responses: session = self._task_runner.session_by_name(config_name, 'codeActionProvider') if session: - tasks.extend([session.run_code_action_async(action, progress=False) for action in code_actions]) - Promise.all(tasks).then(lambda _: self._on_code_actions_completed(document_version)) - - def _on_code_actions_completed(self, previous_document_version: int) -> None: - if previous_document_version != self._task_runner.view.change_count(): - # Give on_text_changed_async a chance to trigger. - sublime.set_timeout_async(self._request_code_actions_async) - else: - self._on_complete() + tasks.extend([ + session.run_code_action_async(action, progress=False, view=view) for action in code_actions + ]) + Promise.all(tasks).then(lambda _: self._on_complete()) LspSaveCommand.register_task(CodeActionOnSaveTask) @@ -345,7 +340,7 @@ def run_async() -> None: config_name, action = actions[index] session = self.session_by_name(config_name) if session: - session.run_code_action_async(action, progress=True) \ + session.run_code_action_async(action, progress=True, view=self.view) \ .then(lambda response: self._handle_response_async(config_name, response)) sublime.set_timeout_async(run_async) @@ -409,7 +404,7 @@ def run_async(self, id: int, event: Optional[dict]) -> None: config_name, action = self.actions_cache[id] session = self.session_by_name(config_name) if session: - session.run_code_action_async(action, progress=True) \ + session.run_code_action_async(action, progress=True, view=self.view) \ .then(lambda response: self._handle_response_async(config_name, response)) def _handle_response_async(self, session_name: str, response: Any) -> None: diff --git a/plugin/core/configurations.py b/plugin/core/configurations.py index cc5d55c41..96aecfe2e 100644 --- a/plugin/core/configurations.py +++ b/plugin/core/configurations.py @@ -4,8 +4,11 @@ from .types import ClientConfig from .typing import Generator, List, Optional, Set, Dict, Deque from .workspace import enable_in_project, disable_in_project +from abc import ABCMeta +from abc import abstractmethod from collections import deque from datetime import datetime, timedelta +from weakref import WeakSet import sublime import urllib.parse @@ -14,6 +17,13 @@ RETRY_COUNT_TIMEDELTA = timedelta(minutes=3) +class WindowConfigChangeListener(metaclass=ABCMeta): + + @abstractmethod + def on_configs_changed(self, config_name: Optional[str] = None) -> None: + raise NotImplementedError() + + class WindowConfigManager(object): def __init__(self, window: sublime.Window, global_configs: Dict[str, ClientConfig]) -> None: self._window = window @@ -21,7 +31,11 @@ def __init__(self, window: sublime.Window, global_configs: Dict[str, ClientConfi self._disabled_for_session = set() # type: Set[str] self._crashes = {} # type: Dict[str, Deque[datetime]] self.all = {} # type: Dict[str, ClientConfig] - self.update() + self._change_listeners = WeakSet() # type: WeakSet[WindowConfigChangeListener] + self._reload_configs(notify_listeners=False) + + def add_change_listener(self, listener: WindowConfigChangeListener) -> None: + self._change_listeners.add(listener) def get_configs(self) -> List[ClientConfig]: return sorted(self.all.values(), key=lambda config: config.name) @@ -45,6 +59,9 @@ def match_view(self, view: sublime.View, include_disabled: bool = False) -> Gene pass def update(self, updated_config_name: Optional[str] = None) -> None: + self._reload_configs(updated_config_name, notify_listeners=True) + + def _reload_configs(self, updated_config_name: Optional[str] = None, notify_listeners: bool = False) -> None: project_settings = (self._window.project_data() or {}).get("settings", {}).get("LSP", {}) if updated_config_name is None: self.all.clear() @@ -67,7 +84,9 @@ def update(self, updated_config_name: Optional[str] = None) -> None: self.all[name] = ClientConfig.from_dict(name, c) except Exception as ex: exception_log("failed to load project-only configuration {}".format(name), ex) - self._window.run_command("lsp_recheck_sessions", {'config_name': updated_config_name}) + if notify_listeners: + for listener in self._change_listeners: + listener.on_configs_changed(updated_config_name) def enable_config(self, config_name: str) -> None: if not self._reenable_disabled_for_session(config_name): @@ -76,7 +95,7 @@ def enable_config(self, config_name: str) -> None: def disable_config(self, config_name: str, only_for_session: bool = False) -> None: if only_for_session: - self._disable_for_session(config_name) + self._disabled_for_session.add(config_name) else: disable_in_project(self._window, config_name) self.update(config_name) @@ -97,9 +116,6 @@ def record_crash(self, config_name: str, exit_code: int, exception: Optional[Exc 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) - def _reenable_disabled_for_session(self, config_name: str) -> bool: try: self._disabled_for_session.remove(config_name) diff --git a/plugin/core/css.py b/plugin/core/css.py index 9a0c87214..737f05d9a 100644 --- a/plugin/core/css.py +++ b/plugin/core/css.py @@ -11,6 +11,8 @@ def __init__(self) -> None: self.sheets = sublime.load_resource("Packages/LSP/sheets.css") self.sheets_classname = "lsp_sheet" self.inlay_hints = sublime.load_resource("Packages/LSP/inlay_hints.css") + self.annotations = sublime.load_resource("Packages/LSP/annotations.css") + self.annotations_classname = "lsp_annotation" _css = None # type: Optional[CSS] diff --git a/plugin/core/message_request_handler.py b/plugin/core/message_request_handler.py index 1329860b9..8c6fb1677 100644 --- a/plugin/core/message_request_handler.py +++ b/plugin/core/message_request_handler.py @@ -1,75 +1,59 @@ +from .protocol import MessageType from .protocol import Response +from .protocol import ShowMessageRequestParams from .sessions import Session -from .typing import Any, List, Callable +from .typing import Any, Dict, List from .views import show_lsp_popup +from .views import text2html import sublime +ICONS = { + MessageType.Error: '❗', + MessageType.Warning: '⚠️', + MessageType.Info: 'ℹ️', + MessageType.Log: '📝' +} # type: Dict[MessageType, str] + + class MessageRequestHandler(): - def __init__(self, view: sublime.View, session: Session, request_id: Any, params: dict, source: str) -> None: + def __init__( + self, view: sublime.View, session: Session, request_id: Any, params: ShowMessageRequestParams, source: str + ) -> None: self.session = session self.request_id = request_id self.request_sent = False self.view = view self.actions = params.get("actions", []) - self.titles = list(action.get("title") for action in self.actions) - self.message = params.get('message', '') + self.action_titles = list(action.get("title") for action in self.actions) + self.message = params['message'] self.message_type = params.get('type', 4) self.source = source - def _send_user_choice(self, href: int = -1) -> None: - if not self.request_sent: - self.request_sent = True - self.view.hide_popup() - # when noop; nothing was selected e.g. the user pressed escape - param = None - index = int(href) - if index != -1: - param = self.actions[index] - response = Response(self.request_id, param) - self.session.send_response(response) - def show(self) -> None: - show_notification( + formatted = [] # type: List[str] + formatted.append("

{}

".format(self.source)) + icon = ICONS.get(self.message_type, '') + formatted.append("
{} {}
".format(icon, text2html(self.message))) + if self.action_titles: + buttons = [] # type: List[str] + for idx, title in enumerate(self.action_titles): + buttons.append("{}".format(idx, text2html(title))) + formatted.append("
" + " ".join(buttons) + "
") + show_lsp_popup( self.view, - self.source, - self.message_type, - self.message, - self.titles, - self._send_user_choice, - self._send_user_choice - ) - - -def message_content(source: str, message_type: int, message: str, titles: List[str]) -> str: - formatted = [] - icons = { - 1: '❗', - 2: '⚠️', - 3: 'ℹ️', - 4: '📝' - } - icon = icons.get(message_type, '') - formatted.append("

{}

".format(source)) - formatted.append("

{} {}

".format(icon, message)) + "".join(formatted), + css=sublime.load_resource("Packages/LSP/notification.css"), + wrapper_class='notification', + on_navigate=self._send_user_choice, + on_hide=self._send_user_choice) - buttons = [] - for idx, title in enumerate(titles): - buttons.append("{}".format(idx, title)) - - formatted.append("

" + " ".join(buttons) + "

") - - return "".join(formatted) - - -def show_notification(view: sublime.View, source: str, message_type: int, message: str, titles: List[str], - on_navigate: Callable, on_hide: Callable) -> None: - stylesheet = sublime.load_resource("Packages/LSP/notification.css") - contents = message_content(source, message_type, message, titles) - show_lsp_popup( - view, - contents, - css=stylesheet, - wrapper_class='notification', - on_navigate=on_navigate, - on_hide=on_hide) + def _send_user_choice(self, href: int = -1) -> None: + if self.request_sent: + return + self.request_sent = True + self.view.hide_popup() + index = int(href) + param = self.actions[index] if index != -1 else None + response = Response(self.request_id, param) + self.session.send_response(response) diff --git a/plugin/core/open.py b/plugin/core/open.py index 094b48ee1..84fc03465 100644 --- a/plugin/core/open.py +++ b/plugin/core/open.py @@ -21,36 +21,37 @@ FRAGMENT_PATTERN = re.compile(r'^L?(\d+)(?:,(\d+))?(?:-L?(\d+)(?:,(\d+))?)?') +def lsp_range_from_uri_fragment(fragment: str) -> Optional[Range]: + match = FRAGMENT_PATTERN.match(fragment) + if match: + selection = {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 0}} # type: Range + # Line and column numbers in the fragment are assumed to be 1-based and need to be converted to 0-based + # numbers for the LSP Position structure. + start_line, start_column, end_line, end_column = [max(0, int(g) - 1) if g else None for g in match.groups()] + if start_line: + selection['start']['line'] = start_line + selection['end']['line'] = start_line + if start_column: + selection['start']['character'] = start_column + selection['end']['character'] = start_column + if end_line: + selection['end']['line'] = end_line + selection['end']['character'] = UINT_MAX + if end_column is not None: + selection['end']['character'] = end_column + return selection + return None + + def open_file_uri( window: sublime.Window, uri: DocumentUri, flags: int = 0, group: int = -1 ) -> Promise[Optional[sublime.View]]: - def parse_fragment(fragment: str) -> Optional[Range]: - match = FRAGMENT_PATTERN.match(fragment) - if match: - selection = {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 0}} # type: Range - # Line and column numbers in the fragment are assumed to be 1-based and need to be converted to 0-based - # numbers for the LSP Position structure. - start_line, start_column, end_line, end_column = [max(0, int(g) - 1) if g else None for g in match.groups()] - if start_line: - selection['start']['line'] = start_line - selection['end']['line'] = start_line - if start_column: - selection['start']['character'] = start_column - selection['end']['character'] = start_column - if end_line: - selection['end']['line'] = end_line - selection['end']['character'] = UINT_MAX - if end_column is not None: - selection['end']['character'] = end_column - return selection - return None - decoded_uri = unquote(uri) # decode percent-encoded characters parsed = urlparse(decoded_uri) open_promise = open_file(window, decoded_uri, flags, group) if parsed.fragment: - selection = parse_fragment(parsed.fragment) + selection = lsp_range_from_uri_fragment(parsed.fragment) if selection: return open_promise.then(lambda view: _select_and_center(view, cast(Range, selection))) return open_promise diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 210f5a9c2..5a0748a7f 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -1,5 +1,6 @@ from .typing import Enum, IntEnum, IntFlag, StrEnum from .typing import Any, Dict, Generic, Iterable, List, Literal, Mapping, NotRequired, Optional, TypedDict, TypeVar, Union # noqa: E501 +from functools import total_ordering import sublime INT_MAX = 2**31 - 1 @@ -118,7 +119,7 @@ class LSPErrorCodes(IntEnum): the cancel. """ -class FoldingRangeKind(Enum): +class FoldingRangeKind(StrEnum): """ A set of predefined range kinds. """ Comment = 'comment' """ Folding range for a comment """ @@ -401,12 +402,23 @@ class MarkupKind(Enum): """ Markdown is supported as a content format """ +class InlineCompletionTriggerKind(IntEnum): + """ Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. + + @since 3.18.0 + @proposed """ + Invoked = 0 + """ Completion was triggered explicitly by a user gesture. """ + Automatic = 1 + """ Completion was triggered automatically while editing. """ + + class PositionEncodingKind(Enum): """ A set of predefined position encoding kinds. @since 3.17.0 """ UTF8 = 'utf-8' - """ Character offsets count UTF-8 code units. """ + """ Character offsets count UTF-8 code units (e.g. bytes). """ UTF16 = 'utf-16' """ Character offsets count UTF-16 code units. @@ -415,7 +427,7 @@ class PositionEncodingKind(Enum): UTF32 = 'utf-32' """ Character offsets count UTF-32 code units. - Implementation note: these are the same as Unicode code points, + Implementation note: these are the same as Unicode codepoints, so this `PositionEncodingKind` may also be used for an encoding-agnostic representation of character offsets. """ @@ -1168,10 +1180,10 @@ class TokenFormat(Enum): ShowDocumentParams = TypedDict('ShowDocumentParams', { - # The document uri to show. + # The uri to show. 'uri': 'URI', # Indicates to show the resource in an external program. - # To show for example `https://code.visualstudio.com/` + # To show, for example, `https://code.visualstudio.com/` # in the default WEB browser set `external` to `true`. 'external': NotRequired[bool], # An optional property to indicate whether the editor @@ -1185,7 +1197,7 @@ class TokenFormat(Enum): # file. 'selection': NotRequired['Range'], }) -""" Params to show a document. +""" Params to show a resource in the UI. @since 3.16.0 """ @@ -1671,6 +1683,63 @@ class TokenFormat(Enum): @since 3.17.0 """ +InlineCompletionParams = TypedDict('InlineCompletionParams', { + # Additional information about the context in which inline completions were + # requested. + 'context': 'InlineCompletionContext', + # The text document. + 'textDocument': 'TextDocumentIdentifier', + # The position inside the text document. + 'position': 'Position', + # An optional token that a server can use to report work done progress. + 'workDoneToken': NotRequired['ProgressToken'], +}) +""" A parameter literal used in inline completion requests. + +@since 3.18.0 +@proposed """ + + +InlineCompletionList = TypedDict('InlineCompletionList', { + # The inline completion items + 'items': List['InlineCompletionItem'], +}) +""" Represents a collection of {@link InlineCompletionItem inline completion items} to be presented in the editor. + +@since 3.18.0 +@proposed """ + + +InlineCompletionItem = TypedDict('InlineCompletionItem', { + # The text to replace the range with. Must be set. + 'insertText': Union[str, 'StringValue'], + # A text that is used to decide if this inline completion should be shown. When `falsy` the {@link InlineCompletionItem.insertText} is used. + 'filterText': NotRequired[str], + # The range to replace. Must begin and end on the same line. + 'range': NotRequired['Range'], + # An optional {@link Command} that is executed *after* inserting this completion. + 'command': NotRequired['Command'], +}) +""" An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. + +@since 3.18.0 +@proposed """ + + +InlineCompletionRegistrationOptions = TypedDict('InlineCompletionRegistrationOptions', { + # A document selector to identify the scope of the registration. If set to null + # the document selector provided on the client side will be used. + 'documentSelector': Union['DocumentSelector', None], + # The id used to register the request. The id can be used to deregister + # the request again. See also Registration#id. + 'id': NotRequired[str], +}) +""" Inline completion options used during static or dynamic registration. + +@since 3.18.0 +@proposed """ + + RegistrationParams = TypedDict('RegistrationParams', { 'registrations': List['Registration'], }) @@ -2544,8 +2613,7 @@ class TokenFormat(Enum): # The command this code lens represents. 'command': NotRequired['Command'], # A data entry field that is preserved on a code lens item between - # a {@link CodeLensRequest} and a [CodeLensResolveRequest] - # (#CodeLensResolveRequest) + # a {@link CodeLensRequest} and a {@link CodeLensResolveRequest} 'data': NotRequired['LSPAny'], }) """ A code lens represents a {@link Command command} that should be shown along with @@ -2581,7 +2649,7 @@ class TokenFormat(Enum): # The range this link applies to. 'range': 'Range', # The uri this link points to. If missing a resolve request is sent later. - 'target': NotRequired[str], + 'target': NotRequired['URI'], # The tooltip text when you hover over this link. # # If a tooltip is provided, is will be displayed in a string that includes instructions on how to @@ -2644,10 +2712,31 @@ class TokenFormat(Enum): # A document selector to identify the scope of the registration. If set to null # the document selector provided on the client side will be used. 'documentSelector': Union['DocumentSelector', None], + # Whether the server supports formatting multiple ranges at once. + # + # @since 3.18.0 + # @proposed + 'rangesSupport': NotRequired[bool], }) """ Registration options for a {@link DocumentRangeFormattingRequest}. """ +DocumentRangesFormattingParams = TypedDict('DocumentRangesFormattingParams', { + # The document to format. + 'textDocument': 'TextDocumentIdentifier', + # The ranges to format + 'ranges': List['Range'], + # The format options + 'options': 'FormattingOptions', + # An optional token that a server can use to report work done progress. + 'workDoneToken': NotRequired['ProgressToken'], +}) +""" The parameters of a {@link DocumentRangesFormattingRequest}. + +@since 3.18.0 +@proposed """ + + DocumentOnTypeFormattingParams = TypedDict('DocumentOnTypeFormattingParams', { # The document to format. 'textDocument': 'TextDocumentIdentifier', @@ -2741,7 +2830,7 @@ class TokenFormat(Enum): # The edits to apply. 'edit': 'WorkspaceEdit', }) -""" The parameters passed via a apply workspace edit request. """ +""" The parameters passed via an apply workspace edit request. """ ApplyWorkspaceEditResult = TypedDict('ApplyWorkspaceEditResult', { @@ -2999,14 +3088,14 @@ class TokenFormat(Enum): offset of b is 3 since `𐐀` is represented using two code units in UTF-16. Since 3.17 clients and servers can agree on a different string encoding representation (e.g. UTF-8). The client announces it's supported encoding -via the client capability [`general.positionEncodings`](#clientCapabilities). +via the client capability [`general.positionEncodings`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#clientCapabilities). The value is an array of position encodings the client supports, with decreasing preference (e.g. the encoding at index `0` is the most preferred one). To stay backwards compatible the only mandatory encoding is UTF-16 represented via the string `utf-16`. The server can pick one of the encodings offered by the client and signals that encoding back to the client via the initialize result's property -[`capabilities.positionEncoding`](#serverCapabilities). If the string value +[`capabilities.positionEncoding`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#serverCapabilities). If the string value `utf-16` is missing from the client's capability `general.positionEncodings` servers can safely assume that the client supports UTF-16. If the server omits the position encoding in its initialize result the encoding defaults @@ -3502,6 +3591,45 @@ class TokenFormat(Enum): @since 3.17.0 """ +InlineCompletionContext = TypedDict('InlineCompletionContext', { + # Describes how the inline completion was triggered. + 'triggerKind': 'InlineCompletionTriggerKind', + # Provides information about the currently selected item in the autocomplete widget if it is visible. + 'selectedCompletionInfo': NotRequired['SelectedCompletionInfo'], +}) +""" Provides information about the context in which an inline completion was requested. + +@since 3.18.0 +@proposed """ + + +StringValue = TypedDict('StringValue', { + # The kind of string value. + 'kind': Literal['snippet'], + # The snippet string. + 'value': str, +}) +""" A string value used as a snippet is a template which allows to insert text +and to control the editor cursor when insertion happens. + +A snippet can define tab stops and placeholders with `$1`, `$2` +and `${3:foo}`. `$0` defines the final tab stop, it defaults to +the end of the snippet. Variables are defined with `$name` and +`${name:default value}`. + +@since 3.18.0 +@proposed """ + + +InlineCompletionOptions = TypedDict('InlineCompletionOptions', { + 'workDoneProgress': NotRequired[bool], +}) +""" Inline completion options used during static registration. + +@since 3.18.0 +@proposed """ + + Registration = TypedDict('Registration', { # The id used to register the request. The id can be used to deregister # the request again. @@ -3511,7 +3639,7 @@ class TokenFormat(Enum): # Options necessary for the registration. 'registerOptions': NotRequired['LSPAny'], }) -""" General parameters to to register for an notification or to register a provider. """ +""" General parameters to register for a notification or to register a provider. """ Unregistration = TypedDict('Unregistration', { @@ -3635,6 +3763,11 @@ class TokenFormat(Enum): # # @since 3.17.0 'diagnosticProvider': NotRequired[Union['DiagnosticOptions', 'DiagnosticRegistrationOptions']], + # Inline completion options used during static registration. + # + # @since 3.18.0 + # @proposed + 'inlineCompletionProvider': NotRequired[Union[bool, 'InlineCompletionOptions']], # Workspace specific server capabilities. 'workspace': NotRequired['__ServerCapabilities_workspace_Type_1'], # Experimental server capabilities. @@ -3997,6 +4130,11 @@ class TokenFormat(Enum): DocumentRangeFormattingOptions = TypedDict('DocumentRangeFormattingOptions', { + # Whether the server supports formatting multiple ranges at once. + # + # @since 3.18.0 + # @proposed + 'rangesSupport': NotRequired[bool], 'workDoneProgress': NotRequired[bool], }) """ Provider options for a {@link DocumentRangeFormattingRequest}. """ @@ -4203,6 +4341,18 @@ class TokenFormat(Enum): @since 3.17.0 """ +SelectedCompletionInfo = TypedDict('SelectedCompletionInfo', { + # The range that will be replaced if this completion item is accepted. + 'range': 'Range', + # The text the range will be replaced with if this completion is accepted. + 'text': str, +}) +""" Describes the currently selected completion item. + +@since 3.18.0 +@proposed """ + + ClientCapabilities = TypedDict('ClientCapabilities', { # Workspace specific client capabilities. 'workspace': NotRequired['WorkspaceClientCapabilities'], @@ -4535,6 +4685,11 @@ class TokenFormat(Enum): # # @since 3.17.0 'diagnostic': NotRequired['DiagnosticClientCapabilities'], + # Client capabilities specific to inline completions. + # + # @since 3.18.0 + # @proposed + 'inlineCompletion': NotRequired['InlineCompletionClientCapabilities'], }) """ Text document specific client capabilities. """ @@ -5023,6 +5178,11 @@ class TokenFormat(Enum): DocumentRangeFormattingClientCapabilities = TypedDict('DocumentRangeFormattingClientCapabilities', { # Whether range formatting supports dynamic registration. 'dynamicRegistration': NotRequired[bool], + # Whether the client supports formatting multiple ranges at once. + # + # @since 3.18.0 + # @proposed + 'rangesSupport': NotRequired[bool], }) """ Client capabilities of a {@link DocumentRangeFormattingRequest}. """ @@ -5240,6 +5400,16 @@ class TokenFormat(Enum): @since 3.17.0 """ +InlineCompletionClientCapabilities = TypedDict('InlineCompletionClientCapabilities', { + # Whether implementation supports dynamic registration for inline completion providers. + 'dynamicRegistration': NotRequired[bool], +}) +""" Client capabilities specific to inline completions. + +@since 3.18.0 +@proposed """ + + NotebookDocumentSyncClientCapabilities = TypedDict('NotebookDocumentSyncClientCapabilities', { # Whether implementation supports dynamic registration. If this is # set to `true` the client supports the new @@ -5767,7 +5937,7 @@ class TokenFormat(Enum): 'language': str, # A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. 'scheme': NotRequired[str], - # A glob pattern, like `*.{ts,js}`. + # A glob pattern, like **​/*.{ts,js}. See TextDocumentFilter for examples. 'pattern': NotRequired[str], }) @@ -5777,7 +5947,7 @@ class TokenFormat(Enum): 'language': NotRequired[str], # A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. 'scheme': str, - # A glob pattern, like `*.{ts,js}`. + # A glob pattern, like **​/*.{ts,js}. See TextDocumentFilter for examples. 'pattern': NotRequired[str], }) @@ -5787,7 +5957,7 @@ class TokenFormat(Enum): 'language': NotRequired[str], # A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. 'scheme': NotRequired[str], - # A glob pattern, like `*.{ts,js}`. + # A glob pattern, like **​/*.{ts,js}. See TextDocumentFilter for examples. 'pattern': str, }) @@ -5967,6 +6137,10 @@ def prepareRename(cls, params: PrepareRenameParams, view: sublime.View, progress def selectionRange(cls, params: SelectionRangeParams) -> 'Request': return Request('textDocument/selectionRange', params) + @classmethod + def foldingRange(cls, params: FoldingRangeParams, view: sublime.View) -> 'Request': + return Request('textDocument/foldingRange', params, view) + @classmethod def workspaceSymbol(cls, params: WorkspaceSymbolParams) -> 'Request': return Request("workspace/symbol", params, None, progress=True) @@ -6102,6 +6276,7 @@ def to_payload(self) -> Dict[str, Any]: return payload +@total_ordering class Point(object): def __init__(self, row: int, col: int) -> None: self.row = int(row) @@ -6112,9 +6287,14 @@ def __repr__(self) -> str: def __eq__(self, other: object) -> bool: if not isinstance(other, Point): - raise NotImplementedError() + return NotImplemented return self.row == other.row and self.col == other.col + def __lt__(self, other: object) -> bool: + if not isinstance(other, Point): + return NotImplemented + return (self.row, self.col) < (other.row, other.col) + @classmethod def from_lsp(cls, point: 'Position') -> 'Point': return Point(point['line'], point['character']) @@ -6146,12 +6326,6 @@ def to_lsp(self) -> 'Position': 'session_name': str }) -ExperimentalTextDocumentRangeParams = TypedDict('ExperimentalTextDocumentRangeParams', { - 'textDocument': TextDocumentIdentifier, - 'position': Position, - 'range': Range, -}) - CompletionItemDefaults = __CompletionList_itemDefaults_Type_1 CompletionEditRange = __CompletionList_itemDefaults_editRange_Type_1 diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 50eaad0ae..f07491bc5 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -1,7 +1,6 @@ from .protocol import Diagnostic from .protocol import Location from .protocol import LocationLink -from .protocol import Point from .sessions import AbstractViewListener from .sessions import Session from .tree_view import TreeDataProvider @@ -10,7 +9,7 @@ from .views import first_selection_region from .views import get_uri_and_position_from_location from .views import MissingUriError -from .views import point_to_offset +from .views import position_to_offset from .views import uri_from_view from .windows import WindowManager from .windows import WindowRegistry @@ -277,17 +276,6 @@ def run_async() -> None: sublime.set_timeout_async(run_async) -class LspRecheckSessionsCommand(sublime_plugin.WindowCommand): - def run(self, config_name: Optional[str] = None) -> None: - - def run_async() -> None: - wm = windows.lookup(self.window) - if wm: - wm.restart_sessions_async(config_name) - - sublime.set_timeout_async(run_async) - - def navigate_diagnostics(view: sublime.View, point: Optional[int], forward: bool = True) -> None: try: uri = uri_from_view(view) @@ -310,11 +298,11 @@ def navigate_diagnostics(view: sublime.View, point: Optional[int], forward: bool # this view after/before the cursor op_func = operator.gt if forward else operator.lt for diagnostic in diagnostics: - diag_pos = point_to_offset(Point.from_lsp(diagnostic['range']['start']), view) + diag_pos = position_to_offset(diagnostic['range']['start'], view) if op_func(diag_pos, point): break else: - diag_pos = point_to_offset(Point.from_lsp(diagnostics[0]['range']['start']), view) + diag_pos = position_to_offset(diagnostics[0]['range']['start'], view) view.run_command('lsp_selection_set', {'regions': [(diag_pos, diag_pos)]}) view.show_at_center(diag_pos) # We need a small delay before showing the popup to wait for the scrolling animation to finish. Otherwise ST would diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 80f686225..5d661c741 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -37,6 +37,7 @@ from .protocol import ExecuteCommandParams from .protocol import FailureHandlingKind from .protocol import FileEvent +from .protocol import FoldingRangeKind from .protocol import GeneralClientCapabilities from .protocol import InitializeError from .protocol import InitializeParams @@ -51,6 +52,8 @@ from .protocol import Notification from .protocol import PrepareSupportDefaultBehavior from .protocol import PreviousResultId +from .protocol import ProgressParams +from .protocol import ProgressToken from .protocol import PublishDiagnosticsParams from .protocol import RegistrationParams from .protocol import Range @@ -66,6 +69,10 @@ from .protocol import TokenFormat from .protocol import UnregistrationParams from .protocol import WindowClientCapabilities +from .protocol import WorkDoneProgressBegin +from .protocol import WorkDoneProgressCreateParams +from .protocol import WorkDoneProgressEnd +from .protocol import WorkDoneProgressReport from .protocol import WorkspaceClientCapabilities from .protocol import WorkspaceDiagnosticParams from .protocol import WorkspaceDiagnosticReport @@ -91,6 +98,7 @@ from .url import parse_uri from .url import unparse_uri from .version import __version__ +from .views import DiagnosticSeverityData from .views import extract_variables from .views import get_storage_path from .views import get_uri_and_range_from_location @@ -333,7 +341,8 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor "dynamicRegistration": True # exceptional }, "rangeFormatting": { - "dynamicRegistration": True + "dynamicRegistration": True, + "rangesSupport": True }, "declaration": { "dynamicRegistration": True, @@ -398,6 +407,16 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor "selectionRange": { "dynamicRegistration": True }, + "foldingRange": { + "dynamicRegistration": True, + "foldingRangeKind": { + "valueSet": [ + FoldingRangeKind.Comment, + FoldingRangeKind.Imports, + FoldingRangeKind.Region + ] + } + }, "codeLens": { "dynamicRegistration": True }, @@ -539,7 +558,9 @@ def has_capability_async(self, capability_path: str) -> bool: def shutdown_async(self) -> None: ... - def present_diagnostics_async(self, is_view_visible: bool) -> None: + def present_diagnostics_async( + self, is_view_visible: bool, data_per_severity: Dict[Tuple[int, bool], DiagnosticSeverityData] + ) -> None: ... def on_request_started_async(self, request_id: int, request: Request) -> None: @@ -819,7 +840,7 @@ def additional_variables(cls) -> Optional[Dict[str, str]]: def storage_path(cls) -> str: """ The storage path. Use this as your base directory to install server files. Its path is '$DATA/Package Storage'. - You should have an additional subdirectory preferrably the same name as your plugin. For instance: + You should have an additional subdirectory preferably the same name as your plugin. For instance: ```python from LSP.plugin import AbstractPlugin @@ -1218,7 +1239,7 @@ def __init__(self, manager: Manager, logger: Logger, workspace_folders: List[Wor self._workspace_folders = workspace_folders self._session_views = WeakSet() # type: WeakSet[SessionViewProtocol] self._session_buffers = WeakSet() # type: WeakSet[SessionBufferProtocol] - self._progress = {} # type: Dict[str, Optional[WindowProgressReporter]] + self._progress = {} # type: Dict[ProgressToken, Optional[WindowProgressReporter]] self._watcher_impl = get_file_watcher_implementation() self._static_file_watchers = [] # type: List[FileWatcher] self._dynamic_file_watchers = {} # type: Dict[str, List[FileWatcher]] @@ -1545,13 +1566,34 @@ def _template_variables(self) -> Dict[str, str]: variables.update(extra_vars) return variables - def execute_command(self, command: ExecuteCommandParams, progress: bool) -> Promise: + def execute_command( + self, command: ExecuteCommandParams, progress: bool, view: Optional[sublime.View] = None + ) -> Promise: """Run a command from any thread. Your .then() continuations will run in Sublime's worker thread.""" if self._plugin: task = Promise.packaged_task() # type: PackagedTask[None] promise, resolve = task if self._plugin.on_pre_server_command(command, lambda: resolve(None)): return promise + command_name = command['command'] + # Handle VSCode-specific command for triggering AC/sighelp + if command_name == "editor.action.triggerSuggest" and view: + # Triggered from set_timeout as suggestions popup doesn't trigger otherwise. + sublime.set_timeout(lambda: view.run_command("auto_complete")) + return Promise.resolve(None) + if command_name == "editor.action.triggerParameterHints" and view: + + def run_async() -> None: + session_view = self.session_view_for_view_async(view) + if not session_view: + return + listener = session_view.listener() + if not listener: + return + listener.do_signature_help_async(manual=False) + + sublime.set_timeout_async(run_async) + return Promise.resolve(None) # TODO: Our Promise class should be able to handle errors/exceptions return Promise( lambda resolve: self.send_request( @@ -1561,7 +1603,9 @@ def execute_command(self, command: ExecuteCommandParams, progress: bool) -> Prom ) ) - def run_code_action_async(self, code_action: Union[Command, CodeAction], progress: bool) -> Promise: + def run_code_action_async( + self, code_action: Union[Command, CodeAction], progress: bool, view: Optional[sublime.View] = None + ) -> Promise: command = code_action.get("command") if isinstance(command, str): code_action = cast(Command, code_action) @@ -1570,20 +1614,21 @@ def run_code_action_async(self, code_action: Union[Command, CodeAction], progres arguments = code_action.get('arguments', None) if isinstance(arguments, list): command_params['arguments'] = arguments - return self.execute_command(command_params, progress) + return self.execute_command(command_params, progress, view) # At this point it cannot be a command anymore, it has to be a proper code action. # A code action can have an edit and/or command. Note that it can have *both*. In case both are present, we # must apply the edits before running the command. code_action = cast(CodeAction, code_action) - return self._maybe_resolve_code_action(code_action).then(self._apply_code_action_async) + return self._maybe_resolve_code_action(code_action, view) \ + .then(lambda code_action: self._apply_code_action_async(code_action, view)) - def open_uri_async( + def try_open_uri_async( self, uri: DocumentUri, r: Optional[Range] = None, flags: int = 0, group: int = -1 - ) -> Promise[Optional[sublime.View]]: + ) -> Optional[Promise[Optional[sublime.View]]]: if uri.startswith("file:"): return self._open_file_uri_async(uri, r, flags, group) # Try to find a pre-existing session-buffer @@ -1597,7 +1642,17 @@ def open_uri_async( # There is no pre-existing session-buffer, so we have to go through AbstractPlugin.on_open_uri_async. if self._plugin: return self._open_uri_with_plugin_async(self._plugin, uri, r, flags, group) - return Promise.resolve(None) + return None + + def open_uri_async( + self, + uri: DocumentUri, + r: Optional[Range] = None, + flags: int = 0, + group: int = -1 + ) -> Promise[Optional[sublime.View]]: + promise = self.try_open_uri_async(uri, r, flags, group) + return Promise.resolve(None) if promise is None else promise def _open_file_uri_async( self, @@ -1623,7 +1678,7 @@ def _open_uri_with_plugin_async( r: Optional[Range], flags: int, group: int, - ) -> Promise[Optional[sublime.View]]: + ) -> Optional[Promise[Optional[sublime.View]]]: # I cannot type-hint an unpacked tuple pair = Promise.packaged_task() # type: PackagedTask[Tuple[str, str, str]] # It'd be nice to have automatic tuple unpacking continuations @@ -1648,7 +1703,7 @@ def open_scratch_buffer(title: str, content: str, syntax: str) -> None: pair[0].then(lambda tup: sublime.set_timeout(lambda: open_scratch_buffer(*tup))) return result[0] - return Promise.resolve(None) + return None def open_location_async(self, location: Union[Location, LocationLink], flags: int = 0, group: int = -1) -> Promise[Optional[sublime.View]]: @@ -1659,15 +1714,24 @@ def notify_plugin_on_session_buffer_change(self, session_buffer: SessionBufferPr if self._plugin: self._plugin.on_session_buffer_changed_async(session_buffer) - def _maybe_resolve_code_action(self, code_action: CodeAction) -> Promise[Union[CodeAction, Error]]: - if "edit" not in code_action and self.has_capability("codeActionProvider.resolveProvider"): - # TODO: Should we accept a SessionBuffer? What if this capability is registered with a documentSelector? - # We must first resolve the command and edit properties, because they can potentially be absent. - request = Request("codeAction/resolve", code_action) - return self.send_request_task(request) + def _maybe_resolve_code_action( + self, code_action: CodeAction, view: Optional[sublime.View] + ) -> Promise[Union[CodeAction, Error]]: + if "edit" not in code_action: + has_capability = self.has_capability("codeActionProvider.resolveProvider") + if not has_capability and view: + session_view = self.session_view_for_view_async(view) + if session_view: + has_capability = session_view.has_capability_async("codeActionProvider.resolveProvider") + if has_capability: + # We must first resolve the command and edit properties, because they can potentially be absent. + request = Request("codeAction/resolve", code_action) + return self.send_request_task(request) return Promise.resolve(code_action) - def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None]) -> Promise[None]: + def _apply_code_action_async( + self, code_action: Union[CodeAction, Error, None], view: Optional[sublime.View] + ) -> Promise[None]: if not code_action: return Promise.resolve(None) if isinstance(code_action, Error): @@ -1684,7 +1748,8 @@ def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None]) arguments = command.get("arguments") if arguments is not None: execute_command['arguments'] = arguments - return promise.then(lambda _: self.execute_command(execute_command, False)) + return promise.then( + lambda _: self.execute_command(execute_command, progress=False, view=view)) return promise def apply_workspace_edit_async(self, edit: WorkspaceEdit) -> Promise[None]: @@ -1970,7 +2035,7 @@ def success(b: Union[None, bool, sublime.View]) -> None: # TODO: ST API does not allow us to say "do not focus this new view" self.open_uri_async(uri, params.get("selection")).then(success) - def m_window_workDoneProgress_create(self, params: Any, request_id: Any) -> None: + def m_window_workDoneProgress_create(self, params: WorkDoneProgressCreateParams, request_id: Any) -> None: """handles the window/workDoneProgress/create request""" self._progress[params['token']] = None self.send_response(Response(request_id, None)) @@ -1984,7 +2049,7 @@ 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: + def _create_window_progress_reporter(self, token: ProgressToken, value: WorkDoneProgressBegin) -> None: self._progress[token] = WindowProgressReporter( window=self.window, key="lspprogress{}{}".format(self.config.name, token), @@ -1992,60 +2057,65 @@ def _create_window_progress_reporter(self, token: str, value: Dict[str, Any]) -> message=value.get("message") ) - def m___progress(self, params: Any) -> None: + def m___progress(self, params: ProgressParams) -> None: """handles the $/progress notification""" token = params['token'] value = params['value'] # Partial Result Progress # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#partialResults - if token.startswith(_PARTIAL_RESULT_PROGRESS_PREFIX): + if isinstance(token, str) and token.startswith(_PARTIAL_RESULT_PROGRESS_PREFIX): request_id = int(token[len(_PARTIAL_RESULT_PROGRESS_PREFIX):]) request = self._response_handlers[request_id][0] if request.method == "workspace/diagnostic": - self._on_workspace_diagnostics_async(value, reset_pending_response=False) + self._on_workspace_diagnostics_async( + cast(WorkspaceDiagnosticReport, value), reset_pending_response=False) return # Work Done Progress # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workDoneProgress - 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) + if isinstance(value, dict) and 'kind' in 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):]) # type: ignore + request = self._response_handlers[request_id][0] + self._invoke_views(request, "on_request_progress", request_id, params) return - pass - debug('unknown $/progress token: {}'.format(token)) - return - if kind == 'begin': - self._create_window_progress_reporter(token, value) - elif kind == 'report': - progress = self._progress[token] - assert isinstance(progress, WindowProgressReporter) - progress(value.get("message"), value.get("percentage")) - elif kind == 'end': - progress = self._progress.pop(token) - assert isinstance(progress, WindowProgressReporter) - title = progress.title - progress = None - message = value.get('message') - if message: - self.window.status_message(title + ': ' + message) + except (TypeError, 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. + value = cast(WorkDoneProgressBegin, value) + self._create_window_progress_reporter(token, value) + return + debug('unknown $/progress token: {}'.format(token)) + return + if kind == 'begin': + value = cast(WorkDoneProgressBegin, value) + self._create_window_progress_reporter(token, value) + elif kind == 'report': + value = cast(WorkDoneProgressReport, value) + progress = self._progress[token] + assert isinstance(progress, WindowProgressReporter) + progress(value.get("message"), value.get("percentage")) + elif kind == 'end': + value = cast(WorkDoneProgressEnd, value) + progress = self._progress.pop(token) + assert isinstance(progress, WindowProgressReporter) + title = progress.title + progress = None + message = value.get('message') + if message: + self.window.status_message(title + ': ' + message) # --- shutdown dance ----------------------------------------------------------------------------------------------- diff --git a/plugin/core/signature_help.py b/plugin/core/signature_help.py index bd40d6590..23a34105d 100644 --- a/plugin/core/signature_help.py +++ b/plugin/core/signature_help.py @@ -49,8 +49,9 @@ def __init__(self, state: SignatureHelp, language_map: Optional[MarkdownLangMap] self._active_parameter_index = self._state.get("activeParameter") or 0 self._function_color = "white" self._active_parameter_color = "white" + self._active_parameter_bold = True + self._active_parameter_underline = True self._inactive_parameter_color = "white" - self._emphasize_active_parameter = True @classmethod def from_lsp( @@ -73,9 +74,15 @@ def render(self, view: sublime.View) -> str: if self.has_multiple_signatures(): formatted.append(self._render_intro()) self._function_color = view.style_for_scope("entity.name.function.sighelp.lsp")["foreground"] - self._active_parameter_color = view.style_for_scope("variable.parameter.sighelp.active.lsp")["foreground"] + active_parameter_style = view.style_for_scope("variable.parameter.sighelp.active.lsp") + self._active_parameter_color = active_parameter_style["foreground"] self._inactive_parameter_color = view.style_for_scope("variable.parameter.sighelp.lsp")["foreground"] - self._emphasize_active_parameter = self._active_parameter_color == self._inactive_parameter_color + if self._active_parameter_color == self._inactive_parameter_color: + self._active_parameter_bold = True + self._active_parameter_underline = True + else: + self._active_parameter_bold = active_parameter_style.get('bold', False) + self._active_parameter_underline = active_parameter_style.get('underline', False) formatted.extend(self._render_label(signature)) formatted.extend(self._render_docs(view, signature)) return "".join(formatted) @@ -178,16 +185,19 @@ def _signature_documentation(self, view: sublime.View, signature: SignatureInfor return None def _function(self, content: str) -> str: - return _wrap_with_color(content, self._function_color, False) - - def _parameter(self, content: str, emphasize: bool) -> str: - color = self._active_parameter_color if emphasize else self._inactive_parameter_color - return _wrap_with_color(content, color, self._emphasize_active_parameter and emphasize) - - -def _wrap_with_color(content: str, color: str, emphasize: bool) -> str: - return '{}'.format( - color, - '; font-weight: bold; text-decoration: underline' if emphasize else '', - html.escape(content, quote=False) - ) + return _wrap_with_color(content, self._function_color) + + def _parameter(self, content: str, active: bool) -> str: + if active: + return _wrap_with_color( + content, self._active_parameter_color, self._active_parameter_bold, self._active_parameter_underline) + return _wrap_with_color(content, self._inactive_parameter_color) + + +def _wrap_with_color(content: str, color: str, bold: bool = False, underline: bool = False) -> str: + style = 'color: {}'.format(color) + if bold: + style += '; font-weight: bold' + if underline: + style += '; text-decoration: underline' + return '{}'.format(style, html.escape(content, quote=False)) diff --git a/plugin/core/transports.py b/plugin/core/transports.py index 29e69d060..d02f11960 100644 --- a/plugin/core/transports.py +++ b/plugin/core/transports.py @@ -66,13 +66,16 @@ def read_data(self, reader: IO[bytes]) -> Optional[Dict[str, Any]]: try: body = reader.read(int(headers.get("Content-Length"))) except TypeError: - # Expected error on process stopping. Stop the read loop. - raise StopLoopError() + if str(headers) == '\n': + # Expected on process stopping. Gracefully stop the transport. + raise StopLoopError() + else: + # Propagate server's output to the UI. + raise Exception("Unexpected payload in server's stdout:\n\n{}".format(headers)) try: return self._decode(body) except Exception as ex: - exception_log("JSON decode error", ex) - return None + raise Exception("JSON decode error: {}".format(ex)) @staticmethod def _encode(data: Dict[str, Any]) -> bytes: @@ -91,9 +94,9 @@ def _decode(message: bytes) -> Dict[str, Any]: class ProcessTransport(Transport[T]): - def __init__(self, name: str, process: subprocess.Popen, socket: Optional[socket.socket], reader: IO[bytes], - writer: IO[bytes], stderr: Optional[IO[bytes]], processor: AbstractProcessor[T], - callback_object: TransportCallbacks[T]) -> None: + def __init__(self, name: str, process: Optional[subprocess.Popen], socket: Optional[socket.socket], + reader: IO[bytes], writer: IO[bytes], stderr: Optional[IO[bytes]], + processor: AbstractProcessor[T], callback_object: TransportCallbacks[T]) -> None: self._closed = False self._process = process self._socket = socket @@ -103,12 +106,13 @@ def __init__(self, name: str, process: subprocess.Popen, socket: Optional[socket self._processor = processor self._reader_thread = threading.Thread(target=self._read_loop, name='{}-reader'.format(name)) self._writer_thread = threading.Thread(target=self._write_loop, name='{}-writer'.format(name)) - self._stderr_thread = threading.Thread(target=self._stderr_loop, name='{}-stderr'.format(name)) self._callback_object = weakref.ref(callback_object) self._send_queue = Queue(0) # type: Queue[Union[T, None]] self._reader_thread.start() self._writer_thread.start() - self._stderr_thread.start() + if stderr: + self._stderr_thread = threading.Thread(target=self._stderr_loop, name='{}-stderr'.format(name)) + self._stderr_thread.start() def send(self, payload: T) -> None: self._send_queue.put_nowait(payload) @@ -132,9 +136,11 @@ def __del__(self) -> None: self.close() self._join_thread(self._writer_thread) self._join_thread(self._reader_thread) - self._join_thread(self._stderr_thread) + if self._stderr_thread: + self._join_thread(self._stderr_thread) def _read_loop(self) -> None: + exception = None try: while self._reader: payload = self._processor.read_data(self._reader) @@ -152,28 +158,32 @@ def invoke(p: T) -> None: except (AttributeError, BrokenPipeError, StopLoopError): pass except Exception as ex: - exception_log("Unexpected exception", ex) - self._send_queue.put_nowait(None) + exception = ex + if exception: + self._end(exception) + else: + self._send_queue.put_nowait(None) def _end(self, exception: Optional[Exception]) -> None: exit_code = 0 - if not exception: - try: - # Allow the process to stop itself. - exit_code = self._process.wait(1) - except (AttributeError, ProcessLookupError, subprocess.TimeoutExpired): - pass - if self._process.poll() is None: - try: - # The process didn't stop itself. Terminate! - self._process.kill() - # still wait for the process to die, or zombie processes might be the result - # Ignore the exit code in this case, it's going to be something non-zero because we sent SIGKILL. - self._process.wait() - except (AttributeError, ProcessLookupError): - pass - except Exception as ex: - exception = ex # TODO: Old captured exception is overwritten + if self._process: + if not exception: + try: + # Allow the process to stop itself. + exit_code = self._process.wait(1) + except (AttributeError, ProcessLookupError, subprocess.TimeoutExpired): + pass + if self._process.poll() is None: + try: + # The process didn't stop itself. Terminate! + self._process.kill() + # still wait for the process to die, or zombie processes might be the result + # Ignore the exit code in this case, it's going to be something non-zero because we sent SIGKILL. + self._process.wait() + except (AttributeError, ProcessLookupError): + pass + except Exception as ex: + exception = ex # TODO: Old captured exception is overwritten def invoke() -> None: callback_object = self._callback_object() @@ -245,13 +255,12 @@ def start_subprocess() -> subprocess.Popen: if config.listener_socket: assert isinstance(config.tcp_port, int) and config.tcp_port > 0 process, sock, reader, writer = _await_tcp_connection( - config.name, - config.tcp_port, - config.listener_socket, - start_subprocess - ) + config.name, config.tcp_port, config.listener_socket, start_subprocess) else: - process = start_subprocess() + if config.command: + process = start_subprocess() + elif not config.tcp_port: + raise RuntimeError("Failed to provide command or tcp_port, at least one of them has to be configured") if config.tcp_port: sock = _connect_tcp(config.tcp_port) if sock is None: @@ -263,8 +272,9 @@ def start_subprocess() -> subprocess.Popen: writer = process.stdin # type: ignore if not reader or not writer: raise RuntimeError('Failed initializing transport: reader: {}, writer: {}'.format(reader, writer)) + stderr = process.stderr if process else None return ProcessTransport( - config.name, process, sock, reader, writer, process.stderr, json_rpc_processor, callback_object) # type: ignore + config.name, process, sock, reader, writer, stderr, json_rpc_processor, callback_object) # type: ignore _subprocesses = weakref.WeakSet() # type: weakref.WeakSet[subprocess.Popen] diff --git a/plugin/core/types.py b/plugin/core/types.py index 03299b394..79c86f3b0 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -110,7 +110,7 @@ def debounced(f: Callable[[], Any], timeout_ms: int = 0, condition: Callable[[], :param f: The function to possibly run. Its return type is discarded. :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 condition: The condition that must evaluate to True in order to run the function :param async_thread: If true, run the function on the async worker thread, otherwise run the function on the main thread """ @@ -142,7 +142,7 @@ class DebouncerNonThreadSafe: 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. + was chosen during initialization through the `async_thread` argument. """ def __init__(self, async_thread: bool) -> None: @@ -158,7 +158,7 @@ def debounce( :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 condition: The condition that must evaluate to True in order to run the function """ def run(debounce_id: int) -> None: @@ -198,6 +198,7 @@ class Settings: hover_highlight_style = cast(str, None) inhibit_snippet_completions = cast(bool, None) inhibit_word_completions = cast(bool, None) + initially_folded = cast(List[str], None) link_highlight_style = cast(str, None) completion_insert_mode = cast(str, None) log_debug = cast(bool, None) @@ -214,6 +215,7 @@ class Settings: show_code_lens = cast(str, None) show_inlay_hints = cast(bool, None) show_code_actions_in_hover = cast(bool, None) + show_diagnostics_annotations_severity_level = cast(int, None) show_diagnostics_count_in_view_status = cast(bool, None) show_multiline_diagnostics_highlights = cast(bool, None) show_multiline_document_highlights = cast(bool, None) @@ -239,6 +241,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None: r("disabled_capabilities", []) r("document_highlight_style", "underline") r("hover_highlight_style", "") + r("initially_folded", []) r("link_highlight_style", "underline") r("log_debug", False) r("log_max_size", 8 * 1024) @@ -254,6 +257,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None: r("show_code_lens", "annotation") r("show_inlay_hints", False) r("show_code_actions_in_hover", True) + r("show_diagnostics_annotations_severity_level", 0) r("show_diagnostics_count_in_view_status", False) r("show_diagnostics_in_view_status", True) r("show_multiline_diagnostics_highlights", True) diff --git a/plugin/core/version.py b/plugin/core/version.py index c6b887fcb..feeea5da7 100644 --- a/plugin/core/version.py +++ b/plugin/core/version.py @@ -1 +1 @@ -__version__ = (1, 24, 0) +__version__ = (1, 26, 0) diff --git a/plugin/core/views.py b/plugin/core/views.py index 2f2bec8ca..a55d1a239 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -18,7 +18,6 @@ from .protocol import DocumentColorParams from .protocol import DocumentHighlightKind from .protocol import DocumentUri -from .protocol import ExperimentalTextDocumentRangeParams from .protocol import Location from .protocol import LocationLink from .protocol import MarkedString @@ -194,27 +193,6 @@ 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] -# 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", DocumentHighlightKind.Read: "read", @@ -280,6 +258,19 @@ } +class DiagnosticSeverityData: + + __slots__ = ('regions', 'regions_with_tag', 'annotations', 'scope', 'icon') + + def __init__(self, severity: int) -> None: + self.regions = [] # type: List[sublime.Region] + self.regions_with_tag = {} # type: Dict[int, List[sublime.Region]] + self.annotations = [] # type: List[str] + _, _, self.scope, self.icon, _, _ = DIAGNOSTIC_SEVERITY[severity - 1] + if userprefs().diagnostics_gutter_marker != "sign": + self.icon = "" if severity == DiagnosticSeverity.Hint else userprefs().diagnostics_gutter_marker + + class InvalidUriSchemeException(Exception): def __init__(self, uri: str) -> None: self.uri = uri @@ -339,6 +330,10 @@ def position(view: sublime.View, offset: int) -> Position: return offset_to_point(view, offset).to_lsp() +def position_to_offset(position: Position, view: sublime.View) -> int: + return point_to_offset(Point.from_lsp(position), view) + + def get_symbol_kind_from_scope(scope_name: str) -> SublimeKind: best_kind = sublime.KIND_AMBIGUOUS best_kind_score = 0 @@ -351,9 +346,7 @@ def get_symbol_kind_from_scope(scope_name: str) -> SublimeKind: def range_to_region(range: Range, view: sublime.View) -> sublime.Region: - start = Point.from_lsp(range['start']) - end = Point.from_lsp(range['end']) - return sublime.Region(point_to_offset(start, view), point_to_offset(end, view)) + return sublime.Region(position_to_offset(range['start'], view), position_to_offset(range['end'], view)) def region_to_range(view: sublime.View, region: sublime.Region) -> Range: @@ -466,15 +459,6 @@ def text_document_position_params(view: sublime.View, location: int) -> TextDocu return {"textDocument": text_document_identifier(view), "position": position(view, location)} -def text_document_range_params(view: sublime.View, location: int, - region: sublime.Region) -> ExperimentalTextDocumentRangeParams: - return { - "textDocument": text_document_identifier(view), - "position": position(view, location), - "range": region_to_range(view, region) - } - - def did_open_text_document_params(view: sublime.View, language_id: str) -> DidOpenTextDocumentParams: return {"textDocument": text_document_item(view, language_id)} @@ -587,6 +571,14 @@ def text_document_range_formatting(view: sublime.View, region: sublime.Region) - }, view, progress=True) +def text_document_ranges_formatting(view: sublime.View) -> Request: + return Request("textDocument/rangesFormatting", { + "textDocument": text_document_identifier(view), + "options": formatting_options(view.settings()), + "ranges": [region_to_range(view, region) for region in view.sel() if not region.empty()] + }, view, progress=True) + + def selection_range_params(view: sublime.View) -> SelectionRangeParams: return { "textDocument": text_document_identifier(view), @@ -618,9 +610,17 @@ def text_document_code_action_params( LSP_POPUP_SPACER_HTML = '
' -def show_lsp_popup(view: sublime.View, contents: str, location: int = -1, md: bool = False, flags: int = 0, - css: Optional[str] = None, wrapper_class: Optional[str] = None, - on_navigate: Optional[Callable] = None, on_hide: Optional[Callable] = None) -> None: +def show_lsp_popup( + view: sublime.View, + contents: str, + location: int = -1, + md: bool = False, + flags: int = 0, + css: Optional[str] = None, + wrapper_class: Optional[str] = None, + on_navigate: Optional[Callable[..., None]] = None, + on_hide: Optional[Callable[..., None]] = None +) -> None: css = css if css is not None else lsp_css().popups wrapper_class = wrapper_class if wrapper_class is not None else lsp_css().popups_classname contents += LSP_POPUP_SPACER_HTML @@ -869,6 +869,23 @@ def diagnostic_severity(diagnostic: Diagnostic) -> DiagnosticSeverity: return diagnostic.get("severity", DiagnosticSeverity.Error) +def format_diagnostics_for_annotation( + diagnostics: List[Diagnostic], severity: DiagnosticSeverity, view: sublime.View +) -> Tuple[List[str], str]: + css_class = DIAGNOSTIC_SEVERITY[severity - 1][1] + scope = DIAGNOSTIC_SEVERITY[severity - 1][2] + color = view.style_for_scope(scope).get('foreground') or 'red' + annotations = [] + for diagnostic in diagnostics: + message = text2html(diagnostic.get('message') or '') + source = diagnostic.get('source') + line = "[{}] {}".format(text2html(source), message) if source else message + content = '
{3}
'.format( + lsp_css().annotations, lsp_css().annotations_classname, css_class, line) + annotations.append(content) + return (annotations, color) + + def format_diagnostic_for_panel(diagnostic: Diagnostic) -> Tuple[str, Optional[int], Optional[str], Optional[str]]: """ Turn an LSP diagnostic into a string suitable for an output panel. @@ -876,7 +893,7 @@ def format_diagnostic_for_panel(diagnostic: Diagnostic) -> Tuple[str, Optional[i :param diagnostic: The diagnostic :returns: Tuple of (content, optional offset, optional code, optional href) When the last three elements are optional, don't show an inline phantom - When the last three elemenst are not optional, show an inline phantom + When the last three elements are not optional, show an inline phantom using the information given. """ formatted, code, href = diagnostic_source_and_code(diagnostic) diff --git a/plugin/core/windows.py b/plugin/core/windows.py index e797bdd82..5ebebcc36 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -1,5 +1,8 @@ from ...third_party import WebsocketServer # type: ignore -from .configurations import WindowConfigManager, RETRY_MAX_COUNT, RETRY_COUNT_TIMEDELTA +from .configurations import RETRY_COUNT_TIMEDELTA +from .configurations import RETRY_MAX_COUNT +from .configurations import WindowConfigChangeListener +from .configurations import WindowConfigManager from .diagnostics_storage import is_severity_included from .logging import debug from .logging import exception_log @@ -62,7 +65,7 @@ def set_diagnostics_count(view: sublime.View, errors: int, warnings: int) -> Non pass -class WindowManager(Manager): +class WindowManager(Manager, WindowConfigChangeListener): def __init__(self, window: sublime.Window, workspace: ProjectFolders, config_manager: WindowConfigManager) -> None: self._window = window @@ -77,10 +80,13 @@ def __init__(self, window: sublime.Window, workspace: ProjectFolders, config_man self._server_log = [] # type: List[Tuple[str, str]] self.panel_manager = PanelManager(self._window) # type: Optional[PanelManager] self.tree_view_sheets = {} # type: Dict[str, TreeViewSheet] + self.formatters = {} # type: Dict[str, str] + self.suppress_sessions_restart_on_project_update = False self.total_error_count = 0 self.total_warning_count = 0 sublime.set_timeout(functools.partial(self._update_panel_main_thread, _NO_DIAGNOSTICS_PLACEHOLDER, [])) self.panel_manager.ensure_log_panel() + self._config_manager.add_change_listener(self) @property def window(self) -> sublime.Window: @@ -102,6 +108,9 @@ def on_load_project_async(self) -> None: self._config_manager.update() def on_post_save_project_async(self) -> None: + if self.suppress_sessions_restart_on_project_update: + self.suppress_sessions_restart_on_project_update = False + return self.on_load_project_async() def update_workspace_folders_async(self) -> None: @@ -482,6 +491,11 @@ def _update_panel_main_thread(self, characters: str, prephantoms: List[Tuple[int phantoms.append(sublime.Phantom(region, "({})".format(make_link(href, code)), sublime.LAYOUT_INLINE)) self._panel_code_phantoms.update(phantoms) + # --- Implements WindowConfigChangeListener ------------------------------------------------------------------------ + + def on_configs_changed(self, config_name: Optional[str] = None) -> None: + sublime.set_timeout_async(lambda: self.restart_sessions_async(config_name)) + class WindowRegistry: def __init__(self) -> None: diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py index 10897767c..018217060 100644 --- a/plugin/core/workspace.py +++ b/plugin/core/workspace.py @@ -63,7 +63,8 @@ def __init__(self, window: sublime.Window) -> None: self._update_exclude_patterns(self.folders) def _update_exclude_patterns(self, folders: List[str]) -> None: - self._folders_exclude_patterns = [] + # Ensure that the number of patterns matches the number of folders so that accessing by index never throws. + self._folders_exclude_patterns = [[]] * len(folders) project_data = self._window.project_data() if not isinstance(project_data, dict): return @@ -79,7 +80,7 @@ def _update_exclude_patterns(self, folders: List[str]) -> None: else: exclude_patterns.append(sublime_pattern_to_glob('//' + pattern, True, path)) exclude_patterns.append(sublime_pattern_to_glob('//**/' + pattern, True, path)) - self._folders_exclude_patterns.append(exclude_patterns) + self._folders_exclude_patterns[i] = exclude_patterns def update(self) -> bool: new_folders = self._window.folders() diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py new file mode 100644 index 000000000..6c917ff40 --- /dev/null +++ b/plugin/diagnostics.py @@ -0,0 +1,43 @@ +from .core.protocol import Diagnostic +from .core.protocol import DiagnosticSeverity +from .core.settings import userprefs +from .core.typing import List, Tuple +from .core.views import DIAGNOSTIC_KINDS +from .core.views import diagnostic_severity +from .core.views import format_diagnostics_for_annotation +import sublime + + +class DiagnosticsAnnotationsView(): + + def __init__(self, view: sublime.View, config_name: str) -> None: + self._view = view + self._config_name = config_name + + def initialize_region_keys(self) -> None: + r = [sublime.Region(0, 0)] + for severity in DIAGNOSTIC_KINDS.keys(): + self._view.add_regions(self._annotation_region_key(severity), r) + + def _annotation_region_key(self, severity: DiagnosticSeverity) -> str: + return 'lsp_da-{}-{}'.format(severity, self._config_name) + + def draw(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: + flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE + max_severity_level = userprefs().show_diagnostics_annotations_severity_level + # To achieve the correct order of annotations (most severe having priority) we have to add regions from the + # most to the least severe. + for severity in DIAGNOSTIC_KINDS.keys(): + if severity <= max_severity_level: + matching_diagnostics = ([], []) # type: Tuple[List[Diagnostic], List[sublime.Region]] + for diagnostic, region in diagnostics: + if diagnostic_severity(diagnostic) != severity: + continue + matching_diagnostics[0].append(diagnostic) + matching_diagnostics[1].append(region) + annotations, color = format_diagnostics_for_annotation(matching_diagnostics[0], severity, self._view) + self._view.add_regions( + self._annotation_region_key(severity), matching_diagnostics[1], flags=flags, + annotations=annotations, annotation_color=color) + else: + self._view.erase_regions(self._annotation_region_key(severity)) diff --git a/plugin/documents.py b/plugin/documents.py index b63fda6fd..167e4221e 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -10,6 +10,8 @@ from .core.protocol import DocumentHighlightKind from .core.protocol import DocumentHighlightParams from .core.protocol import DocumentUri +from .core.protocol import FoldingRange +from .core.protocol import FoldingRangeParams from .core.protocol import Request from .core.protocol import SignatureHelp from .core.protocol import SignatureHelpContext @@ -41,9 +43,11 @@ from .core.views import MarkdownLangMap from .core.views import range_to_region from .core.views import show_lsp_popup +from .core.views import text_document_identifier from .core.views import text_document_position_params from .core.views import update_lsp_popup from .core.windows import WindowManager +from .folding_range import folding_range_to_range from .hover import code_actions_content from .session_buffer import SessionBuffer from .session_view import SessionView @@ -249,7 +253,7 @@ def diagnostics_intersecting_region_async( for diagnostic, candidate in diagnostics: # Checking against points is inclusive unlike checking whether region intersects another region # which is exclusive (at region end) and we want an inclusive behavior in this case. - if region.contains(candidate.a) or region.contains(candidate.b): + if region.intersects(candidate) or region.contains(candidate.a) or region.contains(candidate.b): covering = covering.cover(candidate) intersections.append(diagnostic) if intersections: @@ -329,6 +333,14 @@ def on_load_async(self) -> None: if not self._registered and is_regular_view(self.view): self._register_async() return + initially_folded_kinds = userprefs().initially_folded + if initially_folded_kinds: + session = self.session_async('foldingRangeProvider') + if session: + params = {'textDocument': text_document_identifier(self.view)} # type: FoldingRangeParams + session.send_request_async( + Request.foldingRange(params, self.view), + partial(self._on_initial_folding_ranges, initially_folded_kinds)) self.on_activated_async() def on_activated_async(self) -> None: @@ -354,7 +366,7 @@ def on_activated_async(self) -> None: sb.do_inlay_hints_async(self.view) def on_selection_modified_async(self) -> None: - first_region, any_different = self._update_stored_selection_async() + first_region, _ = self._update_stored_selection_async() if first_region is None: return if not self._is_in_higlighted_region(first_region.b): @@ -554,16 +566,17 @@ def do_signature_help_async(self, manual: bool) -> None: language_map = session.markdown_language_id_to_st_syntax_map() request = Request.signatureHelp(params, self.view) session.send_request_async(request, lambda resp: self._on_signature_help(resp, pos, language_map)) - elif self.view.match_selector(pos, "meta.function-call.arguments"): - # Don't force close the signature help popup while the user is typing the parameters. - # See also: https://github.com/sublimehq/sublime_text/issues/5518 - pass - else: - # TODO: Refactor popup usage to a common class. We now have sigHelp, completionDocs, hover, and diags - # all using a popup. Most of these systems assume they have exclusive access to a popup, while in - # reality there is only one popup per view. - self.view.hide_popup() - self._sighelp = None + elif self._sighelp: + if self.view.match_selector(pos, "meta.function-call.arguments"): + # Don't force close the signature help popup while the user is typing the parameters. + # See also: https://github.com/sublimehq/sublime_text/issues/5518 + pass + else: + # TODO: Refactor popup usage to a common class. We now have sigHelp, completionDocs, hover, and diags + # all using a popup. Most of these systems assume they have exclusive access to a popup, while in + # reality there is only one popup per view. + self.view.hide_popup() + self._sighelp = None def _get_signature_help_session(self) -> Optional[Session]: # NOTE: We take the beginning of the region to check the previous char (see last_char variable). This is for @@ -693,7 +706,7 @@ def handle_code_action_select(self, config_name: str, actions: List[CodeActionOr def run_async() -> None: session = self.session_by_name(config_name) if session: - session.run_code_action_async(actions[index], progress=True) + session.run_code_action_async(actions[index], progress=True, view=self.view) sublime.set_timeout_async(run_async) @@ -773,6 +786,19 @@ def render_highlights_on_main_thread() -> None: sublime.set_timeout(render_highlights_on_main_thread) + # --- textDocument/foldingRange ------------------------------------------------------------------------------------ + + def _on_initial_folding_ranges(self, kinds: List[str], response: Optional[List[FoldingRange]]) -> None: + if not response: + return + regions = [ + range_to_region(folding_range_to_range(folding_range), self.view) + for kind in kinds + for folding_range in response if kind == folding_range.get('kind') + ] + if regions: + self.view.fold(regions) + # --- Public utility methods --------------------------------------------------------------------------------------- def session_async(self, capability: str, point: Optional[int] = None) -> Optional[Session]: @@ -853,7 +879,7 @@ def _register_async(self) -> None: view_id = view.id() if view_id == self_id: continue - listeners = list(sublime_plugin.view_event_listeners[view_id]) + listeners = list(sublime_plugin.view_event_listeners.get(view_id, [])) for listener in listeners: if isinstance(listener, DocumentSyncListener): debug("also registering", listener) @@ -862,7 +888,7 @@ def _register_async(self) -> None: def _on_view_updated_async(self) -> None: self._code_lenses_debouncer_async.debounce( self._do_code_lenses_async, timeout_ms=self.code_lenses_debounce_time) - first_region, any_different = self._update_stored_selection_async() + first_region, _ = self._update_stored_selection_async() if first_region is None: return self._clear_highlight_regions() diff --git a/plugin/execute_command.py b/plugin/execute_command.py index d44fb9b96..cfad5d132 100644 --- a/plugin/execute_command.py +++ b/plugin/execute_command.py @@ -1,10 +1,13 @@ from .core.protocol import Error from .core.protocol import ExecuteCommandParams from .core.registry import LspTextCommand -from .core.registry import windows from .core.typing import List, Optional, Any from .core.views import first_selection_region -from .core.views import uri_from_view, offset_to_point, region_to_range, text_document_identifier, text_document_position_params # noqa: E501 +from .core.views import offset_to_point +from .core.views import region_to_range +from .core.views import text_document_identifier +from .core.views import text_document_position_params +from .core.views import uri_from_view import sublime @@ -19,18 +22,6 @@ def run(self, command_args: Optional[List[Any]] = None, session_name: Optional[str] = None, event: Optional[dict] = None) -> None: - # Handle VSCode-specific command for triggering AC/sighelp - if command_name == "editor.action.triggerSuggest": - # Triggered from set_timeout as suggestions popup doesn't trigger otherwise. - return sublime.set_timeout(lambda: self.view.run_command("auto_complete")) - if command_name == "editor.action.triggerParameterHints": - - def run_async() -> None: - listener = windows.listener_for_view(self.view) - if listener: - listener.do_signature_help_async(manual=False) - - return sublime.set_timeout_async(run_async) session = self.session_by_name(session_name if session_name else self.session_name) if session and command_name: params = {"command": command_name} # type: ExecuteCommandParams @@ -44,7 +35,7 @@ def handle_response(response: Any) -> None: return self.handle_success_async(response, command_name) - session.execute_command(params, progress=True).then(handle_response) + session.execute_command(params, progress=True, view=self.view).then(handle_response) def handle_success_async(self, result: Any, command_name: str) -> None: """ diff --git a/plugin/folding_range.py b/plugin/folding_range.py new file mode 100644 index 000000000..5ab5b172a --- /dev/null +++ b/plugin/folding_range.py @@ -0,0 +1,207 @@ +from .core.protocol import FoldingRange +from .core.protocol import FoldingRangeKind +from .core.protocol import FoldingRangeParams +from .core.protocol import Range +from .core.protocol import Request +from .core.protocol import UINT_MAX +from .core.registry import LspTextCommand +from .core.typing import List, Optional +from .core.views import range_to_region +from .core.views import text_document_identifier +from functools import partial +import sublime + + +def folding_range_to_range(folding_range: FoldingRange) -> Range: + return { + 'start': { + 'line': folding_range['startLine'], + 'character': folding_range.get('startCharacter', UINT_MAX) + }, + 'end': { + 'line': folding_range['endLine'], + 'character': folding_range.get('endCharacter', UINT_MAX) + } + } + + +def sorted_folding_ranges(folding_ranges: List[FoldingRange]) -> List[FoldingRange]: + # Sort by reversed position and from innermost to outermost (if nested) + return sorted( + folding_ranges, + key=lambda r: ( + r['startLine'], + r.get('startCharacter', UINT_MAX), + -r['endLine'], + -r.get('endCharacter', UINT_MAX) + ), + reverse=True + ) + + +class LspFoldCommand(LspTextCommand): + """A command to fold at the current caret position or at a given point. + + Optional command arguments: + + - `prefetch`: Should usually be `false`, except for the built-in menu items under the "Edit" main menu, which + pre-run a request and cache the response to dynamically show or hide the item. + - `hidden`: Can be used for a hidden menu item with the purpose to run a request and store the response. + - `strict`: Allows to configure the folding behavior; `true` means to fold only when the caret is contained + within the folded region (like ST built-in `fold` command), and `false` will fold a region even if + the caret is anywhere else on the starting line. + - `point`: Can be used instead of the caret position, measured as character offset in the document. + """ + + capability = 'foldingRangeProvider' + folding_ranges = [] # type: List[FoldingRange] + change_count = -1 + folding_region = None # type: Optional[sublime.Region] + + def is_visible( + self, + prefetch: bool = False, + hidden: bool = False, + strict: bool = True, + event: Optional[dict] = None, + point: Optional[int] = None + ) -> bool: + if not prefetch: + return True + # There should be a single empty selection in the view, otherwise this functionality would be misleading + selection = self.view.sel() + if len(selection) != 1 or not selection[0].empty(): + return False + if hidden: # This is our dummy menu item, with the purpose to run the request when the "Edit" menu gets opened + view_change_count = self.view.change_count() + # If the stored change_count matches the view's actual change count, the request has already been run for + # this document state (i.e. "Edit" menu was opened before) and the results are still valid - no need to send + # another request. + if self.change_count == view_change_count: + return False + self.change_count = -1 + session = self.best_session(self.capability) + if session: + params = {'textDocument': text_document_identifier(self.view)} # type: FoldingRangeParams + session.send_request_async( + Request.foldingRange(params, self.view), + partial(self._handle_response_async, view_change_count) + ) + return False + return self.folding_region is not None # Already set or unset by self.description + + def _handle_response_async(self, change_count: int, response: Optional[List[FoldingRange]]) -> None: + self.change_count = change_count + self.folding_ranges = response or [] + + def description( + self, + prefetch: bool = False, + hidden: bool = False, + strict: bool = True, + event: Optional[dict] = None, + point: Optional[int] = None + ) -> str: + if not prefetch: + return "LSP: Fold" + # Implementation detail of Sublime Text: TextCommand.description is called *before* TextCommand.is_visible + self.folding_region = None + if self.change_count != self.view.change_count(): # Ensure that the response has already arrived + return "LSP " # is_visible will return False + if point is not None: + pt = point + else: + selection = self.view.sel() + if len(selection) != 1 or not selection[0].empty(): + return "LSP " # is_visible will return False + pt = selection[0].b + for folding_range in sorted_folding_ranges(self.folding_ranges): + region = range_to_region(folding_range_to_range(folding_range), self.view) + if (strict and region.contains(pt) or + not strict and sublime.Region(self.view.line(region.a).a, region.b).contains(pt)) and \ + not self.view.is_folded(region): + # Store the relevant folding region, so that we don't need to do the same computation again in + # self.is_visible and self.run + self.folding_region = region + kind = folding_range.get('kind') + if kind == FoldingRangeKind.Imports: + return "LSP: Fold Imports" + elif kind: + return "LSP: Fold this {}".format(kind.title()) + else: + return "LSP: Fold" + return "LSP " # is_visible will return False + + def run( + self, + edit: sublime.Edit, + prefetch: bool = False, + hidden: bool = False, + strict: bool = True, + event: Optional[dict] = None, + point: Optional[int] = None + ) -> None: + if prefetch: + if self.folding_region is not None: + self.view.fold(self.folding_region) + else: + if point is not None: + pt = point + else: + selection = self.view.sel() + if len(selection) != 1 or not selection[0].empty(): + self.view.run_command('fold') + return + pt = selection[0].b + session = self.best_session(self.capability) + if session: + params = {'textDocument': text_document_identifier(self.view)} # type: FoldingRangeParams + session.send_request_async( + Request.foldingRange(params, self.view), + partial(self._handle_response_manual_async, pt, strict) + ) + + def _handle_response_manual_async(self, point: int, strict: bool, response: Optional[List[FoldingRange]]) -> None: + if not response: + window = self.view.window() + if window: + window.status_message("Code Folding not available") + return + for folding_range in sorted_folding_ranges(response): + region = range_to_region(folding_range_to_range(folding_range), self.view) + if (strict and region.contains(point) or + not strict and sublime.Region(self.view.line(region.a).a, region.b).contains(point)) and \ + not self.view.is_folded(region): + self.view.fold(region) + return + else: + window = self.view.window() + if window: + window.status_message("Code Folding not available") + + +class LspFoldAllCommand(LspTextCommand): + + capability = 'foldingRangeProvider' + + def run(self, edit: sublime.Edit, kind: Optional[str] = None, event: Optional[dict] = None) -> None: + session = self.best_session(self.capability) + if session: + params = {'textDocument': text_document_identifier(self.view)} # type: FoldingRangeParams + session.send_request_async( + Request.foldingRange(params, self.view), partial(self._handle_response_async, kind)) + + def _handle_response_async(self, kind: Optional[str], response: Optional[List[FoldingRange]]) -> None: + if not response: + return + regions = [ + range_to_region(folding_range_to_range(folding_range), self.view) + for folding_range in response if not kind or kind == folding_range.get('kind') + ] + if not regions: + return + # Don't fold regions which contain the caret or selection + selections = self.view.sel() + regions = [region for region in regions if not any(region.intersects(selection) for selection in selections)] + if regions: + self.view.fold(regions) diff --git a/plugin/formatting.py b/plugin/formatting.py index bd2ba297f..73287e98d 100644 --- a/plugin/formatting.py +++ b/plugin/formatting.py @@ -1,9 +1,11 @@ +from .core.collections import DottedDict from .core.edit import parse_text_edit from .core.promise import Promise from .core.protocol import Error from .core.protocol import TextDocumentSaveReason from .core.protocol import TextEdit from .core.registry import LspTextCommand +from .core.registry import windows from .core.sessions import Session from .core.settings import userprefs from .core.typing import Any, Callable, List, Optional, Iterator, Union @@ -12,16 +14,31 @@ from .core.views import has_single_nonempty_selection from .core.views import text_document_formatting from .core.views import text_document_range_formatting +from .core.views import text_document_ranges_formatting from .core.views import will_save_wait_until from .save_command import LspSaveCommand, SaveTask +from functools import partial import sublime FormatResponse = Union[List[TextEdit], None, Error] -def format_document(text_command: LspTextCommand) -> Promise[FormatResponse]: +def get_formatter(window: Optional[sublime.Window], base_scope: str) -> Optional[str]: + window_manager = windows.lookup(window) + if not window_manager: + return None + project_data = window_manager.window.project_data() + return DottedDict(project_data).get('settings.LSP.formatters.{}'.format(base_scope)) if \ + isinstance(project_data, dict) else window_manager.formatters.get(base_scope) + + +def format_document(text_command: LspTextCommand, formatter: Optional[str] = None) -> Promise[FormatResponse]: view = text_command.view + if formatter: + session = text_command.session_by_name(formatter, LspFormatDocumentCommand.capability) + if session: + return session.send_request_task(text_document_formatting(view)) session = text_command.best_session(LspFormatDocumentCommand.capability) if session: # Either use the documentFormattingProvider ... @@ -83,7 +100,9 @@ def is_applicable(cls, view: sublime.View) -> bool: def run_async(self) -> None: super().run_async() self._purge_changes_async() - format_document(self._task_runner).then(self._on_response) + base_scope = self._task_runner.view.syntax().scope + formatter = get_formatter(self._task_runner.view.window(), base_scope) + format_document(self._task_runner, formatter).then(self._on_response) def _on_response(self, response: FormatResponse) -> None: if response and not isinstance(response, Error) and not self._cancelled: @@ -99,32 +118,86 @@ class LspFormatDocumentCommand(LspTextCommand): capability = 'documentFormattingProvider' - def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) -> bool: + def is_enabled(self, event: Optional[dict] = None, select: bool = False) -> bool: + if select: + return len(list(self.sessions(self.capability))) > 1 return super().is_enabled() or bool(self.best_session(LspFormatDocumentRangeCommand.capability)) - def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None: - format_document(self).then(self.on_result) + def run(self, edit: sublime.Edit, event: Optional[dict] = None, select: bool = False) -> None: + session_names = [session.config.name for session in self.sessions(self.capability)] + base_scope = self.view.syntax().scope + if select: + self.select_formatter(base_scope, session_names) + elif len(session_names) > 1: + formatter = get_formatter(self.view.window(), base_scope) + if formatter: + session = self.session_by_name(formatter, self.capability) + if session: + session.send_request_task(text_document_formatting(self.view)).then(self.on_result) + return + self.select_formatter(base_scope, session_names) + else: + format_document(self).then(self.on_result) def on_result(self, result: FormatResponse) -> None: if result and not isinstance(result, Error): apply_text_edits_to_view(result, self.view) + def select_formatter(self, base_scope: str, session_names: List[str]) -> None: + window = self.view.window() + if not window: + return + window.show_quick_panel( + session_names, partial(self.on_select_formatter, base_scope, session_names), placeholder="Select Formatter") + + def on_select_formatter(self, base_scope: str, session_names: List[str], index: int) -> None: + if index == -1: + return + session_name = session_names[index] + window_manager = windows.lookup(self.view.window()) + if window_manager: + window = window_manager.window + project_data = window.project_data() + if isinstance(project_data, dict): + project_settings = project_data.setdefault('settings', dict()) + project_lsp_settings = project_settings.setdefault('LSP', dict()) + project_formatter_settings = project_lsp_settings.setdefault('formatters', dict()) + project_formatter_settings[base_scope] = session_name + window_manager.suppress_sessions_restart_on_project_update = True + window.set_project_data(project_data) + else: # Save temporarily for this window + window_manager.formatters[base_scope] = session_name + session = self.session_by_name(session_name, self.capability) + if session: + session.send_request_task(text_document_formatting(self.view)).then(self.on_result) + class LspFormatDocumentRangeCommand(LspTextCommand): capability = 'documentRangeFormattingProvider' def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) -> bool: - if super().is_enabled(event, point): - return has_single_nonempty_selection(self.view) + if not super().is_enabled(event, point): + return False + if has_single_nonempty_selection(self.view): + return True + if self.view.has_non_empty_selection_region() and \ + bool(self.best_session('documentRangeFormattingProvider.rangesSupport')): + return True return False def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None: - session = self.best_session(self.capability) - selection = first_selection_region(self.view) - if session and selection is not None: - req = text_document_range_formatting(self.view, selection) - session.send_request(req, lambda response: apply_text_edits_to_view(response, self.view)) + if has_single_nonempty_selection(self.view): + session = self.best_session(self.capability) + selection = first_selection_region(self.view) + if session and selection is not None: + req = text_document_range_formatting(self.view, selection) + session.send_request(req, lambda response: apply_text_edits_to_view(response, self.view)) + elif self.view.has_non_empty_selection_region(): + session = self.best_session('documentRangeFormattingProvider.rangesSupport') + if session: + req = text_document_ranges_formatting(self.view) + session.send_request(req, lambda response: apply_text_edits_to_view(response, self.view)) class LspFormatCommand(LspTextCommand): @@ -139,11 +212,20 @@ def is_visible(self, event: Optional[dict] = None, point: Optional[int] = None) return self.is_enabled(event, point) def description(self, **kwargs) -> str: - return "Format Selection" if self._range_formatting_available() else "Format File" + if self._range_formatting_available(): + if has_single_nonempty_selection(self.view): + return "Format Selection" + return "Format Selections" + return "Format File" def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None: command = 'lsp_format_document_range' if self._range_formatting_available() else 'lsp_format_document' self.view.run_command(command) def _range_formatting_available(self) -> bool: - return has_single_nonempty_selection(self.view) and bool(self.best_session('documentRangeFormattingProvider')) + if has_single_nonempty_selection(self.view) and bool(self.best_session('documentRangeFormattingProvider')): + return True + if self.view.has_non_empty_selection_region() and \ + bool(self.best_session('documentRangeFormattingProvider.rangesSupport')): + return True + return False diff --git a/plugin/goto_diagnostic.py b/plugin/goto_diagnostic.py index 082885fbf..e41d92a69 100644 --- a/plugin/goto_diagnostic.py +++ b/plugin/goto_diagnostic.py @@ -192,7 +192,7 @@ def next_input(self, args: dict) -> Optional[sublime_plugin.CommandInputHandler] if diagnostic is None: self._preview = None return DiagnosticInputHandler(self.window, self.view, uri) - return sublime_plugin.BackInputHandler() # type: ignore + return sublime_plugin.BackInputHandler() def confirm(self, value: Optional[DocumentUri]) -> None: self.uri = value diff --git a/plugin/hover.py b/plugin/hover.py index 22046623a..ee24e34ab 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -1,22 +1,20 @@ from .code_actions import actions_manager from .code_actions import CodeActionOrCommand from .code_actions import CodeActionsByConfigName +from .core.open import lsp_range_from_uri_fragment from .core.open import open_file_uri from .core.open import open_in_browser from .core.promise import Promise from .core.protocol import Diagnostic from .core.protocol import DocumentLink from .core.protocol import Error -from .core.protocol import ExperimentalTextDocumentRangeParams from .core.protocol import Hover from .core.protocol import Position from .core.protocol import Range from .core.protocol import Request -from .core.protocol import TextDocumentPositionParams from .core.registry import LspTextCommand from .core.registry import windows from .core.sessions import AbstractViewListener -from .core.sessions import Session from .core.sessions import SessionBufferProtocol from .core.settings import userprefs from .core.typing import List, Optional, Dict, Tuple, Sequence, Union @@ -35,12 +33,13 @@ from .core.views import range_to_region from .core.views import show_lsp_popup from .core.views import text_document_position_params -from .core.views import text_document_range_params from .core.views import unpack_href_location from .core.views import update_lsp_popup from .session_view import HOVER_HIGHLIGHT_KEY from functools import partial +from urllib.parse import urlparse import html +import mdpopups import sublime @@ -103,6 +102,7 @@ class LspHoverCommand(LspTextCommand): def __init__(self, view: sublime.View) -> None: super().__init__(view) self._base_dir = None # type: Optional[str] + self._image_resolver = None def run( self, @@ -153,22 +153,12 @@ def request_symbol_hover_async(self, listener: AbstractViewListener, point: int) hover_promises = [] # type: List[Promise[ResolvedHover]] language_maps = [] # type: List[Optional[MarkdownLangMap]] for session in listener.sessions_async('hoverProvider'): - document_position = self._create_hover_request(session, point) hover_promises.append(session.send_request_task( - Request("textDocument/hover", document_position, self.view) + Request("textDocument/hover", text_document_position_params(self.view, point), self.view) )) language_maps.append(session.markdown_language_id_to_st_syntax_map()) Promise.all(hover_promises).then(partial(self._on_all_settled, listener, point, language_maps)) - def _create_hover_request( - self, session: Session, point: int - ) -> Union[TextDocumentPositionParams, ExperimentalTextDocumentRangeParams]: - if session.get_capability('experimental.rangeHoverProvider'): - region = first_selection_region(self.view) - if region is not None and region.contains(point): - return text_document_range_params(self.view, point, region) - return text_document_position_params(self.view, point) - def _on_all_settled( self, listener: AbstractViewListener, @@ -325,6 +315,13 @@ def _show_hover(self, listener: AbstractViewListener, point: int, only_diagnosti location=point, on_navigate=lambda href: self._on_navigate(href, point), on_hide=lambda: self.view.erase_regions(HOVER_HIGHLIGHT_KEY)) + self._image_resolver = mdpopups.resolve_images( + contents, mdpopups.worker_thread_resolver, partial(self._on_images_resolved, contents)) + + def _on_images_resolved(self, original_contents: str, contents: str) -> None: + self._image_resolver = None + if contents != original_contents and self.view.is_popup_visible(): + update_lsp_popup(self.view, contents) def _on_navigate(self, href: str, point: int) -> None: if href.startswith("subl:"): @@ -367,6 +364,8 @@ def on_select(targets: List[str], idx: int) -> None: position = {"line": row, "character": col_utf16} # type: Position r = {"start": position, "end": position} # type: Range sublime.set_timeout_async(partial(session.open_uri_async, uri, r)) + elif urlparse(href).scheme.lower() not in ("", "http", "https"): + sublime.set_timeout_async(partial(self.try_open_custom_uri_async, href)) else: open_in_browser(href) @@ -377,6 +376,12 @@ def handle_code_action_select(self, config_name: str, actions: List[CodeActionOr def run_async() -> None: session = self.session_by_name(config_name) if session: - session.run_code_action_async(actions[index], progress=True) + session.run_code_action_async(actions[index], progress=True, view=self.view) sublime.set_timeout_async(run_async) + + def try_open_custom_uri_async(self, href: str) -> None: + r = lsp_range_from_uri_fragment(urlparse(href).fragment) + for session in self.sessions(): + if session.try_open_uri_async(href, r) is not None: + return diff --git a/plugin/inlay_hint.py b/plugin/inlay_hint.py index 0cfe58837..7eecf46a0 100644 --- a/plugin/inlay_hint.py +++ b/plugin/inlay_hint.py @@ -2,14 +2,13 @@ from .core.protocol import InlayHint from .core.protocol import InlayHintLabelPart from .core.protocol import MarkupContent -from .core.protocol import Point from .core.protocol import Request from .core.registry import LspTextCommand from .core.registry import LspWindowCommand from .core.sessions import Session from .core.settings import userprefs from .core.typing import cast, Optional, Union -from .core.views import point_to_offset +from .core.views import position_to_offset from .formatting import apply_text_edits_to_view import html import sublime @@ -88,7 +87,7 @@ def handle_label_part_command(self, session_name: str, label_part: Optional[Inla def inlay_hint_to_phantom(view: sublime.View, inlay_hint: InlayHint, session: Session) -> sublime.Phantom: position = inlay_hint["position"] - region = sublime.Region(point_to_offset(Point.from_lsp(position), view)) + region = sublime.Region(position_to_offset(position, view)) phantom_uuid = str(uuid.uuid4()) content = get_inlay_hint_html(view, inlay_hint, session, phantom_uuid) p = sublime.Phantom(region, content, sublime.LAYOUT_INLINE) diff --git a/plugin/locationpicker.py b/plugin/locationpicker.py index 0ee78fd32..16bac6662 100644 --- a/plugin/locationpicker.py +++ b/plugin/locationpicker.py @@ -67,7 +67,8 @@ def __init__( force_group: bool = True, group: int = -1, placeholder: str = "", - kind: SublimeKind = sublime.KIND_AMBIGUOUS + kind: SublimeKind = sublime.KIND_AMBIGUOUS, + selected_index: int = -1 ) -> None: self._view = view self._view_states = ([r.to_tuple() for r in view.sel()], view.viewport_position()) @@ -92,8 +93,9 @@ def __init__( for location in locations ], on_select=self._select_entry, - on_highlight=self._highlight_entry, flags=sublime.KEEP_OPEN_ON_FOCUS_LOST, + selected_index=selected_index, + on_highlight=self._highlight_entry, placeholder=placeholder ) diff --git a/plugin/references.py b/plugin/references.py index 7c84fbd02..35961378b 100644 --- a/plugin/references.py +++ b/plugin/references.py @@ -11,6 +11,7 @@ from .core.views import get_line from .core.views import get_symbol_kind_from_scope from .core.views import get_uri_and_position_from_location +from .core.views import position_to_offset from .core.views import text_document_position_params from .locationpicker import LocationPicker import functools @@ -30,7 +31,8 @@ def is_enabled( side_by_side: bool = False, force_group: bool = True, fallback: bool = False, - group: int = -1 + group: int = -1, + include_declaration: bool = False ) -> bool: return fallback or super().is_enabled(event, point) @@ -41,7 +43,8 @@ def is_visible( side_by_side: bool = False, force_group: bool = True, fallback: bool = False, - group: int = -1 + group: int = -1, + include_declaration: bool = False ) -> bool: if self.applies_to_context_menu(event): return self.is_enabled(event, point, side_by_side, fallback) @@ -55,7 +58,8 @@ def run( side_by_side: bool = False, force_group: bool = True, fallback: bool = False, - group: int = -1 + group: int = -1, + include_declaration: bool = False ) -> None: session = self.best_session(self.capability) file_path = self.view.file_name() @@ -65,7 +69,9 @@ def run( params = { 'textDocument': position_params['textDocument'], 'position': position_params['position'], - 'context': {"includeDeclaration": False}, + 'context': { + "includeDeclaration": include_declaration, + }, } request = Request("textDocument/references", params, self.view, progress=True) word_range = self.view.word(pos) @@ -138,10 +144,22 @@ def _show_references_in_quick_panel( group: int, position: int ) -> None: - self.view.run_command("add_jump_record", {"selection": [(r.a, r.b) for r in self.view.sel()]}) + selection = self.view.sel() + self.view.run_command("add_jump_record", {"selection": [(r.a, r.b) for r in selection]}) placeholder = "References to " + word kind = get_symbol_kind_from_scope(self.view.scope_name(position)) - LocationPicker(self.view, session, locations, side_by_side, force_group, group, placeholder, kind) + index = 0 + locations.sort(key=lambda l: (l['uri'], Point.from_lsp(l['range']['start']))) + if len(selection): + pt = selection[0].b + view_filename = self.view.file_name() + for idx, location in enumerate(locations): + if view_filename != session.config.map_server_uri_to_client_path(location['uri']): + continue + index = idx + if position_to_offset(location['range']['start'], self.view) > pt: + break + LocationPicker(self.view, session, locations, side_by_side, force_group, group, placeholder, kind, index) def _show_references_in_output_panel(self, word: str, session: Session, locations: List[Location]) -> None: wm = windows.lookup(session.window) diff --git a/plugin/save_command.py b/plugin/save_command.py index 170712df8..9cc0c52ff 100644 --- a/plugin/save_command.py +++ b/plugin/save_command.py @@ -105,7 +105,9 @@ def _run_next_task_async(self) -> None: def _on_task_completed_async(self) -> None: self._pending_tasks.pop(0) if self._pending_tasks: - self._run_next_task_async() + # Even though we are on the async thread already, we want to give ST a chance to notify us about + # potential document changes. + sublime.set_timeout_async(self._run_next_task_async) else: self._trigger_native_save() diff --git a/plugin/selection_range.py b/plugin/selection_range.py index cd30de558..d318a721b 100644 --- a/plugin/selection_range.py +++ b/plugin/selection_range.py @@ -17,15 +17,15 @@ def __init__(self, view: sublime.View) -> None: self._regions = [] # type: List[sublime.Region] self._change_count = 0 - def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None, fallback: bool = True) -> bool: + def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None, fallback: bool = False) -> bool: return fallback or super().is_enabled(event, point) - def is_visible(self, event: Optional[dict] = None, point: Optional[int] = None, fallback: bool = True) -> bool: + def is_visible(self, event: Optional[dict] = None, point: Optional[int] = None, fallback: bool = False) -> bool: if self.applies_to_context_menu(event): return self.is_enabled(event, point, fallback) return True - def run(self, edit: sublime.Edit, event: Optional[dict] = None, fallback: bool = True) -> None: + def run(self, edit: sublime.Edit, event: Optional[dict] = None, fallback: bool = False) -> None: position = get_position(self.view, event) if position is None: return diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 3a2db3eaa..2012b97c7 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -1,6 +1,5 @@ from .core.protocol import ColorInformation from .core.protocol import Diagnostic -from .core.protocol import DiagnosticSeverity from .core.protocol import DocumentDiagnosticParams from .core.protocol import DocumentDiagnosticReport from .core.protocol import DocumentDiagnosticReportKind @@ -28,8 +27,8 @@ from .core.types import WORKSPACE_DIAGNOSTICS_TIMEOUT from .core.typing import Any, Callable, Iterable, Optional, List, Protocol, Set, Dict, Tuple, TypeGuard, Union from .core.typing import cast -from .core.views import DIAGNOSTIC_SEVERITY from .core.views import diagnostic_severity +from .core.views import DiagnosticSeverityData from .core.views import did_change from .core.views import did_close from .core.views import did_open @@ -79,19 +78,6 @@ def update(self, version: int, changes: Iterable[sublime.TextChange]) -> None: self.changes.extend(changes) -class DiagnosticSeverityData: - - __slots__ = ('regions', 'regions_with_tag', 'annotations', 'scope', 'icon') - - def __init__(self, severity: int) -> None: - self.regions = [] # type: List[sublime.Region] - self.regions_with_tag = {} # type: Dict[int, List[sublime.Region]] - self.annotations = [] # type: List[str] - _, _, self.scope, self.icon, _, _ = DIAGNOSTIC_SEVERITY[severity - 1] - if userprefs().diagnostics_gutter_marker != "sign": - self.icon = "" if severity == DiagnosticSeverity.Hint else userprefs().diagnostics_gutter_marker - - class SemanticTokensData: __slots__ = ( @@ -124,21 +110,20 @@ def __init__(self, session_view: SessionViewProtocol, buffer_id: int, uri: Docum self._session = session_view.session self._session_views = WeakSet() # type: WeakSet[SessionViewProtocol] self._session_views.add(session_view) - self.last_known_uri = uri - self.id = buffer_id - self.pending_changes = None # type: Optional[PendingChanges] + self._last_known_uri = uri + self._id = buffer_id + self._pending_changes = None # type: Optional[PendingChanges] self.diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] - self.data_per_severity = {} # type: Dict[Tuple[int, bool], DiagnosticSeverityData] self.diagnostics_version = -1 self.diagnostics_flags = 0 - self.diagnostics_are_visible = False + self._diagnostics_are_visible = False self.document_diagnostic_needs_refresh = False - self.document_diagnostic_pending_response = None # type: Optional[int] - self.last_text_change_time = 0.0 - self.diagnostics_debouncer_async = DebouncerNonThreadSafe(async_thread=True) - self.workspace_diagnostics_debouncer_async = DebouncerNonThreadSafe(async_thread=True) - self.color_phantoms = sublime.PhantomSet(view, "lsp_color") - self.document_links = [] # type: List[DocumentLink] + self._document_diagnostic_pending_response = None # type: Optional[int] + self._last_text_change_time = 0.0 + self._diagnostics_debouncer_async = DebouncerNonThreadSafe(async_thread=True) + self._workspace_diagnostics_debouncer_async = DebouncerNonThreadSafe(async_thread=True) + self._color_phantoms = sublime.PhantomSet(view, "lsp_color") + self._document_links = [] # type: List[DocumentLink] self.semantic_tokens = SemanticTokensData() self._semantic_region_keys = {} # type: Dict[str, int] self._last_semantic_region_key = 0 @@ -180,7 +165,7 @@ def _check_did_open(self, view: sublime.View) -> None: def _check_did_close(self) -> None: if self.opened and self.should_notify_did_close(): - self.session.send_notification(did_close(uri=self.last_known_uri)) + self.session.send_notification(did_close(uri=self._last_known_uri)) self.opened = False def get_uri(self) -> Optional[DocumentUri]: @@ -219,11 +204,11 @@ def remove_session_view(self, sv: SessionViewProtocol) -> None: def _on_before_destroy(self) -> None: self.remove_all_inlay_hints() if self.has_capability("diagnosticProvider") and self.session.config.diagnostics_mode == "open_files": - self.session.m_textDocument_publishDiagnostics({'uri': self.last_known_uri, 'diagnostics': []}) + self.session.m_textDocument_publishDiagnostics({'uri': self._last_known_uri, 'diagnostics': []}) wm = self.session.manager() if wm: wm.on_diagnostics_updated() - self.color_phantoms.update([]) + self._color_phantoms.update([]) # If the session is exiting then there's no point in sending textDocument/didClose and there's also no point # in unregistering ourselves from the session. if not self.session.exiting: @@ -291,7 +276,7 @@ def should_notify_did_close(self) -> bool: def on_text_changed_async(self, view: sublime.View, change_count: int, changes: Iterable[sublime.TextChange]) -> None: - self.last_text_change_time = time.time() + self._last_text_change_time = time.time() last_change = list(changes)[-1] if last_change.a.pt == 0 and last_change.b.pt == 0 and last_change.str == '' and view.size() != 0: # Issue https://github.com/sublimehq/sublime_text/issues/3323 @@ -300,18 +285,18 @@ def on_text_changed_async(self, view: sublime.View, change_count: int, pass else: purge = False - if self.pending_changes is None: - self.pending_changes = PendingChanges(change_count, changes) + if self._pending_changes is None: + self._pending_changes = PendingChanges(change_count, changes) purge = True - elif self.pending_changes.version < change_count: - self.pending_changes.update(change_count, changes) + elif self._pending_changes.version < change_count: + self._pending_changes.update(change_count, changes) purge = True if purge: debounced(lambda: self.purge_changes_async(view), FEATURES_TIMEOUT, lambda: view.is_valid() and change_count == view.change_count(), async_thread=True) def on_revert_async(self, view: sublime.View) -> None: - self.pending_changes = None # Don't bother with pending changes + self._pending_changes = None # Don't bother with pending changes version = view.change_count() self.session.send_notification(did_change(view, version, None)) sublime.set_timeout_async(lambda: self._on_after_change_async(view, version)) @@ -319,7 +304,7 @@ def on_revert_async(self, view: sublime.View) -> None: on_reload_async = on_revert_async def purge_changes_async(self, view: sublime.View) -> None: - if self.pending_changes is None: + if self._pending_changes is None: return sync_kind = self.text_sync_kind() if sync_kind == TextDocumentSyncKind.None_: @@ -328,15 +313,15 @@ def purge_changes_async(self, view: sublime.View) -> None: changes = None version = view.change_count() else: - changes = self.pending_changes.changes - version = self.pending_changes.version + changes = self._pending_changes.changes + version = self._pending_changes.version try: notification = did_change(view, version, changes) self.session.send_notification(notification) except MissingUriError: return # we're closing finally: - self.pending_changes = None + self._pending_changes = None self.session.notify_plugin_on_session_buffer_change(self) sublime.set_timeout_async(lambda: self._on_after_change_async(view, version)) @@ -349,7 +334,7 @@ def _on_after_change_async(self, view: sublime.View, version: int) -> None: if self.session.config.diagnostics_mode == "workspace" and \ not self.session.workspace_diagnostics_pending_response and \ self.session.has_capability('diagnosticProvider.workspaceDiagnostics'): - self.workspace_diagnostics_debouncer_async.debounce( + self._workspace_diagnostics_debouncer_async.debounce( self.session.do_workspace_diagnostics_async, timeout_ms=WORKSPACE_DIAGNOSTICS_TIMEOUT) self.do_semantic_tokens_async(view) if userprefs().link_highlight_style in ("underline", "none"): @@ -361,19 +346,19 @@ def on_pre_save_async(self, view: sublime.View) -> None: if self.should_notify_will_save(): self.purge_changes_async(view) # TextDocumentSaveReason.Manual - self.session.send_notification(will_save(self.last_known_uri, TextDocumentSaveReason.Manual)) + self.session.send_notification(will_save(self._last_known_uri, TextDocumentSaveReason.Manual)) def on_post_save_async(self, view: sublime.View, new_uri: DocumentUri) -> None: self._is_saving = False - if new_uri != self.last_known_uri: + if new_uri != self._last_known_uri: self._check_did_close() - self.last_known_uri = new_uri + self._last_known_uri = new_uri self._check_did_open(view) else: send_did_save, include_text = self.should_notify_did_save() if send_did_save: self.purge_changes_async(view) - self.session.send_notification(did_save(view, include_text, self.last_known_uri)) + self.session.send_notification(did_save(view, include_text, self._last_known_uri)) if self._has_changed_during_save: self._has_changed_during_save = False self._on_after_change_async(view, view.change_count()) @@ -411,10 +396,10 @@ def _do_color_boxes_async(self, view: sublime.View, version: int) -> None: def _on_color_boxes_async(self, view: sublime.View, response: List[ColorInformation]) -> None: if response is None: # Guard against spec violation from certain language servers - self.color_phantoms.update([]) + self._color_phantoms.update([]) return phantoms = [lsp_color_to_phantom(view, color_info) for color_info in response] - sublime.set_timeout(lambda: self.color_phantoms.update(phantoms)) + sublime.set_timeout(lambda: self._color_phantoms.update(phantoms)) # --- textDocument/documentLink ------------------------------------------------------------------------------------ @@ -426,18 +411,18 @@ def _do_document_link_async(self, view: sublime.View, version: int) -> None: ) def _on_document_link_async(self, view: sublime.View, response: Optional[List[DocumentLink]]) -> None: - self.document_links = response or [] - if self.document_links and userprefs().link_highlight_style == "underline": + self._document_links = response or [] + if self._document_links and userprefs().link_highlight_style == "underline": view.add_regions( "lsp_document_link", - [range_to_region(link["range"], view) for link in self.document_links], + [range_to_region(link["range"], view) for link in self._document_links], scope="markup.underline.link.lsp", flags=DOCUMENT_LINK_FLAGS) else: view.erase_regions("lsp_document_link") def get_document_link_at_point(self, view: sublime.View, point: int) -> Optional[DocumentLink]: - for link in self.document_links: + for link in self._document_links: if range_to_region(link["range"], view).contains(point): return link else: @@ -445,10 +430,10 @@ def get_document_link_at_point(self, view: sublime.View, point: int) -> Optional def update_document_link(self, new_link: DocumentLink) -> None: new_link_range = new_link["range"] - for link in self.document_links: + for link in self._document_links: if link["range"] == new_link_range: - self.document_links.remove(link) - self.document_links.append(new_link) + self._document_links.remove(link) + self._document_links.append(new_link) break # --- textDocument/diagnostic -------------------------------------------------------------------------------------- @@ -457,37 +442,37 @@ def do_document_diagnostic_async(self, view: sublime.View, version: Optional[int mgr = self.session.manager() if not mgr: return - if mgr.should_ignore_diagnostics(self.last_known_uri, self.session.config): + if mgr.should_ignore_diagnostics(self._last_known_uri, self.session.config): return if version is None: version = view.change_count() if self.has_capability("diagnosticProvider"): - if self.document_diagnostic_pending_response: - self.session.cancel_request(self.document_diagnostic_pending_response) + if self._document_diagnostic_pending_response: + self.session.cancel_request(self._document_diagnostic_pending_response) params = {'textDocument': text_document_identifier(view)} # type: DocumentDiagnosticParams identifier = self.get_capability("diagnosticProvider.identifier") if identifier: params['identifier'] = identifier - result_id = self.session.diagnostics_result_ids.get(self.last_known_uri) + result_id = self.session.diagnostics_result_ids.get(self._last_known_uri) if result_id is not None: params['previousResultId'] = result_id - self.document_diagnostic_pending_response = self.session.send_request_async( + self._document_diagnostic_pending_response = self.session.send_request_async( Request.documentDiagnostic(params, view), partial(self._on_document_diagnostic_async, version), partial(self._on_document_diagnostic_error_async, version) ) def _on_document_diagnostic_async(self, version: int, response: DocumentDiagnosticReport) -> None: - self.document_diagnostic_pending_response = None + self._document_diagnostic_pending_response = None self._if_view_unchanged(self._apply_document_diagnostic_async, version)(response) def _apply_document_diagnostic_async( self, view: Optional[sublime.View], response: DocumentDiagnosticReport ) -> None: - self.session.diagnostics_result_ids[self.last_known_uri] = response.get('resultId') + self.session.diagnostics_result_ids[self._last_known_uri] = response.get('resultId') if is_full_document_diagnostic_report(response): self.session.m_textDocument_publishDiagnostics( - {'uri': self.last_known_uri, 'diagnostics': response['items']}) + {'uri': self._last_known_uri, 'diagnostics': response['items']}) for uri, diagnostic_report in response.get('relatedDocuments', {}): sb = self.session.get_session_buffer_for_uri_async(uri) if sb: @@ -495,7 +480,7 @@ def _apply_document_diagnostic_async( None, cast(DocumentDiagnosticReport, diagnostic_report)) def _on_document_diagnostic_error_async(self, version: int, error: ResponseError) -> None: - self.document_diagnostic_pending_response = None + self._document_diagnostic_pending_response = None if error['code'] == LSPErrorCodes.ServerCancelled: data = error.get('data') if is_diagnostic_server_cancellation_data(data) and data['retriggerRequest']: @@ -512,67 +497,53 @@ def set_document_diagnostic_pending_refresh(self, needs_refresh: bool = True) -> def on_diagnostics_async( self, raw_diagnostics: List[Diagnostic], version: Optional[int], visible_session_views: Set[SessionViewProtocol] ) -> None: - data_per_severity = {} # type: Dict[Tuple[int, bool], DiagnosticSeverityData] view = self.some_view() if view is None: return change_count = view.change_count() if version is None: version = change_count - if version == change_count: - diagnostics_version = version - diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] - for diagnostic in raw_diagnostics: - region = range_to_region(diagnostic["range"], view) - severity = diagnostic_severity(diagnostic) - key = (severity, len(view.split_by_newlines(region)) > 1) - data = data_per_severity.get(key) - if data is None: - data = DiagnosticSeverityData(severity) - data_per_severity[key] = data - tags = diagnostic.get('tags', []) - if tags: - for tag in tags: - data.regions_with_tag.setdefault(tag, []).append(region) - else: - data.regions.append(region) - diagnostics.append((diagnostic, region)) - self._publish_diagnostics_to_session_views_async( - diagnostics_version, diagnostics, data_per_severity, visible_session_views) - - def _publish_diagnostics_to_session_views_async( - self, - diagnostics_version: int, - diagnostics: List[Tuple[Diagnostic, sublime.Region]], - data_per_severity: Dict[Tuple[int, bool], DiagnosticSeverityData], - visible_session_views: Set[SessionViewProtocol], - ) -> None: + if version != change_count: + return + diagnostics_version = version + diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] + data_per_severity = {} # type: Dict[Tuple[int, bool], DiagnosticSeverityData] + for diagnostic in raw_diagnostics: + region = range_to_region(diagnostic["range"], view) + severity = diagnostic_severity(diagnostic) + key = (severity, len(view.split_by_newlines(region)) > 1) + data = data_per_severity.get(key) + if data is None: + data = DiagnosticSeverityData(severity) + data_per_severity[key] = data + tags = diagnostic.get('tags', []) + if tags: + for tag in tags: + data.regions_with_tag.setdefault(tag, []).append(region) + else: + data.regions.append(region) + diagnostics.append((diagnostic, region)) def present() -> None: self.diagnostics_version = diagnostics_version self.diagnostics = diagnostics - self.data_per_severity = data_per_severity - self.diagnostics_are_visible = bool(diagnostics) + self._diagnostics_are_visible = bool(diagnostics) for sv in self.session_views: - sv.present_diagnostics_async(sv in visible_session_views) - - self.diagnostics_debouncer_async.cancel_pending() + sv.present_diagnostics_async(sv in visible_session_views, data_per_severity) - if self.diagnostics_are_visible: + self._diagnostics_debouncer_async.cancel_pending() + if self._diagnostics_are_visible: # Old diagnostics are visible. Update immediately. present() else: # There were no diagnostics visible before. Show them a bit later. - delay_in_seconds = userprefs().diagnostics_delay_ms / 1000.0 + self.last_text_change_time - time.time() - view = self.some_view() - if view is None: - return + delay_in_seconds = userprefs().diagnostics_delay_ms / 1000.0 + self._last_text_change_time - time.time() if view.is_auto_complete_visible(): delay_in_seconds += userprefs().diagnostics_additional_delay_auto_complete_ms / 1000.0 if delay_in_seconds <= 0.0: present() else: - self.diagnostics_debouncer_async.debounce( + self._diagnostics_debouncer_async.debounce( present, timeout_ms=int(1000.0 * delay_in_seconds), condition=lambda: bool(view and view.is_valid() and view.change_count() == diagnostics_version), @@ -758,4 +729,4 @@ def remove_all_inlay_hints(self) -> None: # ------------------------------------------------------------------------------------------------------------------ def __str__(self) -> str: - return '{}:{}:{}'.format(self.session.config.name, self.id, self.get_uri()) + return '{}:{}:{}'.format(self.session.config.name, self._id, self.get_uri()) diff --git a/plugin/session_view.py b/plugin/session_view.py index 0aa36ba3c..6a205b3aa 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -12,17 +12,28 @@ from .core.settings import userprefs from .core.typing import Any, Iterable, List, Tuple, Optional, Dict, Generator from .core.views import DIAGNOSTIC_SEVERITY +from .core.views import DiagnosticSeverityData from .core.views import text_document_identifier +from .diagnostics import DiagnosticsAnnotationsView from .session_buffer import SessionBuffer from weakref import ref from weakref import WeakValueDictionary import functools import sublime -DIAGNOSTIC_TAG_VALUES = [v for (k, v) in DiagnosticTag.__dict__.items() if not k.startswith('_')] +DIAGNOSTIC_TAG_VALUES = [v for (k, v) in DiagnosticTag.__dict__.items() if not k.startswith('_')] # type: List[int] HOVER_HIGHLIGHT_KEY = "lsp_hover_highlight" +class TagData: + __slots__ = ('key', 'regions', 'scope') + + def __init__(self, key: str, regions: List[sublime.Region] = [], scope: str = '') -> None: + self.key = key + self.regions = regions + self.scope = scope + + class SessionView: """ Holds state per session per view. @@ -41,6 +52,7 @@ class SessionView: def __init__(self, listener: AbstractViewListener, session: Session, uri: DocumentUri) -> None: self._view = listener.view self._session = session + self._diagnostic_annotations = DiagnosticsAnnotationsView(self._view, session.config.name) self._initialize_region_keys() self._active_requests = {} # type: Dict[int, ActiveRequest] self._listener = ref(listener) @@ -86,6 +98,9 @@ def on_before_remove(self) -> None: self.view.erase_regions("{}_underline".format(self.diagnostics_key(severity, True))) self.view.erase_regions("lsp_document_link") self.session_buffer.remove_session_view(self) + listener = self.listener() + if listener: + listener.on_diagnostics_updated_async(False) @property def session(self) -> Session: @@ -146,6 +161,7 @@ def _initialize_region_keys(self) -> None: self.view.add_regions("lsp_highlight_{}{}".format(kind, mode), r) if hover_highlight_style in ("underline", "stippled"): self.view.add_regions(HOVER_HIGHLIGHT_KEY, r) + self._diagnostic_annotations.initialize_region_keys() def _clear_auto_complete_triggers(self, settings: sublime.Settings) -> None: '''Remove all of our modifications to the view's "auto_complete_triggers"''' @@ -272,31 +288,42 @@ def diagnostics_tag_scope(self, tag: int) -> Optional[str]: return 'markup.{}.lsp'.format(k.lower()) return None - def present_diagnostics_async(self, is_view_visible: bool) -> None: + def present_diagnostics_async( + self, is_view_visible: bool, data_per_severity: Dict[Tuple[int, bool], DiagnosticSeverityData] + ) -> None: flags = userprefs().diagnostics_highlight_style_flags() # for single lines multiline_flags = None if userprefs().show_multiline_diagnostics_highlights else sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE # noqa: E501 level = userprefs().show_diagnostics_severity_level for sev in reversed(range(1, len(DIAGNOSTIC_SEVERITY) + 1)): - self._draw_diagnostics(sev, level, flags[sev - 1] or DIAGNOSTIC_SEVERITY[sev - 1][4], False) - self._draw_diagnostics(sev, level, multiline_flags or DIAGNOSTIC_SEVERITY[sev - 1][5], True) + self._draw_diagnostics( + data_per_severity, sev, level, flags[sev - 1] or DIAGNOSTIC_SEVERITY[sev - 1][4], multiline=False) + self._draw_diagnostics( + data_per_severity, sev, level, multiline_flags or DIAGNOSTIC_SEVERITY[sev - 1][5], multiline=True) + self._diagnostic_annotations.draw(self.session_buffer.diagnostics) listener = self.listener() if listener: listener.on_diagnostics_updated_async(is_view_visible) - def _draw_diagnostics(self, severity: int, max_severity_level: int, flags: int, multiline: bool) -> None: + def _draw_diagnostics( + self, + data_per_severity: Dict[Tuple[int, bool], DiagnosticSeverityData], + severity: int, + max_severity_level: int, + flags: int, + multiline: bool + ) -> None: ICON_FLAGS = sublime.HIDE_ON_MINIMAP | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE key = self.diagnostics_key(severity, multiline) - key_tags = {tag: '{}_tags_{}'.format(key, tag) for tag in DIAGNOSTIC_TAG_VALUES} - for key_tag in key_tags.values(): - self.view.erase_regions(key_tag) - data = self.session_buffer.data_per_severity.get((severity, multiline)) + tags = {tag: TagData('{}_tags_{}'.format(key, tag)) for tag in DIAGNOSTIC_TAG_VALUES} + data = data_per_severity.get((severity, multiline)) if data and severity <= max_severity_level: non_tag_regions = data.regions for tag, regions in data.regions_with_tag.items(): tag_scope = self.diagnostics_tag_scope(tag) # Trick to only add tag regions if there is a corresponding color scheme scope defined. if tag_scope and 'background' in self.view.style_for_scope(tag_scope): - self.view.add_regions(key_tags[tag], regions, tag_scope, flags=sublime.DRAW_NO_OUTLINE) + tags[tag].regions = regions + tags[tag].scope = tag_scope else: non_tag_regions.extend(regions) self.view.add_regions("{}_icon".format(key), non_tag_regions, data.scope, data.icon, ICON_FLAGS) @@ -304,6 +331,11 @@ def _draw_diagnostics(self, severity: int, max_severity_level: int, flags: int, else: self.view.erase_regions("{}_icon".format(key)) self.view.erase_regions("{}_underline".format(key)) + for data in tags.values(): + if data.regions: + self.view.add_regions(data.key, data.regions, data.scope, flags=sublime.DRAW_NO_OUTLINE) + else: + self.view.erase_regions(data.key) def on_request_started_async(self, request_id: int, request: Request) -> None: self._active_requests[request_id] = ActiveRequest(self, request_id, request) diff --git a/plugin/symbols.py b/plugin/symbols.py index a843c4644..2780405eb 100644 --- a/plugin/symbols.py +++ b/plugin/symbols.py @@ -1,14 +1,18 @@ import weakref -from .core.protocol import Request, DocumentSymbol, SymbolInformation, SymbolKind, SymbolTag +from .core.protocol import DocumentSymbol +from .core.protocol import DocumentSymbolParams +from .core.protocol import Request +from .core.protocol import SymbolInformation +from .core.protocol import SymbolKind +from .core.protocol import SymbolTag from .core.registry import LspTextCommand from .core.sessions import print_to_status_bar -from .core.typing import Any, List, Optional, Tuple, Dict, Generator, Union, cast +from .core.typing import Any, List, Optional, Tuple, Dict, Union, cast from .core.views import range_to_region -from .core.views import SUBLIME_KIND_ID_COLOR_SCOPES from .core.views import SublimeKind from .core.views import SYMBOL_KINDS from .core.views import text_document_identifier -from contextlib import contextmanager +from .goto_diagnostic import PreselectedListInputHandler import os import sublime import sublime_plugin @@ -16,15 +20,40 @@ SUPPRESS_INPUT_SETTING_KEY = 'lsp_suppress_input' +SYMBOL_KIND_NAMES = { + SymbolKind.File: "File", + SymbolKind.Module: "Module", + SymbolKind.Namespace: "Namespace", + SymbolKind.Package: "Package", + SymbolKind.Class: "Class", + SymbolKind.Method: "Method", + SymbolKind.Property: "Property", + SymbolKind.Field: "Field", + SymbolKind.Constructor: "Constructor", + SymbolKind.Enum: "Enum", + SymbolKind.Interface: "Interface", + SymbolKind.Function: "Function", + SymbolKind.Variable: "Variable", + SymbolKind.Constant: "Constant", + SymbolKind.String: "String", + SymbolKind.Number: "Number", + SymbolKind.Boolean: "Boolean", + SymbolKind.Array: "Array", + SymbolKind.Object: "Object", + SymbolKind.Key: "Key", + SymbolKind.Null: "Null", + SymbolKind.EnumMember: "Enum Member", + SymbolKind.Struct: "Struct", + SymbolKind.Event: "Event", + SymbolKind.Operator: "Operator", + SymbolKind.TypeParameter: "Type Parameter" +} # type: Dict[SymbolKind, str] + def unpack_lsp_kind(kind: SymbolKind) -> SublimeKind: return SYMBOL_KINDS.get(kind, sublime.KIND_AMBIGUOUS) -def get_symbol_color_scope_from_lsp_kind(kind: SymbolKind) -> str: - return SUBLIME_KIND_ID_COLOR_SCOPES.get(unpack_lsp_kind(kind)[0], "comment") - - def symbol_information_to_quick_panel_item( item: SymbolInformation, show_file_name: bool = True @@ -47,11 +76,36 @@ def symbol_information_to_quick_panel_item( kind=(st_kind, st_icon, st_display_type)) -@contextmanager -def _additional_name(names: List[str], name: str) -> Generator[None, None, None]: - names.append(name) - yield - names.pop(-1) +def symbol_to_list_input_item( + view: sublime.View, item: Union[DocumentSymbol, SymbolInformation], hierarchy: str = '' +) -> sublime.ListInputItem: + name = item['name'] + kind = item['kind'] + st_kind = SYMBOL_KINDS.get(kind, sublime.KIND_AMBIGUOUS) + details = [] + selection_range = item.get('selectionRange') + if selection_range: + item = cast(DocumentSymbol, item) + detail = item.get('detail') + if detail: + details.append(detail) + if hierarchy: + details.append(hierarchy + " > " + name) + region = range_to_region(selection_range, view) + else: + item = cast(SymbolInformation, item) + container_name = item.get('containerName') + if container_name: + details.append(container_name) + region = range_to_region(item['location']['range'], view) + deprecated = SymbolTag.Deprecated in (item.get('tags') or []) or item.get('deprecated', False) + return sublime.ListInputItem( + name, + {'kind': kind, 'region': [region.a, region.b], 'deprecated': deprecated}, + details=" • ".join(details), + annotation=st_kind[2], + kind=st_kind + ) class LspSelectionClearCommand(sublime_plugin.TextCommand): @@ -84,143 +138,171 @@ def run(self, _: sublime.Edit, regions: List[Tuple[int, int]]) -> None: class LspDocumentSymbolsCommand(LspTextCommand): capability = 'documentSymbolProvider' - REGIONS_KEY = 'lsp_document_symbols' def __init__(self, view: sublime.View) -> None: super().__init__(view) - self.old_regions = [] # type: List[sublime.Region] - self.regions = [] # type: List[Tuple[sublime.Region, Optional[sublime.Region], str]] - - def run(self, edit: sublime.Edit, event: Optional[Dict[str, Any]] = None) -> None: - self.view.settings().set(SUPPRESS_INPUT_SETTING_KEY, True) - session = self.best_session(self.capability) - if session: - session.send_request( - Request.documentSymbols({"textDocument": text_document_identifier(self.view)}, self.view), - lambda response: sublime.set_timeout(lambda: self.handle_response(response)), - lambda error: sublime.set_timeout(lambda: self.handle_response_error(error))) - - def handle_response(self, response: Union[List[DocumentSymbol], List[SymbolInformation], None]) -> None: + self.items = [] # type: List[sublime.ListInputItem] + self.kind = 0 + self.cached = False + self.has_matching_symbols = True + + def run( + self, + edit: sublime.Edit, + event: Optional[Dict[str, Any]] = None, + kind: int = 0, + index: Optional[int] = None + ) -> None: + if index is None: + if not self.has_matching_symbols: + self.has_matching_symbols = True + window = self.view.window() + if window: + kind_name = SYMBOL_KIND_NAMES.get(cast(SymbolKind, self.kind)) + window.status_message('No symbols of kind "{}" in this file'.format(kind_name)) + return + self.kind = kind + session = self.best_session(self.capability) + if session: + self.view.settings().set(SUPPRESS_INPUT_SETTING_KEY, True) + params = {"textDocument": text_document_identifier(self.view)} # type: DocumentSymbolParams + session.send_request( + Request.documentSymbols(params, self.view), self.handle_response_async, self.handle_response_error) + + def input(self, args: dict) -> Optional[sublime_plugin.CommandInputHandler]: + if self.cached: + self.cached = False + if self.kind and not any(item.value['kind'] == self.kind for item in self.items): + self.has_matching_symbols = False + return None + window = self.view.window() + if not window: + return None + symbol_kind = cast(SymbolKind, self.kind) + initial_value = sublime.ListInputItem( + SYMBOL_KIND_NAMES.get(symbol_kind, 'All Kinds'), + self.kind, + kind=SYMBOL_KINDS.get(symbol_kind, sublime.KIND_AMBIGUOUS)) + return DocumentSymbolsKindInputHandler(window, initial_value, self.view, self.items) + return None + + def handle_response_async(self, response: Union[List[DocumentSymbol], List[SymbolInformation], None]) -> None: self.view.settings().erase(SUPPRESS_INPUT_SETTING_KEY) - window = self.view.window() - if window and isinstance(response, list) and len(response) > 0: - panel_items = self.process_symbols(response) - self.old_regions = [sublime.Region(r.a, r.b) for r in self.view.sel()] - # Find region that is either intersecting or before to the current selection end. - selected_index = 0 - if self.old_regions: - first_selection = self.old_regions[0] - for i, (r, _, _) in enumerate(self.regions): - if r.begin() <= first_selection.b: - selected_index = i - else: - break - self.view.run_command("lsp_selection_clear") - window.show_quick_panel( - panel_items, - self.on_symbol_selected, - sublime.KEEP_OPEN_ON_FOCUS_LOST, - selected_index, - self.on_highlighted) + self.items.clear() + if response and self.view.is_valid(): + if 'selectionRange' in response[0]: + items = cast(List[DocumentSymbol], response) + for item in items: + self.items.extend(self.process_document_symbol_recursive(item)) + else: + items = cast(List[SymbolInformation], response) + for item in items: + self.items.append(symbol_to_list_input_item(self.view, item)) + self.items.sort(key=lambda item: item.value['region']) + window = self.view.window() + if window: + self.cached = True + window.run_command('show_overlay', {'overlay': 'command_palette', 'command': 'lsp_document_symbols'}) def handle_response_error(self, error: Any) -> None: self.view.settings().erase(SUPPRESS_INPUT_SETTING_KEY) print_to_status_bar(error) - def region(self, index: int) -> sublime.Region: - return self.regions[index][0] - - def selection_region(self, index: int) -> Optional[sublime.Region]: - return self.regions[index][1] - - def scope(self, index: int) -> str: - return self.regions[index][2] + def process_document_symbol_recursive( + self, item: DocumentSymbol, hierarchy: str = '' + ) -> List[sublime.ListInputItem]: + name = item['name'] + name_hierarchy = hierarchy + " > " + name if hierarchy else name + items = [symbol_to_list_input_item(self.view, item, hierarchy)] + for child in item.get('children') or []: + items.extend(self.process_document_symbol_recursive(child, name_hierarchy)) + return items + + +class DocumentSymbolsKindInputHandler(PreselectedListInputHandler): + + def __init__( + self, + window: sublime.Window, + initial_value: sublime.ListInputItem, + view: sublime.View, + items: List[sublime.ListInputItem], + ) -> None: + super().__init__(window, initial_value) + self.view = view + self.items = items + self.old_selection = [sublime.Region(r.a, r.b) for r in view.sel()] + self.last_selected = 0 + + def name(self) -> str: + return 'kind' - def on_symbol_selected(self, index: int) -> None: - if index == -1: - if self.old_regions: - self.view.run_command("lsp_selection_set", {"regions": [(r.a, r.b) for r in self.old_regions]}) - self.view.show_at_center(self.old_regions[0].begin()) - else: - region = self.selection_region(index) - if not region: - self.view.erase_regions(self.REGIONS_KEY) - region = self.region(index) - self.view.run_command("lsp_selection_set", {"regions": [(region.a, region.a)]}) - self.view.show_at_center(region.a) - self.old_regions.clear() - self.regions.clear() - - def on_highlighted(self, index: int) -> None: - region = self.selection_region(index) - if region: - self.view.run_command("lsp_selection_set", {"regions": [region.to_tuple()]}) - else: - region = self.region(index) - self.view.add_regions(self.REGIONS_KEY, [region], self.scope(index), '', sublime.DRAW_NO_FILL) - self.view.show_at_center(region.a) - - def process_symbols( - self, - items: Union[List[DocumentSymbol], List[SymbolInformation]] - ) -> List[sublime.QuickPanelItem]: - self.regions.clear() - panel_items = [] - if 'selectionRange' in items[0]: - items = cast(List[DocumentSymbol], items) - panel_items = self.process_document_symbols(items) + def placeholder(self) -> str: + return "Symbol Kind" + + def get_list_items(self) -> Tuple[List[sublime.ListInputItem], int]: + items = [sublime.ListInputItem('All Kinds', 0, kind=sublime.KIND_AMBIGUOUS)] + items.extend([ + sublime.ListInputItem(SYMBOL_KIND_NAMES[lsp_kind], lsp_kind, kind=st_kind) + for lsp_kind, st_kind in SYMBOL_KINDS.items() + if any(item.value['kind'] == lsp_kind for item in self.items) + ]) + for index, item in enumerate(items): + if item.value == self.last_selected: + break else: - items = cast(List[SymbolInformation], items) - panel_items = self.process_symbol_informations(items) - # Sort both lists in sync according to the range's begin point. - sorted_results = zip(*sorted(zip(self.regions, panel_items), key=lambda item: item[0][0].begin())) - sorted_regions, sorted_panel_items = sorted_results - self.regions = list(sorted_regions) # type: ignore - return list(sorted_panel_items) # type: ignore - - def process_document_symbols(self, items: List[DocumentSymbol]) -> List[sublime.QuickPanelItem]: - quick_panel_items = [] # type: List[sublime.QuickPanelItem] - names = [] # type: List[str] - for item in items: - self.process_document_symbol_recursive(quick_panel_items, item, names) - return quick_panel_items - - def process_document_symbol_recursive(self, quick_panel_items: List[sublime.QuickPanelItem], item: DocumentSymbol, - names: List[str]) -> None: - lsp_kind = item["kind"] - self.regions.append((range_to_region(item['range'], self.view), - range_to_region(item['selectionRange'], self.view), - get_symbol_color_scope_from_lsp_kind(lsp_kind))) - name = item['name'] - with _additional_name(names, name): - st_kind, st_icon, st_display_type = unpack_lsp_kind(lsp_kind) - formatted_names = " > ".join(names) - st_details = item.get("detail") or "" - if st_details: - st_details = "{} | {}".format(st_details, formatted_names) - else: - st_details = formatted_names - tags = item.get("tags") or [] - if SymbolTag.Deprecated in tags: - st_display_type = "⚠ {} - Deprecated".format(st_display_type) - quick_panel_items.append( - sublime.QuickPanelItem( - trigger=name, - details=st_details, - annotation=st_display_type, - kind=(st_kind, st_icon, st_display_type))) - children = item.get('children') or [] # type: List[DocumentSymbol] - for child in children: - self.process_document_symbol_recursive(quick_panel_items, child, names) - - def process_symbol_informations(self, items: List[SymbolInformation]) -> List[sublime.QuickPanelItem]: - quick_panel_items = [] # type: List[sublime.QuickPanelItem] - for item in items: - self.regions.append((range_to_region(item['location']['range'], self.view), - None, get_symbol_color_scope_from_lsp_kind(item['kind']))) - quick_panel_item = symbol_information_to_quick_panel_item(item, show_file_name=False) - quick_panel_items.append(quick_panel_item) - return quick_panel_items + index = 0 + return items, index + + def confirm(self, text: int) -> None: + self.last_selected = text + + def next_input(self, args: dict) -> Optional[sublime_plugin.CommandInputHandler]: + kind = args.get('kind') + if kind is not None: + return DocumentSymbolsInputHandler(self.view, kind, self.items, self.old_selection) + + +class DocumentSymbolsInputHandler(sublime_plugin.ListInputHandler): + + def __init__( + self, view: sublime.View, kind: int, items: List[sublime.ListInputItem], old_selection: List[sublime.Region] + ) -> None: + super().__init__() + self.view = view + self.kind = kind + self.items = items + self.old_selection = old_selection + + def name(self) -> str: + return 'index' + + def list_items(self) -> Tuple[List[sublime.ListInputItem], int]: + items = [item for item in self.items if not self.kind or item.value['kind'] == self.kind] + selected_index = 0 + if self.old_selection: + pt = self.old_selection[0].b + for index, item in enumerate(items): + if item.value['region'][0] <= pt: + selected_index = index + else: + break + return items, selected_index + + def preview(self, text: Any) -> Union[str, sublime.Html, None]: + if isinstance(text, dict): + r = text.get('region') + if r: + self.view.run_command('lsp_selection_set', {'regions': [(r[0], r[1])]}) + self.view.show_at_center(r[0]) + if text.get('deprecated'): + return "⚠ Deprecated" + return "" + + def cancel(self) -> None: + if self.old_selection: + self.view.run_command('lsp_selection_set', {'regions': [(r.a, r.b) for r in self.old_selection]}) + self.view.show_at_center(self.old_selection[0].begin()) class SymbolQueryInput(sublime_plugin.TextInputHandler): diff --git a/stubs/mdpopups.pyi b/stubs/mdpopups.pyi index e8f95d70f..51954b939 100644 --- a/stubs/mdpopups.pyi +++ b/stubs/mdpopups.pyi @@ -85,3 +85,16 @@ def scope2style( selected: bool = False, explicit_background: bool = False ) -> str: ... + + +def worker_thread_resolver( + url: str, + done: Callable[[bytes], None] +) -> None: ... + + +def resolve_images( + minihtml: str, + resolver: Callable[[str, Callable[[bytes], None]], None], + on_done: Callable[[str], None] +) -> Optional[object]: ... diff --git a/stubs/sublime.pyi b/stubs/sublime.pyi index 943f9062b..f031efdc2 100644 --- a/stubs/sublime.pyi +++ b/stubs/sublime.pyi @@ -919,7 +919,9 @@ class View: def em_width(self) -> float: ... - # def is_folded(self, sr) -> bool: ... + def is_folded(self, sr: Region) -> bool: + ... + # def folded_regions(self): ... def fold(self, x: Union[Region, List[Region]]) -> bool: ... @@ -1006,7 +1008,7 @@ class View: def transform_region_from(self, region: Region, change_id: Any) -> Region: ... - def style_for_scope(self, scope: str) -> Dict[str, str]: + def style_for_scope(self, scope: str) -> Dict[str, Any]: ... @@ -1080,6 +1082,8 @@ class QuickPanelItem: class ListInputItem: + value = ... # type: Any + kind = ... # type: Tuple[int, str, str] def __init__( self, text: str, diff --git a/stubs/sublime_plugin.pyi b/stubs/sublime_plugin.pyi index 605330137..1a7b2a4a1 100644 --- a/stubs/sublime_plugin.pyi +++ b/stubs/sublime_plugin.pyi @@ -170,6 +170,10 @@ class CommandInputHandler: ... +class BackInputHandler(CommandInputHandler): + ... + + class TextInputHandler(CommandInputHandler): ... diff --git a/sublime-package.json b/sublime-package.json index cb4be4cbc..4d2b137da 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -462,6 +462,24 @@ "maximum": 4, "markdownDescription": "Show highlights and gutter markers in the file views for diagnostics with level equal to or less than:\n\n- _none_: `0`,\n- _error_: `1`,\n- _warning_: `2`,\n- _info_: `3`,\n- _hint_: `4`" }, + "show_diagnostics_annotations_severity_level": { + "default": 0, + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "markdownDescription": "Show diagnostics as annotations with level equal to or less than given value.\n\nWhen enabled, it's recommended not to use the `\"annotation\"` value for the `show_code_actions` option as it's impossible to enforce which one gets shown first.", + "markdownEnumDescriptions": [ + "never show", + "error", + "warning", + "info", + "hint" + ] + }, "diagnostics_panel_include_severity_level": { "type": "integer", "minimum": 1, @@ -708,6 +726,30 @@ "default": false, "markdownDescription": "Show inlay hints in the editor. Inlay hints are short annotations within the code, which show variable types or parameter names.\nThis is the default value used for new windows but can be overriden per-window using the `LSP: Toggle Inlay Hints` command from the Command Palette, Main Menu or a custom keybinding." }, + "initially_folded": { + "type": "array", + "items": { + "anyOf": [ + { + "enum": [ + "comment", + "imports", + "region" + ], + "markdownEnumDescriptions": [ + "Comment blocks", + "Imports or includes", + "Regions (e.g. `#region`)" + ] + }, + { + "type": "string" + } + ] + }, + "uniqueItems": true, + "markdownDescription": "Determines ranges which initially should be folded when a document is opened, provided that the language server has support for this." + } }, "additionalProperties": false } @@ -738,6 +780,14 @@ "LSP": { "type": "object", "markdownDescription": "The dictionary of your configured language servers or overrides for existing configurations. The keys of this dictionary are free-form. They give a humany-friendly name to the server configuration. They are shown in the bottom-left corner in the status bar once attached to a view (unless you have `\"show_view_status\"` set to `false`).", + "properties": { + "formatters": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, "additionalProperties": { "$ref": "sublime://settings/LSP#/definitions/ClientConfig" } diff --git a/tests/setup.py b/tests/setup.py index a3f6b8d14..b72f5c134 100644 --- a/tests/setup.py +++ b/tests/setup.py @@ -210,7 +210,7 @@ def await_promise(cls, promise: Union[YieldPromise, Promise]) -> Generator: def await_run_code_action(self, code_action: Dict[str, Any]) -> Generator: promise = YieldPromise() sublime.set_timeout_async( - lambda: self.session.run_code_action_async(code_action, progress=False).then( + lambda: self.session.run_code_action_async(code_action, progress=False, view=self.view).then( promise.fulfill)) yield from self.await_promise(promise) diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index cbd1f57c5..3841e8352 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -142,7 +142,7 @@ def test_requests_with_diagnostics(self) -> Generator: self.assertEquals(entire_content(self.view), 'const x = 1;') self.assertEquals(self.view.is_dirty(), False) - def test_applies_in_two_iterations(self) -> Generator: + def test_applies_only_one_pass(self) -> Generator: self.insert_characters('const x = 1') initial_change_count = self.view.change_count() yield from self.await_client_notification( @@ -179,7 +179,7 @@ def test_applies_in_two_iterations(self) -> Generator: self.view.run_command('lsp_save') # Wait for the view to be saved yield lambda: not self.view.is_dirty() - self.assertEquals(entire_content(self.view), 'const x = 1;\nAnd again!') + self.assertEquals(entire_content(self.view), 'const x = 1;') def test_applies_immediately_after_text_change(self) -> Generator: self.insert_characters('const x = 1') diff --git a/tests/test_single_document.py b/tests/test_single_document.py index 5e32b87a5..50a45a06f 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -347,7 +347,8 @@ def test_run_command(self) -> 'Generator': sublime.set_timeout_async( lambda: self.session.execute_command( {"command": "foo", "arguments": ["hello", "there", "general", "kenobi"]}, - progress=False + progress=False, + view=self.view, ).then(promise.fulfill) ) yield from self.await_promise(promise) diff --git a/third_party/__init__.py b/third_party/__init__.py index c956ee728..2444d0300 100644 --- a/third_party/__init__.py +++ b/third_party/__init__.py @@ -1,5 +1,5 @@ from .websocket_server import WebsocketServer __all__ = [ - WebsocketServer + 'WebsocketServer' ] diff --git a/third_party/websocket_server/__init__.py b/third_party/websocket_server/__init__.py index c956ee728..0bc9f6e92 100644 --- a/third_party/websocket_server/__init__.py +++ b/third_party/websocket_server/__init__.py @@ -1,5 +1,5 @@ from .websocket_server import WebsocketServer __all__ = [ - WebsocketServer -] + 'WebsocketServer' +] \ No newline at end of file