diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a5b140b870..173a9e7466d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: emacs-version: - 27.2 - 28.2 - - 29.3 + - 29.4 experimental: [false] include: - os: ubuntu-latest diff --git a/CHANGELOG.org b/CHANGELOG.org index 955237f4767..d3a464bdd03 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,10 +1,23 @@ * Changelog ** Unreleased 9.0.1 + * Add support for [[https://github.com/glehmann/earthlyls][earthlyls]] * Add support for GNAT Project (~gpr-mode~, ~gpr-ts-mode~). * Add SQL support * Add support for Meson build system. (~meson-mode~). * Add support for go to definition for external files (.dll) in CSharp projects for OmniSharp server. * Added a new optional ~:action-filter~ argument when defining LSP clients that allows code action requests to be modified before they are sent to the server. This is used by the Haskell language server client to work around an ~lsp-mode~ parsing quirk that incorrectly sends ~null~ values instead of ~false~ in code action requests. + * Add support for C# via the [[https://github.com/dotnet/roslyn/tree/main/src/LanguageServer][Roslyn language server]]. + * Add basic support for [[https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics][pull diagnostics]] requests. + * Add ~lsp-flush-delayed-changes-before-next-message~ customization point to enforce throttling document change notifications. + * Add ~lsp-sql-show-tables~ command. + * Fix bug in ~rust-analyzer.check.features~ configuration via ~lsp-rust-checkonsave-features~ Emacs setting: we were defaulting to ~[]~, but ~rust-analyzer~ defaults to inheriting the value from ~rust-analyzer.cargo.features~. The bug resulted in code hidden behind features not getting type checked when those features were enabled by setting ~rust-analyzer.cargo.features~ via the ~lsp-rust-features~ Emacs setting. + * Change ~ruff-lsp~ to ~ruff~ for python lsp client. All ~ruff-lsp~ customizable variable change to ~ruff~. Lsp server command now is ~["ruff" "server"]~ instead of ~["ruff-lsp"]~. + * Add futhark support + * Optimize overlay creation by checking window visibility first + * Replace the per-interface ~(INTERFACE ...)~ pcase forms with a single, + unified ~(lsp-interface INTERFACE ...)~ form. The per-interface forms are no + longer generated. *This is a breaking change.* (See #4430.) + * If asm-lsp is installed, lsp-asm won't try to download it to cache store * Fix lsp-unzip on windows when unzip was found on the PATH ** 9.0.0 diff --git a/clients/lsp-ada.el b/clients/lsp-ada.el index c11e4feb2b5..703b98bedae 100644 --- a/clients/lsp-ada.el +++ b/clients/lsp-ada.el @@ -33,18 +33,31 @@ :tag "Language Server" :package-version '(lsp-mode . "6.2")) -(lsp-defcustom lsp-ada-project-file "default.gpr" - "Set the project file full path to configure the language server with. - The ~ prefix (for the user home directory) is supported. - See https://github.com/AdaCore/ada_language_server for a per-project - configuration example." - :type 'string +(lsp-defcustom lsp-ada-project-file nil + "GNAT Project file used to configure the Language Server. + +Both absolute and relative paths are supported within the project file +name. When a relative path is used, the path is relative to the root +folder. + +When the project file is not specified, the Language Server will attempt +to determine the project file itself, either by querying \\='alr\\=', if +the root folder contains an alire.toml file and \\='alr\\=' was found in +the path, or otherwise by searching for a unique project file in the +root folder. For Alire projects, whose project file was discovered by +querying \\='alr\\=', the server will also query and populate the Alire +environment." + :type '(choice (string :tag "File") + (const :tag "Not Specified" nil)) :group 'lsp-ada - :package-version '(lsp-mode . "6.2") + :link '(url-link :tag "Configuration Example" + "https://github.com/AdaCore/ada_language_server") + :package-version '(lsp-mode . "9.0.1") :lsp-path "ada.projectFile") +;;;###autoload(put 'lsp-ada-project-file 'safe-local-variable 'stringp) (lsp-defcustom lsp-ada-option-charset "UTF-8" - "The charset to use by the Ada Language server. Defaults to 'UTF-8'." + "The charset to use by the Ada Language server. Defaults to \\='UTF-8\\='." :type 'string :group 'lsp-ada :package-version '(lsp-mode . "6.2") @@ -63,12 +76,6 @@ :risky t :type 'file) -(defcustom lsp-ada-alire-executable "alr" - "The alire executable to run when a project is detected." - :type 'string - :group 'lsp-ada - :package-version '(lsp-mode "9.0.0")) - (defcustom lsp-ada-semantic-token-face-overrides '(("namespace" . default) ("modifier" . lsp-face-semhl-keyword)) @@ -132,26 +139,6 @@ (lsp-ada--als-latest-release-url) "ada-ls")))) -(defun lsp-ada--environment () - "Add environmental variables if needed." - (let ((project-root (lsp-workspace-root))) - ;; When there is an alire project, include its environment - (when (file-exists-p - (concat (file-name-as-directory project-root) - "alire.toml")) - (let ((alr-executable (executable-find lsp-ada-alire-executable))) - (if alr-executable - ;; Transform output variables to environment - (let ((env-output (shell-command-to-string (concat alr-executable " printenv --unix")))) - (let ((var-strings (split-string env-output "\n"))) - (mapcar (lambda (string) - (if (string-match (rx "export" space (group (one-or-more ascii)) "=" "\"" (group (one-or-more ascii)) "\"") string) - (let ((var-name (match-string 1 string)) - (var-value (match-string 2 string))) - (cons var-name var-value)))) - var-strings))) - (lsp--error "Found alire.toml but the executable %s could not be found" alr-executable)))))) - (lsp-dependency 'ada-ls '(:download :url lsp-ada--als-latest-release-url @@ -175,8 +162,7 @@ :semantic-tokens-faces-overrides `( :types ,lsp-ada-semantic-token-face-overrides :modifiers ,lsp-ada-semantic-token-modifier-face-overrides) :server-id 'ada-ls - :synchronize-sections '("ada") - :environment-fn 'lsp-ada--environment)) + :synchronize-sections '("ada"))) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection @@ -186,8 +172,7 @@ :priority -1 :download-server-fn (lambda (_client callback error-callback _update?) (lsp-package-ensure 'ada-ls callback error-callback)) - :server-id 'gpr-ls - :environment-fn #'lsp-ada--environment)) + :server-id 'gpr-ls)) (lsp-consistency-check lsp-ada) diff --git a/clients/lsp-angular.el b/clients/lsp-angular.el index 288c43ecba3..867fe346859 100644 --- a/clients/lsp-angular.el +++ b/clients/lsp-angular.el @@ -75,13 +75,12 @@ Has no effects when `lsp-clients-angular-language-server-command' is set." ;; so we "cache" its results after running once (setq lsp-clients-angular-language-server-command (list - "node" - (f-join node-modules-path "@angular/language-server") - "--ngProbeLocations" - node-modules-path + "ngserver" + "--stdio" "--tsProbeLocations" node-modules-path - "--stdio")) + "--ngProbeLocations" + (f-join node-modules-path "@angular/language-server/node_modules/"))) lsp-clients-angular-language-server-command)))) :activation-fn (lambda (&rest _args) diff --git a/clients/lsp-asm.el b/clients/lsp-asm.el index 578558242b8..bd7eddf8a9f 100644 --- a/clients/lsp-asm.el +++ b/clients/lsp-asm.el @@ -81,7 +81,8 @@ Will update if UPDATE? is t." (make-lsp-client :new-connection (lsp-stdio-connection #'lsp-asm--server-command - (lambda () (f-exists? lsp-asm-store-path))) + (lambda () (or (executable-find "asm-lsp") + (f-exists? lsp-asm-store-path)))) :major-modes lsp-asm-active-modes :priority -1 :server-id 'asm-lsp diff --git a/clients/lsp-csharp.el b/clients/lsp-csharp.el index a978c44401e..1e8815fab3b 100644 --- a/clients/lsp-csharp.el +++ b/clients/lsp-csharp.el @@ -120,6 +120,7 @@ Usually this is to be set in your .dir-locals.el on the project root directory." :group 'lsp-csharp-omnisharp :type 'file) + (defcustom lsp-csharp-omnisharp-enable-decompilation-support nil "Decompile bytecode when browsing method metadata for types in assemblies. @@ -127,6 +128,22 @@ Otherwise only declarations for the methods are visible (the default)." :group 'lsp-csharp :type 'boolean) +(defcustom lsp-csharp-csharpls-use-dotnet-tool t + "Whether to use a dotnet tool version of the expected C# + language server; only available for csharp-ls" + :group 'lsp-csharp + :type 'boolean + :risky t) + +(defcustom lsp-csharp-csharpls-use-local-tool nil + "Whether to use csharp-ls as a global or local dotnet tool. + +Note: this variable has no effect if +lsp-csharp-csharpls-use-dotnet-tool is nil." + :group 'lsp-csharp + :type 'boolean + :risky t) + (lsp-dependency 'omnisharp-roslyn `(:download :url lsp-csharp-omnisharp-roslyn-download-url @@ -485,6 +502,15 @@ filename is returned so lsp-mode can display this file." (with-temp-buffer (insert-file-contents metadata-file-name) (buffer-string)))))) +(defun lsp-csharp--cls-find-executable () + (or (when lsp-csharp-csharpls-use-dotnet-tool + (if lsp-csharp-csharpls-use-local-tool + (list "dotnet" "tool" "run" "csharp-ls") + (list "csharp-ls"))) + (executable-find "csharp-ls") + (f-join (or (getenv "USERPROFILE") (getenv "HOME")) + ".dotnet" "tools" "csharp-ls"))) + (defun lsp-csharp--cls-make-launch-cmd () "Return command line to invoke csharp-ls." @@ -504,16 +530,24 @@ filename is returned so lsp-mode can display this file." (t nil))) - (csharp-ls-exec (or (executable-find "csharp-ls") - (f-join (or (getenv "USERPROFILE") (getenv "HOME")) - ".dotnet" "tools" "csharp-ls"))) + (csharp-ls-exec (lsp-csharp--cls-find-executable)) (solution-file-params (when lsp-csharp-solution-file (list "-s" lsp-csharp-solution-file)))) (append startup-wrapper - (list csharp-ls-exec) + (if (listp csharp-ls-exec) + csharp-ls-exec + (list csharp-ls-exec)) solution-file-params))) +(defun lsp-csharp--cls-test-csharp-ls-present () + "Return non-nil if dotnet tool csharp-ls is installed as a dotnet tool." + (string-match-p "csharp-ls" + (shell-command-to-string + (if lsp-csharp-csharpls-use-local-tool + "dotnet tool list" + "dotnet tool list -g")))) + (defun lsp-csharp--cls-download-server (_client callback error-callback update?) "Install/update csharp-ls language server using `dotnet tool'. @@ -522,7 +556,7 @@ Will update if UPDATE? is t" (lsp-async-start-process callback error-callback - "dotnet" "tool" (if update? "update" "install") "-g" "csharp-ls")) + "dotnet" "tool" (if update? "update" "install") (if lsp-csharp-csharpls-use-local-tool "" "-g") "csharp-ls")) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection #'lsp-csharp--cls-make-launch-cmd) diff --git a/clients/lsp-cucumber.el b/clients/lsp-cucumber.el index e1d9aaaaaea..3ad3cf6cc4f 100644 --- a/clients/lsp-cucumber.el +++ b/clients/lsp-cucumber.el @@ -44,6 +44,35 @@ This is only for development use." :type 'list :group 'lsp-cucumber) +(lsp-defcustom lsp-cucumber-features + ["src/test/**/*.feature" "features/**/*.feature" "tests/**/*.feature" "*specs*/**/*.feature"] + "Configure where the extension should look for .feature files." + :type '(repeat string) + :group 'lsp-cucumber + :package-version '(lsp-mode . "9.0.0") + :lsp-path "cucumber.features") + +(lsp-defcustom lsp-cucumber-glue + ["*specs*/**/*.cs" "features/**/*.js" "features/**/*.jsx" "features/**/*.php" "features/**/*.py" "features/**/*.rs" "features/**/*.rb" "features/**/*.ts" "features/**/*.tsx" "features/**/*_test.go" "**/*_test.go" "src/test/**/*.java" "tests/**/*.py" "tests/**/*.rs"] + "Configure where the extension should look for source code where +step definitions and parameter types are defined." + :type '(repeat string) + :group 'lsp-cucumber + :package-version '(lsp-mode . "9.0.0") + :lsp-path "cucumber.glue") + +(lsp-defcustom lsp-cucumber-parameter-types [] + "Configure parameters types to convert output parameters to your own types. + +Details at https://github.com/cucumber/cucumber-expressions#custom-parameter-types. +Sample: +[(:name \"actor\" + :regexp \"[A-Z][a-z]+\")]" + :type '(lsp-repeatable-vector plist) + :group 'lsp-cucumber + :package-version '(lsp-mode . "9.0.0") + :lsp-path "cucumber.parameterTypes") + (defun lsp-cucumber--server-command () "Generate startup command for Cucumber language server." (or (and lsp-cucumber-server-path diff --git a/clients/lsp-earthly.el b/clients/lsp-earthly.el new file mode 100644 index 00000000000..4fa9ef50d2d --- /dev/null +++ b/clients/lsp-earthly.el @@ -0,0 +1,94 @@ +;;; lsp-earthly.el --- earthlyls client -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Samuel Loury + +;; Author: Samuel Loury +;; Keywords: earthly lsp + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; LSP client for Earthfile + +;;; Code: + +(require 'lsp-mode) + +(defgroup lsp-earthly nil + "LSP support for Earthfile." + :group 'lsp-mode + :link '(url-link "https://github.com/glehmann/earthlyls") + :package-version `(lsp-mode . "9.0.0")) + +(defcustom lsp-earthly-active-modes + '(earthfile-mode) + "List of major mode that work with earthlyls." + :type '(list symbol) + :group 'lsp-earthly) + +(defcustom lsp-earthly-home-url + "https://github.com/glehmann/earthlyls" + "Url we use to install earthlyls." + :type 'string + :group 'lsp-earthly + :package-version '(lsp-mode . "9.0.0")) + +(defcustom lsp-earthly-store-path (f-join lsp-server-install-dir "earthly") + "The path to the file in which `earthlyls' will be stored." + :type 'file + :group 'lsp-earthly + :package-version '(lsp-mode . "9.0.0")) + +(defun lsp-earthly--download-server (_client callback error-callback update?) + "Install/update earthly-ls language server using `cargo install'. + +Will invoke CALLBACK or ERROR-CALLBACK based on result. +Will update if UPDATE? is t." + (when update? + (ignore-errors (delete-directory lsp-earthly-store-path t))) + (lsp-async-start-process + callback + error-callback + "cargo" "install" "--git" lsp-earthly-home-url "--root" + lsp-earthly-store-path "earthlyls")) + +(defun lsp-earthly--executable () + "Return earthlyls executable." + (let ((local (f-join lsp-earthly-store-path "bin" + (if (eq system-type 'windows-nt) + "earthlyls.exe" + "earthlyls")))) + (or (and (f-exists? local) local) + (executable-find "earthlyls") + (user-error "`earthlyls' is not installed; for installation see %s for more information" lsp-earthly-home-url)))) + +(defun lsp-earthly--server-command () + "Startup command for the earthlyls server." + (list (lsp-earthly--executable))) + +(lsp-register-client + (make-lsp-client + :new-connection (lsp-stdio-connection + #'lsp-earthly--server-command + (lambda () (f-exists? lsp-earthly-store-path))) + :major-modes lsp-earthly-active-modes + :priority -1 + :server-id 'earthlyls + :download-server-fn #'lsp-earthly--download-server)) + +(lsp-consistency-check lsp-earthly) + +(provide 'lsp-earthly) +;;; lsp-earthly.el ends here diff --git a/clients/lsp-elixir.el b/clients/lsp-elixir.el index 8944449b9f6..3c03b03645e 100644 --- a/clients/lsp-elixir.el +++ b/clients/lsp-elixir.el @@ -105,7 +105,7 @@ Leave as default to let `executable-find' search for it." :type '(repeat string) :package-version '(lsp-mode . "8.0.0")) -(defcustom lsp-elixir-ls-version "v0.20.0" +(defcustom lsp-elixir-ls-version "v0.22.0" "Elixir-Ls version to download. It has to be set before `lsp-elixir.el' is loaded and it has to be available here: https://github.com/elixir-lsp/elixir-ls/releases/" diff --git a/clients/lsp-eslint.el b/clients/lsp-eslint.el index 1fb71aee6f7..3094c4f6e72 100644 --- a/clients/lsp-eslint.el +++ b/clients/lsp-eslint.el @@ -42,7 +42,7 @@ :group 'lsp-eslint :package-version '(lsp-mode . "8.0.0")) -(defcustom lsp-eslint-download-url "https://github.com/emacs-lsp/lsp-server-binaries/blob/master/dbaeumer.vscode-eslint-2.2.2.vsix?raw=true" +(defcustom lsp-eslint-download-url "https://github.com/emacs-lsp/lsp-server-binaries/blob/master/dbaeumer.vscode-eslint-3.0.10.vsix?raw=true" "ESLint language server download url." :type 'string :group 'lsp-eslint diff --git a/clients/lsp-fsharp.el b/clients/lsp-fsharp.el index cd39e016b10..b2e4d5fb2dc 100644 --- a/clients/lsp-fsharp.el +++ b/clients/lsp-fsharp.el @@ -157,15 +157,6 @@ with test projects are not autoloaded by FSharpAutoComplete." :type 'boolean :package-version '(lsp-mode . "9.0.0")) -(defun lsp-fsharp--fsac-install (_client callback error-callback update?) - "Install/update fsautocomplete language server using `dotnet tool'. -Will invoke CALLBACK or ERROR-CALLBACK based on result. Will update if -UPDATE? is t." - (lsp-async-start-process - callback - error-callback - "dotnet" "tool" (if update? "update" "install") "-g" "fsautocomplete")) - (defcustom lsp-fsharp-use-dotnet-tool-for-fsac t "Run FsAutoComplete as a dotnet tool. @@ -176,9 +167,42 @@ available, else the globally installed tool." :type 'boolean :risky t) + +(defcustom lsp-fsharp-use-dotnet-local-tool nil + "When running FsAutoComplete as a dotnet tool, use the local version. + +This variable will have no effect if +`lsp-fsharp-use-dotnet-tool-for-fsac' is nil. + +This variable is risky as a buffer-local, and should instead be +set per-project (e.g. in a .dir-locals.el at the root of a +repository)." + :group 'lsp-fsharp + :type 'boolean + :risky t) + +(defcustom lsp-fsharp-workspace-extra-exclude-dirs nil + "Additional directories to exclude from FsAutoComplete + workspace loading / discovery." + :group 'lsp-fsharp + :type 'lsp-string-vector) + +(defun lsp-fsharp--fsac-install (_client callback error-callback update?) + "Install/update fsautocomplete language server using `dotnet tool'. +Will invoke CALLBACK or ERROR-CALLBACK based on result. Will update if +UPDATE? is t." + (lsp-async-start-process + callback + error-callback + "dotnet" "tool" (if update? "update" "install") (when lsp-fsharp-use-dotnet-local-tool "-g") "fsautocomplete")) + (defun lsp-fsharp--fsac-cmd () "The location of fsautocomplete executable." - (or (-let [maybe-local-executable (expand-file-name "fsautocomplete" lsp-fsharp-server-install-dir)] + (or (when lsp-fsharp-use-dotnet-tool-for-fsac + (if lsp-fsharp-use-dotnet-local-tool + (list "dotnet" "tool" "run" "fsautocomplete") + (list "fsautocomplete"))) + (-let [maybe-local-executable (expand-file-name "fsautocomplete" lsp-fsharp-server-install-dir)] (when (f-exists-p maybe-local-executable) maybe-local-executable)) (executable-find "fsautocomplete") @@ -209,22 +233,32 @@ available, else the globally installed tool." (t nil))) (fsautocomplete-exec (lsp-fsharp--fsac-cmd))) (append startup-wrapper - (list fsautocomplete-exec) + (if (listp fsautocomplete-exec) + fsautocomplete-exec + (list fsautocomplete-exec)) lsp-fsharp-server-args))) (defun lsp-fsharp--test-fsautocomplete-present () "Return non-nil if dotnet tool fsautocomplete is installed globally." (if lsp-fsharp-use-dotnet-tool-for-fsac - (string-match-p "fsautocomplete" - (shell-command-to-string "dotnet tool list -g")) + (-let* ((cmd-str (if lsp-fsharp-use-dotnet-local-tool + "dotnet tool list" + "dotnet tool list -g")) + (res (string-match-p "fsautocomplete" + (shell-command-to-string cmd-str)))) + (if res res + (error "Failed to locate fsautocomplete binary; due to lsp-fsharp-use-dotnet-local-tool == %s, checked with command %s" lsp-fsharp-use-dotnet-local-tool cmd-str))) + (f-exists? (lsp-fsharp--fsac-cmd)))) (defun lsp-fsharp--project-list (workspace) "Get the list of files we need to send to fsharp/workspaceLoad." - (let* ((response (lsp-request "fsharp/workspacePeek" + (let* ((base-exlude-dirs ["paket-files" ".git" "packages" "node_modules"]) + (exclude-dirs (apply 'vector (append base-exlude-dirs lsp-fsharp-workspace-extra-exclude-dirs))) + (response (lsp-request "fsharp/workspacePeek" `(:directory ,(lsp--workspace-root workspace) :deep 10 - :excludedDirs ["paket-files" ".git" "packages" "node_modules"]))) + :excludedDirs ,exclude-dirs))) (data (lsp--read-json (lsp-get response :content))) (found (-> data (lsp-get :Data) (lsp-get :Found))) (directory (seq-find (lambda (d) (equal "directory" (lsp-get d :Type))) found))) diff --git a/clients/lsp-futhark.el b/clients/lsp-futhark.el new file mode 100644 index 00000000000..85d8aee0936 --- /dev/null +++ b/clients/lsp-futhark.el @@ -0,0 +1,42 @@ +;;; lsp-futhark.el --- lsp-mode futhark integration -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 lsp-mode maintainers + +;; Keywords: languages + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; Client for the futhark language server. + +;;; Code: + +(require 'lsp-mode) + +(defgroup lsp-futhark nil + "LSP support for Futhark, using futhark lsp" + :group 'lsp-mode + :link '(url-link "https://github.com/diku-dk/futhark/tree/master/src/Futhark/LSP") + :package-version `(lsp-mode . "9.0.1")) + +(lsp-register-client + (make-lsp-client :new-connection (lsp-stdio-connection '("futhark" "lsp")) + :activation-fn (lsp-activate-on "futhark") + :server-id 'futhark)) + +(lsp-consistency-check lsp-futhark) + +(provide 'lsp-futhark) +;;; lsp-futhark.el ends here diff --git a/clients/lsp-gleam.el b/clients/lsp-gleam.el index f78756feda9..5e9651c8648 100644 --- a/clients/lsp-gleam.el +++ b/clients/lsp-gleam.el @@ -40,7 +40,7 @@ (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection lsp-gleam-executable) - :major-modes '(gleam-mode) + :major-modes '(gleam-mode gleam-ts-mode) :priority -1 :server-id 'gleam-lsp)) diff --git a/clients/lsp-golangci-lint.el b/clients/lsp-golangci-lint.el index e280dbdb9b4..1620aa56fed 100644 --- a/clients/lsp-golangci-lint.el +++ b/clients/lsp-golangci-lint.el @@ -35,7 +35,7 @@ (defgroup lsp-golangci-lint nil "Configuration options for lsp-golangci-lint." :group 'lsp-mode - :link '(url-lint "https://github.com/nametake/golangci-lint-langserver") + :link '(url-link "https://github.com/nametake/golangci-lint-langserver") :package-version '(lsp-mode . "9.0.0")) (defcustom lsp-golangci-lint-server-path "golangci-lint-langserver" diff --git a/clients/lsp-lua.el b/clients/lsp-lua.el index 56fc321fcdc..c06bbc5266b 100644 --- a/clients/lsp-lua.el +++ b/clients/lsp-lua.el @@ -85,7 +85,7 @@ "Lua LSP client, provided by the Lua Language Server." :group 'lsp-mode :version "8.0.0" - :link '(url-link "https://github.com/sumneko/lua-language-server")) + :link '(url-link "https://github.com/LuaLS/lua-language-server")) (defcustom lsp-clients-lua-language-server-install-dir (f-join lsp-server-install-dir "lua-language-server/") "Installation directory for Lua Language Server." @@ -547,12 +547,13 @@ and `../lib` ,exclude `../lib/temp`. (funcall callback)) error-callback :url (lsp--find-latest-gh-release-url - "https://api.github.com/repos/sumneko/lua-language-server/releases/latest" + "https://api.github.com/repos/LuaLS/lua-language-server/releases/latest" (format "%s%s.tar.gz" (pcase system-type ('gnu/linux (pcase (lsp-resolve-value lsp--system-arch) - ('x64 "linux-x64"))) + ('x64 "linux-x64") + ('arm64 "linux-arm64"))) ('darwin (pcase (lsp-resolve-value lsp--system-arch) ('x64 "darwin-x64") diff --git a/clients/lsp-magik.el b/clients/lsp-magik.el index 3e5c6821590..fb2edb663f9 100644 --- a/clients/lsp-magik.el +++ b/clients/lsp-magik.el @@ -34,7 +34,7 @@ :tag "Lsp Magik" :package-version '(lsp-mode . "9.0.0")) -(defcustom lsp-magik-version "0.9.0" +(defcustom lsp-magik-version "0.10.1" "Version of LSP server." :type `string :group `lsp-magik @@ -58,51 +58,99 @@ :group `lsp-magik :package-version '(lsp-mode . "9.0.0")) -(defcustom lsp-magik-java-home nil - "Path to Java Runtime, Java 11 minimum." +(lsp-defcustom lsp-magik-java-home nil + "Path to Java Runtime, Java 17 minimum." :type `string :group `lsp-magik - :package-version '(lsp-mode . "9.0.0")) + :package-version '(lsp-mode . "9.0.0") + :lsp-path "magik.javaHome") -(defcustom lsp-magik-smallworld-gis nil - "Path to Smallworld Core." - :type `string +(lsp-defcustom lsp-magik-product-dirs [] + "Paths to (compiled, containing a libs/ directory) products." + :type `lsp-string-vector :group `lsp-magik - :package-version '(lsp-mode . "9.0.0")) + :package-version '(lsp-mode . "9.0.1") + :lsp-path "magik.productDirs") -(defcustom lsp-magik-typing-type-database-paths [] +(lsp-defcustom lsp-magik-lint-override-config-file nil + "Override path to magiklintrc.properties." + :type 'string + :group `lsp-magik + :package-version '(lsp-mode . "9.0.0") + :lsp-path "magik.lint.overrideConfigFile") + +(lsp-defcustom lsp-magik-typing-type-database-paths [] "Paths to type databases." :type `lsp-string-vector :group `lsp-magik - :package-version '(lsp-mode . "9.0.0")) + :package-version '(lsp-mode . "9.0.0") + :lsp-path "magik.typing.typeDatabasePaths") -(defcustom lsp-magik-typing-enable-checks nil +(lsp-defcustom lsp-magik-typing-show-typing-inlay-hints nil + "Show typing inlay hints." + :type `boolean + :group `lsp-magik + :package-version '(lsp-mode . "9.0.1") + :lsp-path "magik.typing.showTypingInlayHints") + +(lsp-defcustom lsp-magik-typing-show-argument-inlay-hints nil + "Show (certain) argument name inlay hints." + :type `boolean + :group `lsp-magik + :package-version '(lsp-mode . "9.0.1") + :lsp-path "magik.typing.showArgumentInlayHints") + +(lsp-defcustom lsp-magik-typing-enable-checks nil "Enable typing checks." :type `boolean :group `lsp-magik - :package-version '(lsp-mode . "9.0.0")) + :package-version '(lsp-mode . "9.0.0") + :lsp-path "magik.typing.enableChecks") -(defcustom lsp-magik-trace-server "off" - "Traces the communication between VS Code and the Magik language server." - :type `(choice (const "off") (const "message") (const "verbose")) +(lsp-defcustom lsp-magik-typing-index-global-usages t + "Enable indexing of usages of globals by methods." + :type `boolean :group `lsp-magik - :package-version '(lsp-mode . "9.0.0")) + :package-version '(lsp-mode . "9.0.1") + :lsp-path "magik.typing.indexGlobalUsages") + +(lsp-defcustom lsp-magik-typing-index-method-usages nil + "Enable indexing of usages of methods by methods." + :type `boolean + :group `lsp-magik + :package-version '(lsp-mode . "9.0.1") + :lsp-path "magik.typing.indexMethodUsages") + +(lsp-defcustom lsp-magik-typing-index-slot-usages t + "Enable indexing of usages of slots by methods." + :type `boolean + :group `lsp-magik + :package-version '(lsp-mode . "9.0.1") + :lsp-path "magik.typing.indexSlotUsages") + +(lsp-defcustom lsp-magik-typing-index-condition-usages t + "Enable indexing of usages of conditions by methods." + :type `boolean + :group `lsp-magik + :package-version '(lsp-mode . "9.0.1") + :lsp-path "magik.typing.indexConditionUsages") + +(lsp-defcustom lsp-magik-typing-cache-indexed-definitions-method-usages t + "Store and load the indexed definitions in the workspace folders." + :type `boolean + :group `lsp-magik + :package-version '(lsp-mode . "9.0.1") + :lsp-path "magik.typing.cacheIndexedDefinitions") (defcustom lsp-magik-java-path (lambda () (cond ((eq system-type 'windows-nt) (or (lsp-resolve-value (executable-find (expand-file-name "bin/java" (getenv "JAVA_HOME")))) (lsp-resolve-value (executable-find "java")))) (t "java"))) - "Path of the java executable." - :type 'string - :group `lsp-magik - :package-version '(lsp-mode . "9.0.0")) - -(defcustom lsp-magik-lint-override-config-file nil - "Override path to magiklintrc.properties." + "Path to Java Runtime, Java 11 minimum." :type 'string :group `lsp-magik - :package-version '(lsp-mode . "9.0.0")) + :package-version '(lsp-mode . "9.0.1")) (lsp-register-client (make-lsp-client @@ -121,14 +169,6 @@ (lsp--set-configuration (lsp-configuration-section "magik")))) :server-id 'magik)) -(lsp-register-custom-settings - `(("magik.javaHome" lsp-magik-java-home) - ("magik.smallworldGis" lsp-magik-smallworld-gis) - ("magik.typing.typeDatabasePaths" lsp-magik-typing-type-database-paths) - ("magik.typing.enableChecks" lsp-magik-typing-enable-checks) - ("magik.trace.server" lsp-magik-trace-server) - ("magik.lint.overrideConfigFile" lsp-magik-lint-override-config-file))) - (lsp-consistency-check lsp-magik) (provide 'lsp-magik) diff --git a/clients/lsp-nix.el b/clients/lsp-nix.el index 19d847f5c6e..b50b40c6595 100644 --- a/clients/lsp-nix.el +++ b/clients/lsp-nix.el @@ -56,7 +56,7 @@ (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection (lambda () lsp-nix-nixd-server-path)) - :major-modes '(nix-mode) + :major-modes '(nix-mode nix-ts-mode) :server-id 'nixd-lsp :priority -1)) diff --git a/clients/lsp-pylsp.el b/clients/lsp-pylsp.el index 67bb28256bd..2406caad8f2 100644 --- a/clients/lsp-pylsp.el +++ b/clients/lsp-pylsp.el @@ -234,6 +234,16 @@ Drastically increases startup time." :type 'boolean :group 'lsp-pylsp) +(defcustom lsp-pylsp-plugins-rope-autoimport-completions-enabled nil + "Enable or disable completions from rope-autoimport." + :type 'boolean + :group 'lsp-pylsp) + +(defcustom lsp-pylsp-plugins-rope-autoimport-code-actions-enabled nil + "Enable or disable code actions from rope-autoimport." + :type 'boolean + :group 'lsp-pylsp) + (defcustom lsp-pylsp-plugins-rope-completion-enabled nil "Enable or disable the plugin." :type 'boolean @@ -583,6 +593,8 @@ So it will rename only references it can find." ("pylsp.plugins.pyls_isort.enabled" lsp-pylsp-plugins-isort-enabled t) ("pylsp.plugins.rope_autoimport.enabled" lsp-pylsp-plugins-rope-autoimport-enabled t) ("pylsp.plugins.rope_autoimport.memory" lsp-pylsp-plugins-rope-autoimport-memory t) + ("pylsp.plugins.rope_autoimport.completions.enabled" lsp-pylsp-plugins-rope-autoimport-completions-enabled t) + ("pylsp.plugins.rope_autoimport.code_actions.enabled" lsp-pylsp-plugins-rope-autoimport-code-actions-enabled t) ("pylsp.plugins.rope_completion.enabled" lsp-pylsp-plugins-rope-completion-enabled t) ("pylsp.plugins.rope_completion.eager" lsp-pylsp-plugins-rope-completion-eager t) ("pylsp.plugins.pyflakes.enabled" lsp-pylsp-plugins-pyflakes-enabled t) diff --git a/clients/lsp-roslyn-stdpipe.ps1 b/clients/lsp-roslyn-stdpipe.ps1 new file mode 100644 index 00000000000..0071424cb95 --- /dev/null +++ b/clients/lsp-roslyn-stdpipe.ps1 @@ -0,0 +1,136 @@ +param ( + [string]$ServerName = ".", + [Parameter(Mandatory=$true)][string]$PipeName +) + +# Use named pipe as stdin/out + +$Source = @" +using System; +using System.Text; +using System.IO; +using System.IO.Pipes; +using System.Threading.Tasks; + +public static class StdPipe +{ + public static void RouteToPipe(string pipeServer, string pipeName) + { + var pipeClient = new NamedPipeClientStream(pipeServer, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + pipeClient.Connect(); + + var pipeReader = new BufferedStream(pipeClient); + var pipeWriter = new BufferedStream(pipeClient); + + var stdin = new BufferedStream(Console.OpenStandardInput()); + var stdout = new BufferedStream(Console.OpenStandardOutput()); + + var tasks = new Task[2] + { + ReadHeaderDelimitedAsync(pipeReader), + ReadHeaderDelimitedAsync(stdin) + }; + + while (true) + { + var doneIdx = Task.WaitAny(tasks); + + var bytesRead = tasks[doneIdx].Result; + if (doneIdx == 0) + { + // pipe in -> stdout + if (bytesRead.Length == 0) + { + // pipe was closed + break; + } + + stdout.Write(bytesRead, 0, bytesRead.Length); + stdout.Flush(); + tasks[doneIdx] = ReadHeaderDelimitedAsync(pipeReader); + } + else + { + // stdin -> pipe out + pipeWriter.Write(bytesRead, 0, bytesRead.Length); + pipeWriter.Flush(); + tasks[doneIdx] = ReadHeaderDelimitedAsync(stdin); + } + } + } + + private static async Task ReadHeaderDelimitedAsync(Stream stream) + { + // Assigning new tasks with this function blocks the thread + // unless this is awaited first. + await Task.Yield(); + + var idx = 0; + var header = new byte[64]; + int b = 0; + do + { + var bytesRead = await stream.ReadAsync(header, idx, 1); + if (bytesRead == 0) + continue; + b = header[idx++]; + } while (b != '\r'); + + var colonPos = Array.IndexOf(header, (byte)':'); + if (colonPos == -1) + { + return new byte[0]; + } + + var headerName = new byte[colonPos]; + Array.Copy(header, headerName, colonPos); + var headerContent = new byte[idx - colonPos - 1]; + Array.Copy(header, colonPos + 2, headerContent, 0, headerContent.Length - 2); + + if (Encoding.ASCII.GetString(headerName) != "Content-Length") + { + return new byte[0]; + } + if (headerContent.Length > 20) + { + return new byte[0]; + } + int contentLength; + try + { + contentLength = int.Parse(Encoding.ASCII.GetString(headerContent)); + } + catch (Exception) + { + return new byte[0]; + } + + var buffer = new byte[contentLength + idx + 3]; + var c = 0; + for (var i = 0; i < idx; i++) + { + buffer[c++] = header[i]; + } + + // LF, CRLF + var bytesToRead = contentLength + 3; + while (bytesToRead > 0) + { + var bytesRead = await stream.ReadAsync(buffer, c, bytesToRead); + bytesToRead -= bytesRead; + c += bytesRead; + } + + return buffer; + } +} +"@ + +Add-Type -TypeDefinition $Source -Language CSharp + +try { + [StdPipe]::RouteToPipe($ServerName, $PipeName) +} catch [System.AggregateException] { + Write-Error $error[0].exception.innerexception + throw $error[0].exception.innerexception +} diff --git a/clients/lsp-roslyn.el b/clients/lsp-roslyn.el new file mode 100644 index 00000000000..9a086f79d13 --- /dev/null +++ b/clients/lsp-roslyn.el @@ -0,0 +1,360 @@ +;;; lsp-roslyn.el --- description -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Ruin0x11 + +;; Author: Ruin0x11 +;; Keywords: + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; C# client using the Roslyn language server + +;;; Code: + +(require 'lsp-mode) + +(defgroup lsp-roslyn nil + "LSP support for the C# programming language, using the Roslyn language server." + :link '(url-link "https://github.com/dotnet/roslyn/tree/main/src/LanguageServer") + :group 'lsp-mode + :package-version '(lsp-mode . "8.0.0")) + +(defconst lsp-roslyn--stdpipe-path (expand-file-name + "lsp-roslyn-stdpipe.ps1" + (file-name-directory (locate-library "lsp-roslyn"))) + "Path to the `stdpipe' script. +On Windows, this script is used as a proxy for the language server's named pipe. +Unused on other platforms.") + +(defcustom lsp-roslyn-install-path (expand-file-name "roslyn" lsp-server-install-dir) + "The path to install the Roslyn server to." + :type 'string + :package-version '(lsp-mode . "8.0.0") + :group 'lsp-roslyn) + +(defcustom lsp-roslyn-server-dll-override-path nil + "Custom path to Microsoft.CodeAnalysis.LanguageServer.dll." + :type '(choice (const nil) string) + :package-version '(lsp-mode . "8.0.0") + :group 'lsp-roslyn) + +(defcustom lsp-roslyn-server-timeout-seconds 60 + "Amount of time to wait for Roslyn server startup, in seconds." + :type 'integer + :package-version '(lsp-mode . "8.0.0") + :group 'lsp-roslyn) + +(defcustom lsp-roslyn-server-log-level "Information" + "Log level for the Roslyn language server." + :type '(choice (:tag "None" "Trace" "Debug" "Information" "Warning" "Error" "Critical")) + :package-version '(lsp-mode . "8.0.0") + :group 'lsp-roslyn) + +(defcustom lsp-roslyn-server-log-directory (concat (temporary-file-directory) (file-name-as-directory "lsp-roslyn")) + "Log directory for the Roslyn language server." + :type 'string + :package-version '(lsp-mode . "8.0.0") + :group 'lsp-roslyn) + +(defcustom lsp-roslyn-server-extra-args '() + "Extra arguments for the Roslyn language server." + :type '(repeat string) + :package-version '(lsp-mode . "8.0.0") + :group 'lsp-roslyn) + +(defcustom lsp-roslyn-dotnet-executable "dotnet" + "Dotnet executable to use with the Roslyn language server." + :type 'string + :package-version '(lsp-mode . "8.0.0") + :group 'lsp-roslyn) + +(defcustom lsp-roslyn-package-version "4.12.0-3.24470.11" + "Version of the Roslyn package to install. +Gotten from https://dev.azure.com/azure-public/vside/_artifacts/feed/vs-impl/NuGet/Microsoft.CodeAnalysis.LanguageServer.win-x64" + :type 'string + :package-version '(lsp-mode . "8.0.0") + :group 'lsp-roslyn) + +(defvar lsp-roslyn--pipe-name nil) + +(defun lsp-roslyn--parse-pipe-name (pipe) + (if (eq system-type 'windows-nt) + (progn + (string-match "\\([a-z0-9]+\\)$" pipe) + (match-string 1 pipe)) + pipe)) + +(defun lsp-roslyn--parent-process-filter (_process output) + "Parses the named pipe's name that the Roslyn server process prints on stdout." + (let* ((data (json-parse-string output :object-type 'plist)) + (pipe (plist-get data :pipeName))) + (when pipe + (setq lsp-roslyn--pipe-name (lsp-roslyn--parse-pipe-name pipe))))) + +(defun lsp-roslyn--make-named-pipe-process (filter sentinel environment-fn process-name stderr-buf) + "Creates the process that will handle the JSON-RPC communication." + (let* ((process-environment + (lsp--compute-process-environment environment-fn)) + (default-directory (lsp--default-directory-for-connection))) + (cond + ((eq system-type 'windows-nt) + (make-process + :name process-name + :connection-type 'pipe + :buffer (format "*%s*" process-name) + :coding 'no-conversion + :filter filter + :sentinel sentinel + :stderr stderr-buf + :noquery t + :command (lsp-resolve-final-command + `("PowerShell" "-NoProfile" "-ExecutionPolicy" "Bypass" "-Command" + ,lsp-roslyn--stdpipe-path "." + ,lsp-roslyn--pipe-name)))) + (t (make-network-process + :name process-name + :remote lsp-roslyn--pipe-name + :sentinel sentinel + :filter filter + :noquery t))))) + +(defun lsp-roslyn--connect (filter sentinel name environment-fn _workspace) + "Creates a connection to the Roslyn language server's named pipe. + +First creates an instance of the language server process, then +creates another process connecting to the named pipe it specifies." + (setq lsp-roslyn--pipe-name nil) + (let* ((parent-process-name name) + (parent-stderr-buf (format "*%s::stderr*" parent-process-name)) + (command-process (make-process + :name parent-process-name + :buffer (generate-new-buffer-name parent-process-name) + :coding 'no-conversion + :filter 'lsp-roslyn--parent-process-filter + :sentinel sentinel + :stderr parent-stderr-buf + :command `(,lsp-roslyn-dotnet-executable + ,(lsp-roslyn--get-server-dll-path) + ,(format "--logLevel=%s" lsp-roslyn-server-log-level) + ,(format "--extensionLogDirectory=%s" lsp-roslyn-server-log-directory) + ,@lsp-roslyn-server-extra-args) + :noquery t))) + (accept-process-output command-process lsp-roslyn-server-timeout-seconds) ; wait for JSON with pipe name to print on stdout, like {"pipeName":"\\\\.\\pipe\\d1b72351"} + (when (not lsp-roslyn--pipe-name) + (error "Failed to receieve pipe name from Roslyn server process")) + (let* ((process-name (generate-new-buffer-name (format "%s-pipe" name))) + (stderr-buf (format "*%s::stderr*" process-name)) + (communication-process + (lsp-roslyn--make-named-pipe-process filter sentinel environment-fn process-name stderr-buf))) + (with-current-buffer (get-buffer parent-stderr-buf) + (special-mode)) + (when-let ((stderr-buffer (get-buffer stderr-buf))) + (with-current-buffer stderr-buffer + ;; Make the *NAME::stderr* buffer buffer-read-only, q to bury, etc. + (special-mode)) + (set-process-query-on-exit-flag (get-buffer-process stderr-buffer) nil)) + (set-process-query-on-exit-flag command-process nil) + (set-process-query-on-exit-flag communication-process nil) + (cons communication-process communication-process)))) + +(defun lsp-roslyn--uri-to-path (uri) + "Convert a URI to a file path, without unhexifying." + (let* ((url (url-generic-parse-url uri)) + (type (url-type url)) + (target (url-target url)) + (file + (concat (decode-coding-string (url-filename url) + (or locale-coding-system 'utf-8)) + (when (and target + (not (s-match + (rx "#" (group (1+ num)) (or "," "#") + (group (1+ num)) + string-end) + uri))) + (concat "#" target)))) + (file-name (if (and type (not (string= type "file"))) + (if-let ((handler (lsp--get-uri-handler type))) + (funcall handler uri) + uri) + ;; `url-generic-parse-url' is buggy on windows: + ;; https://github.com/emacs-lsp/lsp-mode/pull/265 + (or (and (eq system-type 'windows-nt) + (eq (elt file 0) ?\/) + (substring file 1)) + file)))) + (->> file-name + (concat (-some #'lsp--workspace-host-root (lsp-workspaces))) + (lsp-remap-path-if-needed)))) + +(defun lsp-roslyn--path-to-uri (path) + "Convert PATH to a URI, without hexifying." + (url-unhex-string (lsp--path-to-uri-1 path))) + +(lsp-defun lsp-roslyn--log-message (_workspace params) + (let ((type (gethash "type" params)) + (mes (gethash "message" params))) + (cl-case type + (1 (lsp--error "%s" mes)) ; Error + (2 (lsp--warn "%s" mes)) ; Warning + (3 (lsp--info "%s" mes)) ; Info + (t (lsp--info "%s" mes))))) ; Log + +(lsp-defun lsp-roslyn--on-project-initialization-complete (workspace _params) + (lsp--info "%s: Project initialized successfully." + (lsp--workspace-print workspace))) + +(defun lsp-roslyn--find-files-in-parent-directories (directory regex &optional result) + "Search DIRECTORY for files matching REGEX and return their full paths if found." + (let* ((parent-dir (file-truename (concat (file-name-directory directory) "../"))) + (found (directory-files directory 't regex)) + (result (append (or result '()) found))) + (if (and (not (string= (file-truename directory) parent-dir)) + (< (length parent-dir) (length (file-truename directory)))) + (lsp-roslyn--find-files-in-parent-directories parent-dir regex result) + result))) + +(defun lsp-roslyn--pick-solution-file-interactively (solution-files) + (completing-read "Solution file for this workspace: " solution-files nil t)) + +(defun lsp-roslyn--find-solution-file () + (let ((solutions (lsp-roslyn--find-files-in-parent-directories + (file-name-directory (buffer-file-name)) + (rx (* any) ".sln" eos)))) + (cond + ((not solutions) nil) + ((eq (length solutions) 1) (cl-first solutions)) + (t (lsp-roslyn--pick-solution-file-interactively solutions))))) + +(defun lsp-roslyn-open-solution-file () + "Chooses the solution file to associate with the Roslyn language server." + (interactive) + (let ((solution-file (lsp-roslyn--find-solution-file))) + (if solution-file + (lsp-notify "solution/open" (list :solution (lsp--path-to-uri solution-file))) + (lsp--error "No solution file was found for this workspace.")))) + +(defun lsp-roslyn--on-initialized (_workspace) + "Handler for Roslyn server initialization." + (lsp-roslyn-open-solution-file)) + +(defun lsp-roslyn--get-package-name () + "Gets the package name of the Roslyn language server." + (format "microsoft.codeanalysis.languageserver.%s" (lsp-roslyn--get-rid))) + +(defun lsp-roslyn--get-server-dll-path () + "Gets the path to the language server DLL. +Assumes it was installed with the server install function." + (if lsp-roslyn-server-dll-override-path + lsp-roslyn-server-dll-override-path + (f-join lsp-roslyn-install-path "out" + (lsp-roslyn--get-package-name) + lsp-roslyn-package-version + "content" "LanguageServer" + (lsp-roslyn--get-rid) + "Microsoft.CodeAnalysis.LanguageServer.dll"))) + +(defun lsp-roslyn--get-rid () + "Retrieves the .NET Runtime Identifier (RID) for the current system." + (let* ((is-x64 (string-match-p (rx (or "x86_64" "aarch64")) system-configuration)) + (is-x86 (and (string-match-p "x86" system-configuration) (not is-x64))) + (is-arm (string-match-p (rx (or "arm" "aarch")) system-configuration))) + (if-let ((platform-name (cond + ((eq system-type 'gnu/linux) "linux") + ((eq system-type 'darwin) "osx") + ((eq system-type 'windows-nt) "win"))) + (arch-name (cond + (is-x64 "x64") + (is-x86 "x86") + (is-arm "arm64")))) + (format "%s-%s" platform-name arch-name) + (error "Unsupported platform: %s (%s)" system-type system-configuration)))) + +;; Adapted from roslyn.nvim's version +(defconst lsp-roslyn--temp-project-nuget-config + " + + + + +" + "The nuget.config to use when downloading Roslyn.") + +;; Adapted from roslyn.nvim's version +(defun lsp-roslyn--temp-project-csproj (pkg-name pkg-version) + "Generates a temporary .csproj to use for downloading the language server." + (format + " + + + out + + net7.0 + + true + + false + + + + +" + pkg-name pkg-version)) + +(defun lsp-roslyn--download-server (_client callback error-callback update?) + "Downloads the Roslyn language server to `lsp-roslyn-install-path'. +CALLBACK is called when the download finish successfully otherwise +ERROR-CALLBACK is called. +UPDATE is non-nil if it is already downloaded. +FORCED if specified with prefix argument." + + (let ((pkg-name (lsp-roslyn--get-package-name))) + (when update? + (ignore-errors (delete-directory lsp-roslyn-install-path t))) + (unless (f-exists? lsp-roslyn-install-path) + (mkdir lsp-roslyn-install-path 'create-parent)) + (f-write-text lsp-roslyn--temp-project-nuget-config + 'utf-8 (expand-file-name "nuget.config" lsp-roslyn-install-path)) + (f-write-text (lsp-roslyn--temp-project-csproj pkg-name lsp-roslyn-package-version) + 'utf-8 (expand-file-name "DownloadRoslyn.csproj" lsp-roslyn-install-path)) + (lsp-async-start-process + callback + error-callback + lsp-roslyn-dotnet-executable "restore" "--interactive" lsp-roslyn-install-path + (format "/p:PackageName=%s" pkg-name) + (format "/p:PackageVersion=%s" lsp-roslyn-package-version)))) + +(defun lsp-roslyn--make-connection () + (list :connect (lambda (f s n e w) (lsp-roslyn--connect f s n e w)) + :test? (lambda () (f-exists? (lsp-roslyn--get-server-dll-path))))) + +(lsp-register-client + (make-lsp-client :new-connection (lsp-roslyn--make-connection) + :priority 0 + :server-id 'csharp-roslyn + :activation-fn (lsp-activate-on "csharp") + :notification-handlers (ht ("window/logMessage" 'lsp-roslyn--log-message) + ("workspace/projectInitializationComplete" 'lsp-roslyn--on-project-initialization-complete)) + + ;; These two functions are the same as lsp-mode's except they do not + ;; (un)hexify URIs. + :path->uri-fn 'lsp-roslyn--path-to-uri + :uri->path-fn 'lsp-roslyn--uri-to-path + + :initialized-fn #'lsp-roslyn--on-initialized + :download-server-fn #'lsp-roslyn--download-server)) + +(provide 'lsp-roslyn) +;;; lsp-roslyn.el ends here diff --git a/clients/lsp-ruby-lsp.el b/clients/lsp-ruby-lsp.el index e6039dafd76..0b4ba23f53e 100644 --- a/clients/lsp-ruby-lsp.el +++ b/clients/lsp-ruby-lsp.el @@ -38,16 +38,48 @@ :safe #'booleanp :group 'lsp-ruby-lsp) +(defcustom lsp-ruby-lsp-library-directories + '("~/.rbenv/" "/usr/lib/ruby/" "~/.rvm/" "~/.gem/" "~/.asdf") + "List of directories which will be considered to be libraries." + :type '(repeat string) + :group 'lsp-ruby-lsp + :package-version '(lsp-mode . "9.0.1")) + (defun lsp-ruby-lsp--build-command () (append (if lsp-ruby-lsp-use-bundler '("bundle" "exec")) '("ruby-lsp"))) +(defun lsp-ruby-lsp--open-file (arg_hash) + "Open a file. This function is for code-lens provided by ruby-lsp-rails." + (let* ((arguments (gethash "arguments" arg_hash)) + (uri (aref (aref arguments 0) 0)) + (path-with-line-number (split-string (lsp--uri-to-path uri) "#L")) + (path (car path-with-line-number)) + (line-number (cadr path-with-line-number))) + (find-file path) + (when line-number (forward-line (1- (string-to-number line-number)))))) + +(defun lsp-ruby-lsp--run-test (arg_hash) + "Run a test file. This function is for code-lens provided by ruby-lsp-rails." + (let* ((arguments (gethash "arguments" arg_hash)) + (command (aref arguments 2)) + (default-directory (lsp-workspace-root)) + (buffer-name "*run test results*") + (buffer (progn + (when (get-buffer buffer-name) (kill-buffer buffer-name)) + (generate-new-buffer buffer-name)))) + (async-shell-command command buffer))) + (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection #'lsp-ruby-lsp--build-command) :activation-fn (lsp-activate-on "ruby") + :library-folders-fn (lambda (_workspace) lsp-ruby-lsp-library-directories) :priority -2 + :action-handlers (ht ("rubyLsp.openFile" #'lsp-ruby-lsp--open-file) + ("rubyLsp.runTest" #'lsp-ruby-lsp--run-test) + ("rubyLsp.runTestInTerminal" #'lsp-ruby-lsp--run-test)) :server-id 'ruby-lsp-ls)) (lsp-consistency-check lsp-ruby-lsp) diff --git a/clients/lsp-ruff-lsp.el b/clients/lsp-ruff-lsp.el deleted file mode 100644 index c95359d5c18..00000000000 --- a/clients/lsp-ruff-lsp.el +++ /dev/null @@ -1,115 +0,0 @@ -;;; lsp-ruff-lsp.el --- ruff-lsp support -*- lexical-binding: t; -*- - -;; Copyright (C) 2023 Freja Nordsiek -;; -;; Author: Freja Nordsiek . - -;;; Commentary: - -;; ruff-lsp Client for the Python programming language - -;;; Code: - -(require 'lsp-mode) - -(defgroup lsp-ruff-lsp nil - "LSP support for Python, using ruff-lsp's Python Language Server." - :group 'lsp-mode - :link '(url-link "https://github.com/charliermarsh/ruff-lsp")) - -(defcustom lsp-ruff-lsp-server-command '("ruff-lsp") - "Command to start ruff-lsp." - :risky t - :type '(repeat string) - :group 'lsp-ruff-lsp) - -(defcustom lsp-ruff-lsp-ruff-path ["ruff"] - "Paths to ruff to try, in order." - :risky t - :type 'lsp-string-vector - :group 'lsp-ruff-lsp) - -(defcustom lsp-ruff-lsp-ruff-args [] - "Arguments, passed to ruff." - :risky t - :type 'lsp-string-vector - :group 'lsp-ruff-lsp) - -(defcustom lsp-ruff-lsp-log-level "error" - "Tracing level." - :type '(choice (const "debug") - (const "error") - (const "info") - (const "off") - (const "warn")) - :group 'lsp-ruff-lsp) - -(defcustom lsp-ruff-lsp-python-path "python3" - "Path to the Python interpreter." - :risky t - :type 'string - :group 'lsp-ruff-lsp) - -(defcustom lsp-ruff-lsp-show-notifications "off" - "When notifications are shown." - :type '(choice (const "off") - (const "onError") - (const "onWarning") - (const "always")) - :group 'lsp-ruff-lsp) - -(defcustom lsp-ruff-lsp-advertize-organize-imports t - "Whether to report ability to handle source.organizeImports actions." - :type 'boolean - :group 'lsp-ruff-lsp) - -(defcustom lsp-ruff-lsp-advertize-fix-all t - "Whether to report ability to handle source.fixAll actions." - :type 'boolean - :group 'lsp-ruff-lsp) - -(defcustom lsp-ruff-lsp-import-strategy "fromEnvironment" - "Where ruff is imported from if lsp-ruff-lsp-ruff-path is not set." - :type '(choice (const "fromEnvironment") - (const "useBundled")) - :group 'lsp-ruff-lsp) - - -(lsp-register-client - (make-lsp-client - :new-connection (lsp-stdio-connection - (lambda () lsp-ruff-lsp-server-command)) - :activation-fn (lsp-activate-on "python") - :server-id 'ruff-lsp - :priority -2 - :add-on? t - :initialization-options - (lambda () - (list :settings - (list :args lsp-ruff-lsp-ruff-args - :logLevel lsp-ruff-lsp-log-level - :path lsp-ruff-lsp-ruff-path - :interpreter (vector lsp-ruff-lsp-python-path) - :showNotifications lsp-ruff-lsp-show-notifications - :organizeImports (lsp-json-bool lsp-ruff-lsp-advertize-organize-imports) - :fixAll (lsp-json-bool lsp-ruff-lsp-advertize-fix-all) - :importStrategy lsp-ruff-lsp-import-strategy))))) - -(lsp-consistency-check lsp-ruff-lsp) - -(provide 'lsp-ruff-lsp) -;;; lsp-ruff-lsp.el ends here diff --git a/clients/lsp-ruff.el b/clients/lsp-ruff.el new file mode 100644 index 00000000000..41122563b99 --- /dev/null +++ b/clients/lsp-ruff.el @@ -0,0 +1,107 @@ +;;; lsp-ruff.el --- ruff lsp support -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Freja Nordsiek +;; +;; Author: Freja Nordsiek . + +;;; Commentary: + +;; ruff LSP Client for the Python programming language + +;;; Code: + +(require 'lsp-mode) + +(defgroup lsp-ruff nil + "LSP support for Python, using ruff's Python Language Server." + :group 'lsp-mode + :link '(url-link "https://github.com/astral-sh/ruff")) + +(defcustom lsp-ruff-server-command '("ruff" "server") + "Command to start ruff lsp. +Previous ruff-lsp should change this to (\"ruff-lsp\")" + :risky t + :type '(repeat string) + :group 'lsp-ruff) + +(defcustom lsp-ruff-ruff-args '() + "Arguments, passed to ruff." + :risky t + :type '(repeat string) + :group 'lsp-ruff) + +(defcustom lsp-ruff-log-level "error" + "Tracing level." + :type '(choice (const "debug") + (const "error") + (const "info") + (const "off") + (const "warn")) + :group 'lsp-ruff) + +(defcustom lsp-ruff-python-path "python3" + "Path to the Python interpreter." + :risky t + :type 'string + :group 'lsp-ruff) + +(defcustom lsp-ruff-show-notifications "off" + "When notifications are shown." + :type '(choice (const "off") + (const "onError") + (const "onWarning") + (const "always")) + :group 'lsp-ruff) + +(defcustom lsp-ruff-advertize-organize-imports t + "Whether to report ability to handle source.organizeImports actions." + :type 'boolean + :group 'lsp-ruff) + +(defcustom lsp-ruff-advertize-fix-all t + "Whether to report ability to handle source.fixAll actions." + :type 'boolean + :group 'lsp-ruff) + +(defcustom lsp-ruff-import-strategy "fromEnvironment" + "Where ruff is imported from if lsp-ruff-ruff-path is not set." + :type '(choice (const "fromEnvironment") + (const "useBundled")) + :group 'lsp-ruff) + + +(lsp-register-client + (make-lsp-client + :new-connection (lsp-stdio-connection + (lambda () (append lsp-ruff-server-command lsp-ruff-ruff-args))) + :activation-fn (lsp-activate-on "python") + :server-id 'ruff + :priority -2 + :add-on? t + :initialization-options + (lambda () + (list :settings + (list :logLevel lsp-ruff-log-level + :showNotifications lsp-ruff-show-notifications + :organizeImports (lsp-json-bool lsp-ruff-advertize-organize-imports) + :fixAll (lsp-json-bool lsp-ruff-advertize-fix-all) + :importStrategy lsp-ruff-import-strategy))))) + +(lsp-consistency-check lsp-ruff) + +(provide 'lsp-ruff) +;;; lsp-ruff.el ends here diff --git a/clients/lsp-rust.el b/clients/lsp-rust.el index 5b003747474..48c17181fd1 100644 --- a/clients/lsp-rust.el +++ b/clients/lsp-rust.el @@ -187,6 +187,7 @@ the latest build duration." (defcustom lsp-rust-features [] "List of features to activate. +Corresponds to the `rust-analyzer` setting `rust-analyzer.cargo.features`. Set this to `\"all\"` to pass `--all-features` to cargo." :type 'lsp-string-vector :group 'lsp-rust-rls @@ -596,9 +597,15 @@ The command should include `--message=format=json` or similar option." :group 'lsp-rust-analyzer :package-version '(lsp-mode . "8.0.2")) -(defcustom lsp-rust-analyzer-checkonsave-features [] +(defcustom lsp-rust-analyzer-checkonsave-features nil "List of features to activate. -Set this to `\"all\"` to pass `--all-features` to cargo." +Corresponds to the `rust-analyzer` setting `rust-analyzer.check.features`. +When set to `nil` (default), the value of `lsp-rust-features' is inherited. +Set this to `\"all\"` to pass `--all-features` to cargo. +Note: setting this to `nil` means \"unset\", whereas setting this +to `[]` (empty vector) means \"set to empty list of features\", +which overrides any value that would otherwise be inherited from +`lsp-rust-features'." :type 'lsp-string-vector :group 'lsp-rust-rust-analyzer :package-version '(lsp-mode . "8.0.2")) @@ -1666,11 +1673,22 @@ https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.m :merge (:glob ,(lsp-json-bool lsp-rust-analyzer-imports-merge-glob)) :prefix ,lsp-rust-analyzer-import-prefix) :lruCapacity ,lsp-rust-analyzer-lru-capacity + ;; This `checkOnSave` is called `check` in the `rust-analyzer` docs, not + ;; `checkOnSave`, but the `rust-analyzer` source code shows that both names + ;; work. The `checkOnSave` name has been supported by `rust-analyzer` for a + ;; long time, whereas the `check` name was introduced here in 2023: + ;; https://github.com/rust-lang/rust-analyzer/commit/d2bb62b6a81d26f1e41712e04d4ac760f860d3b3 :checkOnSave ( :enable ,(lsp-json-bool lsp-rust-analyzer-cargo-watch-enable) :command ,lsp-rust-analyzer-cargo-watch-command :extraArgs ,lsp-rust-analyzer-cargo-watch-args :allTargets ,(lsp-json-bool lsp-rust-analyzer-check-all-targets) - :features ,lsp-rust-analyzer-checkonsave-features + ;; We need to distinguish between setting this to the empty + ;; vector, and not setting it at all, which `rust-analyzer` + ;; interprets as "inherit from + ;; `rust-analyzer.cargo.features`". We use `nil` to mean + ;; "unset". + ,@(when (vectorp lsp-rust-analyzer-checkonsave-features) + `(:features ,lsp-rust-analyzer-checkonsave-features)) :overrideCommand ,lsp-rust-analyzer-cargo-override-command) :highlightRelated ( :breakPoints (:enable ,(lsp-json-bool lsp-rust-analyzer-highlight-breakpoints)) :closureCaptures (:enable ,(lsp-json-bool lsp-rust-analyzer-highlight-closure-captures)) @@ -1761,7 +1779,17 @@ https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.m :semantic-tokens-faces-overrides `( :discard-default-modifiers t :modifiers ,(lsp-rust-analyzer--semantic-modifiers)) :server-id 'rust-analyzer - :custom-capabilities `((experimental . ((snippetTextEdit . ,(and lsp-enable-snippet (fboundp 'yas-minor-mode)))))) + :custom-capabilities `((experimental . + ((snippetTextEdit . ,(and lsp-enable-snippet (fboundp 'yas-minor-mode))) + (commands . ((commands . + [ + "rust-analyzer.runSingle" + "rust-analyzer.debugSingle" + "rust-analyzer.showReferences" + ;; "rust-analyzer.gotoLocation" + "rust-analyzer.triggerParameterHints" + ;; "rust-analyzer.rename" + ])))))) :download-server-fn (lambda (_client callback error-callback _update?) (lsp-package-ensure 'rust-analyzer callback error-callback)))) diff --git a/clients/lsp-sqls.el b/clients/lsp-sqls.el index e60f69b7b4d..84b6e877bf4 100644 --- a/clients/lsp-sqls.el +++ b/clients/lsp-sqls.el @@ -29,7 +29,7 @@ (defgroup lsp-sqls nil "LSP support for SQL, using sqls." :group 'lsp-mode - :link '(url-link "https://github.com/lighttiger2505/sqls") + :link '(url-link "https://github.com/sqls-server/sqls") :package-version `(lsp-mode . "7.0")) (defcustom lsp-sqls-server "sqls" @@ -143,6 +143,14 @@ use the current region if set, otherwise the entire buffer." "workspace/executeCommand" (list :command "showConnections" :timeout lsp-sqls-timeout)))) +(defun lsp-sql-show-tables (&optional _command) + "Show tables." + (interactive) + (lsp-sqls--show-results + (lsp-request + "workspace/executeCommand" + (list :command "showTables" :timeout lsp-sqls-timeout)))) + (defun lsp-sql-switch-database (&optional _command) "Switch database." (interactive) @@ -176,6 +184,7 @@ use the current region if set, otherwise the entire buffer." ("showDatabases" #'lsp-sql-show-databases) ("showSchemas" #'lsp-sql-show-schemas) ("showConnections" #'lsp-sql-show-connections) + ("showTables" #'lsp-sql-show-tables) ("switchDatabase" #'lsp-sql-switch-database) ("switchConnections" #'lsp-sql-switch-connection)) :server-id 'sqls diff --git a/clients/lsp-tex.el b/clients/lsp-tex.el index ca6f475837e..31c47016862 100644 --- a/clients/lsp-tex.el +++ b/clients/lsp-tex.el @@ -46,7 +46,7 @@ (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection lsp-clients-digestif-executable) - :major-modes '(plain-tex-mode latex-mode context-mode texinfo-mode LaTex-mode) + :major-modes '(plain-tex-mode latex-mode context-mode texinfo-mode LaTeX-mode) :priority (if (eq lsp-tex-server 'digestif) 1 -1) :server-id 'digestif)) diff --git a/clients/lsp-yaml.el b/clients/lsp-yaml.el index 24db7e511eb..376cac7bae8 100644 --- a/clients/lsp-yaml.el +++ b/clients/lsp-yaml.el @@ -172,11 +172,16 @@ Limited for performance reasons." (lsp-package-ensure 'yaml-language-server callback error-callback)))) -(defconst lsp-yaml--built-in-kubernetes-schema - '((name . "Kubernetes") - (description . "Built-in kubernetes manifest schema definition") - (url . "kubernetes") - (fileMatch . ["*-k8s.yaml" "*-k8s.yml"]))) +(defcustom lsp-yaml-schema-extensions '(((name . "Kubernetes v1.30.3") + (description . "Kubernetes v1.30.3 manifest schema definition") + (url . "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.30.3-standalone-strict/all.json") + (fileMatch . ["*-k8s.yaml" "*-k8s.yml"]))) + "User defined schemas that extend default schema store. +Used in `lsp-yaml--get-supported-schemas' to supplement schemas provided by +`lsp-yaml-schema-store-uri'." + :type 'list + :group 'lsp-yaml + :package-version '(lsp-mode . "9.0.1")) (defun lsp-yaml-download-schema-store-db (&optional force-downloading) "Download remote schema store at `lsp-yaml-schema-store-uri' into local cache. @@ -194,7 +199,7 @@ Set FORCE-DOWNLOADING to non-nil to force re-download the database." (lsp-yaml-download-schema-store-db) (setq lsp-yaml--schema-store-schemas-alist (alist-get 'schemas (json-read-file lsp-yaml-schema-store-local-db)))) - (seq-concatenate 'list (list lsp-yaml--built-in-kubernetes-schema) lsp-yaml--schema-store-schemas-alist)) + (seq-concatenate 'list lsp-yaml-schema-extensions lsp-yaml--schema-store-schemas-alist)) (defun lsp-yaml-set-buffer-schema (uri-string) "Set yaml schema for the current buffer to URI-STRING." diff --git a/docs/lsp-clients.json b/docs/lsp-clients.json index 5c2af6b8397..1581095c2e9 100644 --- a/docs/lsp-clients.json +++ b/docs/lsp-clients.json @@ -135,6 +135,16 @@ "lsp-install-server": "omnisharp", "debugger": "Yes (netcoredbg)" }, + { + "name": "csharp-roslyn", + "common-group-name": "csharp", + "full-name": "C# (csharp-roslyn)", + "server-name": "Microsoft.CodeAnalysis.LanguageServer (Roslyn)", + "server-url": "https://github.com/dotnet/roslyn/tree/main/src/LanguageServer", + "installation": "Supports automatic installation via NuGet", + "lsp-install-server": "csharp-roslyn", + "debugger": "Yes (netcoredbg)" + }, { "name": "ccls", "full-name": "C++", @@ -248,6 +258,15 @@ "lsp-install-server": "dot-ls", "debugger": "Not available" }, + { + "name": "earthly", + "full-name": "Earthfile language server", + "server-name": "earthlyls", + "server-url": "https://github.com/glehmann/earthlyls", + "installation": "cargo install earthlyls", + "lsp-install-server": "earthlyls", + "debugger": "Not available" + }, { "name": "elixir", "full-name": "Elixir", @@ -315,6 +334,14 @@ "installation": "pip install fortls", "debugger": "Yes" }, + { + "name": "futhark", + "full-name": "Futhark", + "server-name": "futhark-lsp", + "server-url": "https://github.com/diku-dk/futhark", + "installation": "A part of the compiler since 0.21.9", + "debugger": "Not available" + }, { "name": "gdscript", "full-name": "GDScript", @@ -543,8 +570,8 @@ "common-group-name": "lua", "full-name": "Lua", "server-name": "lua-language-server", - "installation-url": "https://github.com/sumneko/lua-language-server/wiki/Build-and-Run-(Standalone)", - "server-url": "https://github.com/sumneko/lua-language-server", + "installation-url": "https://github.com/LuaLS/lua-language-server/wiki/Getting-Started#build", + "server-url": "https://github.com/LuaLS/lua-language-server", "debugger": "Not available" }, { @@ -922,11 +949,11 @@ "debugger": "Not available" }, { - "name": "ruff-lsp", + "name": "ruff", "full-name": "Python", - "server-name": "ruff-lsp", - "server-url": "https://github.com/charliermarsh/ruff-lsp", - "installation": "pip install ruff-lsp", + "server-name": "ruff", + "server-url": "https://github.com/astral-sh/ruff", + "installation": "pip install ruff (previous pip install ruff-lsp)", "debugger": "Not available" }, { @@ -1010,8 +1037,8 @@ "name": "sqls", "full-name": "SQL (sqls)", "server-name": "sqls", - "server-url": "https://github.com/lighttiger2505/sqls", - "installation": "go install github.com/lighttiger2505/sqls@latest", + "server-url": "https://github.com/sqls-server/sqls", + "installation": "go install github.com/sqls-server/sqls@latest", "debugger": "Not available" }, { diff --git a/docs/manual-language-docs/lsp-sqls.md b/docs/manual-language-docs/lsp-sqls.md index b8b2b6c410d..54f7f5474a1 100644 --- a/docs/manual-language-docs/lsp-sqls.md +++ b/docs/manual-language-docs/lsp-sqls.md @@ -14,7 +14,10 @@ root_file: docs/manual-language-docs/lsp-sqls.md ``` -Alternatively, you can leave `lsp-sqls-workspace-config-path` to the default "workspace" value, and put a json file in `/.sqls/config.json` containing +## Storing Configuration in `/.sqls/config.json` + +Alternatively, you can store your configuration in the project root at `/.sqls/config.json`: + ``` { "sqls": { @@ -29,4 +32,29 @@ Alternatively, you can leave `lsp-sqls-workspace-config-path` to the default "wo } ``` -Now lsp should start in sql-mode buffers, and you can pick a server connection with `M-x lsp-execute-code-action` and "Switch Connections" (or directly with `M-x lsp-sql-switch-connection`). You can change database with `M-x lsp-execute-code-action` and "Switch Database" (or `M-x lsp-sql-switch-database`). +In this case, you need to set `lsp-sqls-workspace-config-path` to "root": + +```emacs-lisp +(setq lsp-sqls-workspace-config-path "root") +``` + +## Storing Configuration in the Current Directory + +If you want to configure it for the current directory, you can create a `.sqls/config.json` file: + +``` +.sqls/config.json +target.sql +``` + +For this setup, ensure that `lsp-sqls-workspace-config-path` is set to "workspace": + +```emacs-lisp +(setq lsp-sqls-workspace-config-path "workspace") +``` + +# Switching Connections and Databases + +Now, lsp should start in sql-mode buffers. You can choose a server connection using `M-x lsp-execute-code-action` and then selecting "Switch Connections", or directly with `M-x lsp-sql-switch-connection`. + +To change the database, use `M-x lsp-execute-code-action` and select "Switch Database" (or `M-x lsp-sql-switch-database`). diff --git a/lsp-completion.el b/lsp-completion.el index 0f2be895444..dab10513fd5 100644 --- a/lsp-completion.el +++ b/lsp-completion.el @@ -575,8 +575,8 @@ Others: CANDIDATES" (apply #'delete-region markers) (insert prefix) (pcase text-edit? - ((TextEdit) (lsp--apply-text-edit text-edit?)) - ((InsertReplaceEdit :insert :replace :new-text) + ((lsp-interface TextEdit) (lsp--apply-text-edit text-edit?)) + ((lsp-interface InsertReplaceEdit :insert :replace :new-text) (lsp--apply-text-edit (lsp-make-text-edit :new-text new-text diff --git a/lsp-mode.el b/lsp-mode.el index b85de1cde20..2266d9b240a 100644 --- a/lsp-mode.el +++ b/lsp-mode.el @@ -177,16 +177,16 @@ As defined by the Language Server Protocol 3.16." lsp-autotools lsp-awk lsp-bash lsp-beancount lsp-bufls lsp-clangd lsp-clojure lsp-cmake lsp-cobol lsp-credo lsp-crystal lsp-csharp lsp-css lsp-cucumber lsp-cypher lsp-d lsp-dart lsp-dhall lsp-docker lsp-dockerfile - lsp-elixir lsp-elm lsp-emmet lsp-erlang lsp-eslint lsp-fortran lsp-fsharp - lsp-gdscript lsp-gleam lsp-glsl lsp-go lsp-golangci-lint lsp-grammarly + lsp-earthly lsp-elixir lsp-elm lsp-emmet lsp-erlang lsp-eslint lsp-fortran lsp-futhark + lsp-fsharp lsp-gdscript lsp-gleam lsp-glsl lsp-go lsp-golangci-lint lsp-grammarly lsp-graphql lsp-groovy lsp-hack lsp-haskell lsp-haxe lsp-idris lsp-java lsp-javascript lsp-jq lsp-json lsp-kotlin lsp-latex lsp-lisp lsp-ltex lsp-lua lsp-magik lsp-markdown lsp-marksman lsp-mdx lsp-meson lsp-metals lsp-mint lsp-mojo lsp-move lsp-mssql lsp-nginx lsp-nim lsp-nix lsp-nushell lsp-ocaml lsp-openscad lsp-pascal lsp-perl lsp-perlnavigator lsp-php lsp-pls lsp-purescript lsp-pwsh lsp-pyls lsp-pylsp lsp-pyright lsp-python-ms - lsp-qml lsp-r lsp-racket lsp-remark lsp-rf lsp-rubocop lsp-ruby-lsp - lsp-ruby-syntax-tree lsp-ruff-lsp lsp-rust lsp-semgrep lsp-shader + lsp-qml lsp-r lsp-racket lsp-remark lsp-rf lsp-roslyn lsp-rubocop lsp-ruby-lsp + lsp-ruby-syntax-tree lsp-ruff lsp-rust lsp-semgrep lsp-shader lsp-solargraph lsp-solidity lsp-sonarlint lsp-sorbet lsp-sourcekit lsp-sql lsp-sqls lsp-steep lsp-svelte lsp-tailwindcss lsp-terraform lsp-tex lsp-tilt lsp-toml lsp-trunk lsp-ttcn3 lsp-typeprof lsp-v @@ -769,6 +769,7 @@ Changes take effect only when a new session is started." ("\\.cs\\'" . "csharp") ("\\.css$" . "css") ("\\.cypher$" . "cypher") + ("Earthfile" . "earthfile") ("\\.ebuild$" . "shellscript") ("\\.go\\'" . "go") ("\\.html$" . "html") @@ -870,6 +871,7 @@ Changes take effect only when a new session is started." (go-ts-mode . "go") (graphql-mode . "graphql") (haskell-mode . "haskell") + (haskell-ts-mode . "haskell") (hack-mode . "hack") (php-mode . "php") (php-ts-mode . "php") @@ -891,6 +893,7 @@ Changes take effect only when a new session is started." (reason-mode . "reason") (caml-mode . "ocaml") (tuareg-mode . "ocaml") + (futhark-mode . "futhark") (swift-mode . "swift") (elixir-mode . "elixir") (elixir-ts-mode . "elixir") @@ -901,6 +904,7 @@ Changes take effect only when a new session is started." (ruby-mode . "ruby") (enh-ruby-mode . "ruby") (ruby-ts-mode . "ruby") + (feature-mode . "cucumber") (fortran-mode . "fortran") (f90-mode . "fortran") (elm-mode . "elm") @@ -936,7 +940,7 @@ Changes take effect only when a new session is started." (robot-mode . "robot") (racket-mode . "racket") (nix-mode . "nix") - (nix-ts-mode . "Nix") + (nix-ts-mode . "nix") (prolog-mode . "prolog") (vala-mode . "vala") (actionscript-mode . "actionscript") @@ -956,6 +960,7 @@ Changes take effect only when a new session is started." (idris-mode . "idris") (idris2-mode . "idris2") (gleam-mode . "gleam") + (gleam-ts-mode . "gleam") (graphviz-dot-mode . "dot") (tiltfile-mode . "tiltfile") (solidity-mode . "solidity") @@ -1032,6 +1037,7 @@ directory") ("textDocument/signatureHelp" :capability :signatureHelpProvider) ("textDocument/typeDefinition" :capability :typeDefinitionProvider) ("textDocument/typeHierarchy" :capability :typeHierarchyProvider) + ("textDocument/diagnostic" :capability :diagnosticProvider) ("workspace/executeCommand" :capability :executeCommandProvider) ("workspace/symbol" :capability :workspaceSymbolProvider)) @@ -1499,6 +1505,7 @@ INHERIT-INPUT-METHOD will be proxied to `completing-read' without changes." (_ 'x64))) ('gnu/linux (pcase system-configuration + ((rx bol "aarch64-") 'arm64) ((rx bol "x86_64") 'x64) ((rx bol (| "i386" "i886")) 'x32))) (_ @@ -2296,6 +2303,17 @@ Common usecase are: The result format is vector [_ errors warnings infos hints] or nil." (gethash (lsp--fix-path-casing path) lsp-diagnostic-stats)) +(defun lsp-diagnostics--request-pull-diagnostics (workspace) + "Request new diagnostics for the current file within WORKSPACE. +This is only executed if the server supports pull diagnostics." + (when (lsp-feature? "textDocument/diagnostic") + (let ((path (lsp--fix-path-casing (buffer-file-name)))) + (lsp-request-async "textDocument/diagnostic" + (list :textDocument (lsp--text-document-identifier)) + (-lambda ((&DocumentDiagnosticReport :kind :items?)) + (lsp-diagnostics--apply-pull-diagnostics workspace path kind items?)) + :mode 'tick)))) + (defun lsp-diagnostics--update-path (path new-stats) (let ((new-stats (copy-sequence new-stats)) (path (lsp--fix-path-casing (directory-file-name path)))) @@ -2305,9 +2323,8 @@ The result format is vector [_ errors warnings infos hints] or nil." (aref new-stats idx))) (puthash path new-stats lsp-diagnostic-stats)))) -(lsp-defun lsp--on-diagnostics-update-stats (workspace - (&PublishDiagnosticsParams :uri :diagnostics)) - (let ((path (lsp--fix-path-casing (lsp--uri-to-path uri))) +(defun lsp-diagnostics--convert-and-update-path-stats (workspace path diagnostics) + (let ((path (lsp--fix-path-casing path)) (new-stats (make-vector 5 0))) (mapc (-lambda ((&Diagnostic :severity?)) (cl-incf (aref new-stats (or severity? 1)))) @@ -2321,6 +2338,27 @@ The result format is vector [_ errors warnings infos hints] or nil." (directory-file-name path))))) (lsp-diagnostics--update-path path new-stats)))) +(lsp-defun lsp--on-diagnostics-update-stats (workspace + (&PublishDiagnosticsParams :uri :diagnostics)) + (lsp-diagnostics--convert-and-update-path-stats workspace (lsp--uri-to-path uri) diagnostics)) + +(defun lsp-diagnostics--apply-pull-diagnostics (workspace path kind diagnostics?) + "Update WORKSPACE diagnostics at PATH with DIAGNOSTICS?. +Depends on KIND being a \\='full\\=' update." + (cond + ((equal kind "full") + ;; TODO support `lsp-diagnostic-filter' + ;; (the params types differ from the published diagnostics response) + (lsp-diagnostics--convert-and-update-path-stats workspace path diagnostics?) + (-let* ((lsp--virtual-buffer-mappings (ht)) + (workspace-diagnostics (lsp--workspace-diagnostics workspace))) + (if (seq-empty-p diagnostics?) + (remhash path workspace-diagnostics) + (puthash path (append diagnostics? nil) workspace-diagnostics)) + (run-hooks 'lsp-diagnostics-updated-hook))) + ((equal kind "unchanged") t) + (t (lsp--error "Unknown pull diagnostic result kind '%s'" kind)))) + (defun lsp--on-diagnostics (workspace params) "Callback for textDocument/publishDiagnostics. interface PublishDiagnosticsParams { @@ -3741,6 +3779,8 @@ disappearing, unset all the variables related to it." (publishDiagnostics . ((relatedInformation . t) (tagSupport . ((valueSet . [1 2]))) (versionSupport . t))) + (diagnostic . ((dynamicRegistration . :json-false) + (relatedDocumentSupport . :json-false))) (linkedEditingRange . ((dynamicRegistration . t))))) (window . ((workDoneProgress . t) (showDocument . ((support . t)))))) @@ -4082,7 +4122,8 @@ yet." (cond ((and (or (equal lsp-signature-auto-activate t) (memq :on-trigger-char lsp-signature-auto-activate)) - signature-help-handler) + signature-help-handler + (not cleanup?)) (add-hook 'post-self-insert-hook signature-help-handler nil t)) ((or cleanup? @@ -4262,6 +4303,8 @@ yet." (lsp-managed-mode 1) + (lsp-diagnostics--request-pull-diagnostics lsp--cur-workspace) + (run-hooks 'lsp-after-open-hook) (when-let ((client (-some-> lsp--cur-workspace (lsp--workspace-client)))) (-some-> (lsp--client-after-open-fn client) @@ -4806,7 +4849,8 @@ Added to `after-change-functions'." (with-lsp-workspace workspace (lsp-notify "textDocument/didChange" (list :contentChanges (vector (lsp--full-change-event)) - :textDocument (lsp--versioned-text-document-identifier)))))) + :textDocument (lsp--versioned-text-document-identifier))) + (lsp-diagnostics--request-pull-diagnostics workspace)))) (2 (with-lsp-workspace workspace (lsp-notify @@ -4816,7 +4860,8 @@ Added to `after-change-functions'." (if content-change-event-fn (funcall content-change-event-fn start end length) (lsp--text-document-content-change-event - start end length))))))))) + start end length))))) + (lsp-diagnostics--request-pull-diagnostics workspace))))) (lsp-workspaces)) (when lsp--delay-timer (cancel-timer lsp--delay-timer)) (setq lsp--delay-timer (run-with-idle-timer @@ -4825,14 +4870,7 @@ Added to `after-change-functions'." #'lsp--flush-delayed-changes)) ;; force cleanup overlays after each change (lsp--remove-overlays 'lsp-highlight) - (lsp--after-change (current-buffer)) - (setq lsp--signature-last-index nil - lsp--signature-last nil) - ;; cleanup diagnostics - (when lsp-diagnostic-clean-after-change - (lsp-foreach-workspace - (-let [diagnostics (lsp--workspace-diagnostics lsp--cur-workspace)] - (remhash (lsp--fix-path-casing (buffer-file-name)) diagnostics)))))))) + (lsp--after-change (current-buffer)))))) @@ -4889,6 +4927,16 @@ Added to `after-change-functions'." (run-hooks 'lsp-on-change-hook))) (defun lsp--after-change (buffer) + "Called after most textDocument/didChange events." + (setq lsp--signature-last-index nil + lsp--signature-last nil) + + ;; cleanup diagnostics + (when lsp-diagnostic-clean-after-change + (dolist (workspace (lsp-workspaces)) + (-let [diagnostics (lsp--workspace-diagnostics workspace)] + (remhash (lsp--fix-path-casing (buffer-file-name)) diagnostics)))) + (when (fboundp 'lsp--semantic-tokens-refresh-if-enabled) (lsp--semantic-tokens-refresh-if-enabled buffer)) (when lsp--on-change-timer @@ -5198,11 +5246,11 @@ identifier and the position respectively." type Location, LocationLink, Location[] or LocationLink[]." (setq locations (pcase locations - ((seq (or (Location) - (LocationLink))) + ((seq (or (lsp-interface Location) + (lsp-interface LocationLink))) (append locations nil)) - ((or (Location) - (LocationLink)) + ((or (lsp-interface Location) + (lsp-interface LocationLink)) (list locations)))) (cl-labels ((get-xrefs-in-file @@ -5569,9 +5617,9 @@ When language is nil render as markup if `markdown-mode' is loaded." (let ((inhibit-message t)) (or (pcase content - ((MarkedString :value :language) + ((lsp-interface MarkedString :value :language) (lsp--render-string value language)) - ((MarkupContent :value :kind) + ((lsp-interface MarkupContent :value :kind) (lsp--render-string value kind)) ;; plain string ((pred stringp) (lsp--render-string content "markdown")) @@ -6027,7 +6075,7 @@ in place, based on the BOOLEAN-ACTION-ARGUMENTS list. The values in this list can be either symbols or lists of symbols that represent paths to boolean arguments in code actions: -> (lsp-fix-code-action-booleans command '(:foo :bar (:some :nested :boolean))) +> (lsp-fix-code-action-booleans command `(:foo :bar (:some :nested :boolean))) When there are available code actions, the server sends `lsp-mode' a list of possible command names and arguments as @@ -6215,15 +6263,15 @@ A reference is highlighted only if it is visible in a window." (-map (-lambda ((start-window . end-window)) ;; Make the overlay only if the reference is visible - (let ((start-point (lsp--position-to-point start)) - (end-point (lsp--position-to-point end))) - (when (and (> (1+ start-line) start-window) - (< (1+ end-line) end-window) - (not (and lsp-symbol-highlighting-skip-current - (<= start-point (point) end-point)))) - (-doto (make-overlay start-point end-point) - (overlay-put 'face (cdr (assq (or kind? 1) lsp--highlight-kind-face))) - (overlay-put 'lsp-highlight t))))) + (when (and (> (1+ start-line) start-window) + (< (1+ end-line) end-window)) + (let ((start-point (lsp--position-to-point start)) + (end-point (lsp--position-to-point end))) + (when (not (and lsp-symbol-highlighting-skip-current + (<= start-point (point) end-point))) + (-doto (make-overlay start-point end-point) + (overlay-put 'face (cdr (assq (or kind? 1) lsp--highlight-kind-face))) + (overlay-put 'lsp-highlight t)))))) wins-visible-pos)) highlights))) @@ -6361,11 +6409,11 @@ perform the request synchronously." (-mapcat (-lambda (sym) (pcase-exhaustive sym - ((DocumentSymbol :name :children? :selection-range (Range :start)) + ((lsp-interface DocumentSymbol :name :children? :selection-range (lsp-interface Range :start)) (cons (cons (concat path name) (lsp--position-to-point start)) (lsp--xref-elements-index children? (concat path name " / ")))) - ((SymbolInformation :name :location (Location :range (Range :start))) + ((lsp-interface SymbolInformation :name :location (lsp-interface Location :range (lsp-interface Range :start))) (list (cons (concat path name) (lsp--position-to-point start)))))) symbols)) @@ -6698,13 +6746,24 @@ textDocument/didOpen for the new file." (advice-add 'set-visited-file-name :around #'lsp--on-set-visited-file-name) -(defvar lsp--flushing-delayed-changes nil) +(defcustom lsp-flush-delayed-changes-before-next-message t + "If non-nil send the document changes update before sending other messages. + +If nil, and `lsp-debounce-full-sync-notifications' is non-nil, + change notifications will be throttled by + `lsp-debounce-full-sync-notifications-interval' regardless of + other messages." + :group 'lsp-mode + :type 'boolean) + +(defvar lsp--not-flushing-delayed-changes t) (defun lsp--send-no-wait (message proc) "Send MESSAGE to PROC without waiting for further output." - (unless lsp--flushing-delayed-changes - (let ((lsp--flushing-delayed-changes t)) + (when (and lsp--not-flushing-delayed-changes + lsp-flush-delayed-changes-before-next-message) + (let ((lsp--not-flushing-delayed-changes nil)) (lsp--flush-delayed-changes))) (lsp-process-send proc message)) @@ -8635,16 +8694,24 @@ TBL - a hash table, PATHS is the path to the nested VALUE." "Defines `lsp-mode' server property." (declare (doc-string 3) (debug (name body)) (indent defun)) - (let ((path (plist-get args :lsp-path))) + (let ((path (plist-get args :lsp-path)) + (setter (intern (concat (symbol-name symbol) "--set")))) (cl-remf args :lsp-path) `(progn (lsp-register-custom-settings (quote ((,path ,symbol ,(equal ''boolean (plist-get args :type)))))) - (defcustom ,symbol ,standard ,doc - :set (lambda (sym val) - (lsp--set-custom-property sym val ,path)) - ,@args)))) + (defcustom ,symbol ,standard ,doc ,@args) + + ;; Use a variable watcher instead of registering a `defcustom' + ;; setter since `hack-local-variables' is not aware of custom + ;; setters and won't invoke them. + + (defun ,setter (sym val op _where) + (when (eq op 'set) + (lsp--set-custom-property sym val ,path))) + + (add-variable-watcher ',symbol #',setter)))) (defun lsp--set-custom-property (sym val path) (set sym val) diff --git a/lsp-protocol.el b/lsp-protocol.el index 034107a5b32..f3b8b6cd821 100644 --- a/lsp-protocol.el +++ b/lsp-protocol.el @@ -129,7 +129,7 @@ Allowed params: %s" interface (reverse (-map #'cl-first params))) $$result)) (-partition 2 plist)) $$result))) - `(pcase-defmacro ,interface (&rest property-bindings) + `(cl-defun ,(intern (format "lsp--pcase-macroexpander-%s" interface)) (&rest property-bindings) ,(if lsp-use-plists ``(and (pred listp) @@ -246,6 +246,25 @@ Allowed params: %s" interface (reverse (-map #'cl-first params))) (apply #'append) (cl-list* 'progn)))) +(pcase-defmacro lsp-interface (interface &rest property-bindings) + "If EXPVAL is an instance of INTERFACE, destructure it by matching its +properties. EXPVAL should be a plist or hash table depending on the variable +`lsp-use-plists'. + +INTERFACE should be an LSP interface defined with `lsp-interface'. This form +will not match if any of INTERFACE's required fields are missing in EXPVAL. + +Each :PROPERTY keyword matches a field in EXPVAL. The keyword may be followed by +an optional PATTERN, which is a `pcase' pattern to apply to the field's value. +Otherwise, PROPERTY is let-bound to the field's value. + +\(fn INTERFACE [:PROPERTY [PATTERN]]...)" + (cl-check-type interface symbol) + (let ((lsp-pcase-macroexpander + (intern (format "lsp--pcase-macroexpander-%s" interface)))) + (cl-assert (fboundp lsp-pcase-macroexpander) "not a known LSP interface: %s" interface) + (apply lsp-pcase-macroexpander property-bindings))) + (if lsp-use-plists (progn (defun lsp-get (from key) @@ -603,10 +622,15 @@ See `-let' for a description of the destructuring mechanism." (DefinitionCapabilities nil (:dynamicRegistration :linkSupport)) (DeleteFileOptions nil (:ignoreIfNotExists :recursive)) (Diagnostic (:range :message) (:code :relatedInformation :severity :source :tags)) + (DiagnosticClientCapabilities nil (:dynamicRegistration :relatedDocumentSupport)) + (DiagnosticOptions (:interFileDependencies :workspaceDiagnostics) (:identifier)) (DiagnosticRelatedInformation (:location :message) nil) + (DiagnosticServerCancellationData (:retriggerRequest) nil) (DiagnosticsTagSupport (:valueSet) nil) (DidChangeConfigurationCapabilities nil (:dynamicRegistration)) (DidChangeWatchedFilesCapabilities nil (:dynamicRegistration)) + (DocumentDiagnosticParams (:textDocument) (:identifier :previousResultId)) + (DocumentDiagnosticReport (:kind) (:resultId :items :relatedDocuments)) (DocumentFilter nil (:language :pattern :scheme)) (DocumentHighlightCapabilities nil (:dynamicRegistration)) (DocumentLinkCapabilities nil (:dynamicRegistration :tooltipSupport)) diff --git a/mkdocs.yml b/mkdocs.yml index d24b22498f4..6a80cea7040 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,6 +59,7 @@ nav: - C++ (ccls): page/lsp-ccls.md - C++ (clangd): page/lsp-clangd.md - C# (omnisharp-roslyn): page/lsp-csharp-omnisharp.md + - C# (csharp-roslyn): page/lsp-csharp-roslyn.md - C# (csharp-ls): page/lsp-csharp-ls.md - Clojure: page/lsp-clojure.md - CMake: page/lsp-cmake.md @@ -71,6 +72,7 @@ nav: - Dart: https://emacs-lsp.github.io/lsp-dart - Dhall: page/lsp-dhall.md - Dockerfile: page/lsp-dockerfile.md + - Earthfile: page/lsp-earthly.md - Elixir: page/lsp-elixir.md - Elm: page/lsp-elm.md - Emmet: page/lsp-emmet.md @@ -78,6 +80,7 @@ nav: - ESLint: page/lsp-eslint.md - F#: page/lsp-fsharp.md - Fortran: page/lsp-fortran.md + - Futhark: page/lsp-futhark.md - GDScript: page/lsp-gdscript.md - Gleam: page/lsp-gleam.md - GLSL: page/lsp-glsl.md @@ -136,7 +139,7 @@ nav: - Python (Palantir deprecated): page/lsp-pyls.md - Python (Pyright): https://emacs-lsp.github.io/lsp-pyright - Python (Microsoft): https://emacs-lsp.github.io/lsp-python-ms - - Python (Ruff): page/lsp-ruff-lsp.md + - Python (Ruff): page/lsp-ruff.md - QML: page/lsp-qml.md - R: page/lsp-r.md - Racket (jeapostrophe): page/lsp-racket-langserver.md diff --git a/test/fixtures/SamplesForMock/sample.txt b/test/fixtures/SamplesForMock/sample.txt new file mode 100644 index 00000000000..c2e6b1ec302 --- /dev/null +++ b/test/fixtures/SamplesForMock/sample.txt @@ -0,0 +1,4 @@ +Line 0 unique word fegam and common +line 1 unique word broming + common +line 2 unique word normalw common here +line 3 words here and here diff --git a/test/lsp-mock-server-test.el b/test/lsp-mock-server-test.el new file mode 100644 index 00000000000..7d3323c7d75 --- /dev/null +++ b/test/lsp-mock-server-test.el @@ -0,0 +1,867 @@ +;;; lsp-mock-server-test.el --- Unit test utilities -*- lexical-binding: t -*- + +;; Copyright (C) 2024-2024 emacs-lsp maintainers + +;; Author: Arseniy Zaostrovnykh +;; Package-Requires: ((emacs "27.1")) +;; Version: 0.0.1 +;; License: GPL-3.0-or-later + +;; URL: https://github.com/emacs-lsp/lsp-mode +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; A collection of tests that check lsp-mode interaction with +;; an lsp server mocked by mock-lsp-server.el +;; The tests define a custom lsp client execute scenarios +;; such as opening a file, chagning it, or receiving updated diagnostics +;; and assert how lsp-mode updates the diagnostics. + +;;; Code: + +(require 'seq) +(require 'lsp-mode) + +;; Taken from lsp-integration-tests.el +(defconst lsp-test-location (file-name-directory (or load-file-name buffer-file-name)) + "Directory of the tests containing mock server and fixtures.") + +(defconst lsp-test-mock-server-location + (expand-file-name "mock-lsp-server.el" lsp-test-location) + "Path to the mock server script.") + +(defconst lsp-test-mock-server-command-file + (expand-file-name "mock-server-commands.el" lsp-test-location) + "File mock server reads commands from.") + +(defconst lsp-test-sample-file + (f-join lsp-test-location "fixtures/SamplesForMock/sample.txt") + "The sample file used to conduct tests upon.") + +(defun lsp-test-send-command-to-mock-server (command) + "Pass the given COMMAND to the mock server. + +It uses the pre-configured file to write command to, then sends a +notification to the server so that it looks into the file and +executes the command." + ;; Can run only one command at a time + (should (not (file-exists-p lsp-test-mock-server-command-file))) + (write-region command nil lsp-test-mock-server-command-file nil nil nil 'excl) + ;; Nudge the server to find and execute the command + (lsp-notify "$/setTrace" '(:value "messages"))) + +(defun register-mock-client () + "Register mock client that spawns the mock server." + (lsp-register-client + (make-lsp-client + :new-connection (lsp-stdio-connection + `("emacs" "--script" ,lsp-test-mock-server-location)) + :major-modes '(prog-mode) + :priority 100 + :server-id 'mock-server))) + +(defun lsp-test-total-folder-count () + "Count total number of active root folders in the session." + (hash-table-count (lsp-session-folder->servers (lsp-session)))) + +(defun lsp-test--find-line (file-content line) + "Find LINE in the multi-line FILE-CONTENT string." + (let ((lines (split-string file-content "\n")) + (line-number 0) + (found nil)) + (while (and lines (not found)) + (when (string= (car lines) line) + (setq found line-number)) + (setq lines (cdr lines)) + (setq line-number (1+ line-number))) + (when (not found) + (error "Line %s not found" line)) + found)) + +(defun lsp-test-range-make (file-content line marker) + "Create a single-line diagnostics range summary. + +Find LINE in FILE-CONTENT and take that as the line number. +Set the :from and :to characters to reflect the position of +`^^^^' in the MARKER. + +Example (suppose line #3 of current buffer is \"full line\"): + +(lsp-test-range-make (buffer-string) + \"full line\" + \" ^^^^\") + +-> (:line 3 :from 5 :to 8) +" + (let ((line-number (lsp-test--find-line file-content line))) + (should-not (null line-number)) + (should (eq (length marker) (length line))) + (should (string-match "^ *\\(\\^+\\) *$" marker)) + (list :line line-number :from (match-beginning 1) :to (match-end 1)))) + +(defun lsp-test-full-range (short-range) + "Convert SHORT-RANGE to a full range. + +SHORT-RANGE is a p-list with :line, :from, and :to keys. +Returns a full range p-list with :start and :end keys." + (list :start (list :line (plist-get short-range :line) + :character (plist-get short-range :from)) + :end (list :line (plist-get short-range :line) + :character (plist-get short-range :to)))) + +(defun lsp-test-diag-get (diagnostic) + "Get the single-line diagnostics range summary of DIAGNOSTIC. + +DIAGNOSTIC must have a single-line range. +Returns its range converted to `(:line .. :from .. :to ..)' format." + (let* ((range (ht-get diagnostic "range")) + (start (ht-get range "start")) + (end (ht-get range "end"))) + (should (eq (ht-get start "line") (ht-get end "line"))) + (list :line (ht-get start "line") + :from (ht-get start "character") + :to (ht-get end "character")))) + +(defun lsp-test-find-all-words (contents word) + "Find all occurences of WORD in CONTENTS and return a list of ranges." + (with-temp-buffer + (insert contents) + (goto-char (point-min)) + (let (locs) + (while (re-search-forward word nil t) + (let ((line (- (line-number-at-pos (point)) 1)) + (end-col (current-column)) + (start-col (- (current-column) (length word)))) + (push (list :start (list :line line :character start-col) + :end (list :line line :character end-col)) + locs))) + locs))) + +(defun lsp-test-make-diagnostics (for-file contents forbidden-word) + "Come up with a diagnostic highlighting FORBIDDEN-WORD. + +Scan CONTENTS for FORBIDDEN-WORD and produce diagnostics for each occurence. +Returns a p-list compatible with the mock server. + +FOR-FILE is the path to the file to include in the diagnostics. +It might not contain exactly CONTENTS because it the diagnostic +might be generated for a modified and not saved buffer content." + (let ((diagnostics (mapcar (lambda (loc) + (list :source "mockS" + :code "E001" + :range loc + :message (format "Do not use word '%s'" forbidden-word) + :severity 2)) + (lsp-test-find-all-words contents forbidden-word)))) + ;; Use vconcat diagnostics to ensure proper JSON serialization of the list + `(:uri ,(concat "file://" for-file) :diagnostics ,(vconcat diagnostics)))) + +(defun lsp-test-command-send-diags (file-path file-contents forbidden-word) + "Generate and command the mock server to publish diagnostics. + +Command the mock server to publish diagnostics highlighting every occurence of +FORBIDDEN-WORD in FILE-CONTENTS that corresponds to FILE-PATH." + (let ((diags (lsp-test-make-diagnostics file-path file-contents forbidden-word))) + (lsp-test-send-command-to-mock-server + (format "(publish-diagnostics '%s)" + (prin1-to-string diags))))) + +(defun lsp-test-crash-server-with-message (message) + "Command the mock server to crash with MESSAGE." + (lsp-test-send-command-to-mock-server (format "(error %S)" message))) + +;; I could not figure out how to use lsp-test-wait safely +;; (e.g., aborting it after a failed test), so I use a simpler +;; version. +(defun lsp-test--sync-wait-for (condition-func) + "Synchronously waiting for CONDITION-FUNC to return non-nil. + +Returns the non-nil return value of CONDITION-FUNC." + (let ((result (funcall condition-func))) + (while (not result) + (sleep-for 0.05) + (setq result (funcall condition-func))) + result)) + +(defmacro lsp-test-sync-wait (condition) + "Wait for the CONDITION to become non-nil and return it." + `(lsp-test--sync-wait-for (lambda () ,condition))) + +(defun lsp-mock--run-with-mock-server (test-body) + "Run TEST-BODY function with mock LSP client connected to the mock server. + +This is an environment function that configures lsp-mode, mock lsp-client, +opens the `lsp-test-sample-file' and starts the mock server." + (let ((lsp-clients (lsp-ht)) ; clear all clients + (lsp-diagnostics-provider :none) ; focus on LSP itself, not its UI integration + (lsp-restart 'ignore) ; Avoid restarting the server or prompting user on a crash + (lsp-enable-snippet nil) ; Avoid warning that lsp-yasnippet is not intalled + (lsp-warn-no-matched-clients nil) ; Mute warning LSP can't figure out src lang + (workspace-root (file-name-directory lsp-test-sample-file)) + (initial-server-count (lsp-test-total-folder-count))) + (register-mock-client) ; register mock client as the one an only lsp client + + ;; xref in emacs 27.2 does not have these vars, + ;; but lsp-mode uses them in lsp-show-xrefs. + ;; For the purpose of this test, it does not matter. + (unless (boundp 'xref-auto-jump-to-first-xref) + (defvar xref-auto-jump-to-first-xref nil)) + (unless (boundp 'xref-auto-jump-to-first-definition) + (defvar xref-auto-jump-to-first-definition nil)) + + (lsp-workspace-folders-add workspace-root) + (let* ((buf (find-file-noselect lsp-test-sample-file))) + (unwind-protect + (with-timeout (5 (error "Timeout running a test with mock server")) + (with-current-buffer buf + (prog-mode) + (lsp) + ;; Make sure the server started + (should (eq (lsp-test-total-folder-count) (1+ initial-server-count))) + (lsp-test-sync-wait (eq 'initialized + (lsp--workspace-status (cl-first (lsp-workspaces))))) + (funcall test-body))) + (with-current-buffer buf + (set-buffer-modified-p nil); Inhibut the "kill unsaved buffer"p prompt + (kill-buffer buf)) + (lsp-workspace-folders-remove workspace-root) + ;; Remove possibly unhandled commands + (when (file-exists-p lsp-test-mock-server-command-file) + (delete-file lsp-test-mock-server-command-file)))) + (with-timeout (5 (error "LSP mock server refuses to stop")) + ;; Make sure the server stopped + (lsp-test-sync-wait (= initial-server-count (lsp-test-total-folder-count)))))) + +(defmacro lsp-mock-run-with-mock-server (&rest test-body) + "Evaluate TEST-BODY in the context of a mock client connected to mock server. + +Opens the `lsp-test-sample-file' and initiates the LSP session. +TEST-BODY can interact with the mock server." + `(lsp-mock--run-with-mock-server (lambda () ,@test-body))) + +(ert-deftest lsp-mock-server-reports-issues () + (lsp-mock-run-with-mock-server + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 0)) + (lsp-test-command-send-diags lsp-test-sample-file (buffer-string) "broming") + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 1)) + (should (equal (lsp-test-diag-get (car (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (lsp-test-range-make (buffer-string) + "line 1 unique word broming + common" + " ^^^^^^^ "))))) + +(ert-deftest lsp-mock-server-crashes () + "Test that the mock server crashes when instructed so." + (let ((initial-serv-count (lsp-test-total-folder-count))) + (when-let ((buffer (get-buffer "*mock-server::stderr*"))) + (kill-buffer buffer)) + (lsp-mock-run-with-mock-server + (should (eq (lsp-test-total-folder-count) (1+ initial-serv-count))) + (lsp-test-crash-server-with-message "crashed by command") + (lsp-test-sync-wait (eq initial-serv-count (lsp-test-total-folder-count))) + (let ((buffer (get-buffer "*mock-server::stderr*"))) + (should buffer) + (with-current-buffer buffer + (goto-char (point-min)) + (should (search-forward "crashed by command")) + (goto-char (point-max))))))) + +(defun lsp-mock-get-first-diagnostic-line () + "Get the line number of the first diagnostic on `lsp-test-sample-file'." + (let ((diags (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (when diags + (let* ((diag (car diags)) + (range (ht-get diag "range")) + (start (ht-get range "start"))) + (ht-get start "line"))))) + +(ert-deftest lsp-mock-server-updates-diagnostics () + "Test that mock server can update diagnostics and lsp-mode reflects that." + (lsp-mock-run-with-mock-server + ;; There are no diagnostics at first + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 0)) + + ;; Server found diagnostic + (lsp-test-command-send-diags lsp-test-sample-file (buffer-string) "broming") + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 1)) + + ;; The diagnostic is properly received + (should (equal (lsp-test-diag-get (car (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (lsp-test-range-make (buffer-string) + "line 1 unique word broming + common" + " ^^^^^^^ "))) + + ;; Server found a different diagnostic + (lsp-test-command-send-diags lsp-test-sample-file (buffer-string) "fegam") + (let ((old-line (lsp-mock-get-first-diagnostic-line))) + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (not (equal old-line (lsp-mock-get-first-diagnostic-line)))))) + + ;; The new diagnostics is properly displayed instead of the old one + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 1)) + (should (equal (lsp-test-diag-get (car (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (lsp-test-range-make (buffer-string) + "Line 0 unique word fegam and common" + " ^^^^^ "))))) + +(ert-deftest lsp-mock-server-updates-diags-with-delay () + "Test demonstrating delay in the diagnostics update. + +If server takes noticeable time to update diagnostics after a +document change, and `lsp-diagnostic-clean-after-change' is +nil (default), diagnostic ranges will be off until server +publishes the update. This test demonstrates this behavior." + (lsp-mock-run-with-mock-server + ;; There are no diagnostics at first + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 0)) + + ;; Server found diagnostic + (lsp-test-command-send-diags lsp-test-sample-file (buffer-string) "broming") + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 1)) + + ;; The diagnostic is properly received + (should (equal (lsp-test-diag-get (car (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (lsp-test-range-make (buffer-string) + "line 1 unique word broming + common" + " ^^^^^^^ "))) + + ;; Change the text: remove the first line + (goto-char (point-min)) + (kill-line 1) + (should (string-equal (buffer-string) + "line 1 unique word broming + common +line 2 unique word normalw common here +line 3 words here and here +")) + ;; Give it some time to update + (sleep-for 0.5) + ;; The diagnostic is not updated and now points to a wrong line + (should (equal (lsp-test-diag-get (car (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (lsp-test-range-make (buffer-string) + "line 2 unique word normalw common here" + " ^^^^^^^ "))) + + ;; Server sent an update + (lsp-test-command-send-diags lsp-test-sample-file (buffer-string) "broming") + + (let ((old-line (lsp-mock-get-first-diagnostic-line))) + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (not (equal old-line (lsp-mock-get-first-diagnostic-line)))))) + + ;; Now the diagnostic is correct again + (should (equal (lsp-test-diag-get (car (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (lsp-test-range-make (buffer-string) + "line 1 unique word broming + common" + " ^^^^^^^ "))))) + +(ert-deftest lsp-mock-server-updates-diags-clears-up () + "Test ensuring diagnostics are cleared after a change." + (let ((lsp-diagnostic-clean-after-change t)) + (lsp-mock-run-with-mock-server + ;; There are no diagnostics at first + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 0)) + + ;; Server found diagnostic + (lsp-test-command-send-diags lsp-test-sample-file (buffer-string) "broming") + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (should (eq (length (gethash lsp-test-sample-file (lsp-diagnostics t))) 1)) + + ;; The diagnostic is properly received + (should (equal (lsp-test-diag-get (car (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (lsp-test-range-make (buffer-string) + "line 1 unique word broming + common" + " ^^^^^^^ "))) + + ;; Change the text: remove the first line + (goto-char (point-min)) + (kill-line 1) + + ;; After a short while, diagnostics are cleared up + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (null (gethash lsp-test-sample-file (lsp-diagnostics t))))) + + ;; Server sent an update + (lsp-test-command-send-diags lsp-test-sample-file (buffer-string) "broming") + + (let ((old-line (lsp-mock-get-first-diagnostic-line))) + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (not (equal old-line (lsp-mock-get-first-diagnostic-line)))))) + + ;; Now the diagnostic is correct again + (should (equal (lsp-test-diag-get (car (gethash lsp-test-sample-file (lsp-diagnostics t)))) + (lsp-test-range-make (buffer-string) + "line 1 unique word broming + common" + " ^^^^^^^ ")))))) + +(defun lsp-test-xref-loc-to-range (xref-loc) + "Convert XREF-LOC to a range p-list. + +XREF-LOC is an xref-location object. The function returns a p-list +in the form of `(:line .. :from .. :to ..)'." + (let ((line (- (xref-location-line (xref-item-location xref-loc)) 1)) + (len (xref-match-length xref-loc)) + (col (xref-file-location-column (xref-item-location xref-loc)))) + (list :line line :from col :to (+ col len)))) + +(defun lsp-test-make-references (for-file contents word) + "Come up with a list of references to WORD in CONTENTS. + +Scan CONTENTS for all occurences of WORD and compose a list of references." + (let ((add-uri (lambda (range) `(:uri ,(concat "file://" for-file) + :range ,range)))) + (vconcat (mapcar add-uri (lsp-test-find-all-words contents word))))) + +(defun lsp-test-schedule-response (method response) + "Schedule a RESPONSE to be sent in response to METHOD." + (lsp-test-send-command-to-mock-server + (format "(schedule-response %S %S)" method response))) + +(ert-deftest lsp-mock-server-provides-references () + "Test ensuring that lsp-mode accepts correct locations for references." + (let* (found-xrefs + (xref-show-xrefs-function (lambda (fetcher &rest _params) + (setq found-xrefs (funcall fetcher))))) + (lsp-mock-run-with-mock-server + (lsp-test-schedule-response "textDocument/references" + (lsp-test-make-references + lsp-test-sample-file (buffer-string) "unique")) + (lsp-find-references) + (should found-xrefs) + (should (eq (length found-xrefs) 3)) + (should (equal (lsp-test-xref-loc-to-range (nth 0 found-xrefs)) + (lsp-test-range-make (buffer-string) + "Line 0 unique word fegam and common" + " ^^^^^^ "))) + (should (equal (lsp-test-xref-loc-to-range (nth 1 found-xrefs)) + (lsp-test-range-make (buffer-string) + "line 1 unique word broming + common" + " ^^^^^^ "))) + (should (equal (lsp-test-xref-loc-to-range (nth 2 found-xrefs)) + (lsp-test-range-make (buffer-string) + "line 2 unique word normalw common here" + " ^^^^^^ ")))))) + +(ert-deftest lsp-mock-server-provides-folding-ranges () + "Test ensuring that lsp-mode accepts correct locations for folding ranges." + (lsp-mock-run-with-mock-server + (lsp-test-schedule-response + "textDocument/foldingRange" + [(:kind "region" :startLine 0 :startCharacter 10 :endLine 1) + (:kind "region" :startLine 1 :startCharacter 5 :endLine 2)]) + + (let ((folding-ranges (lsp--get-folding-ranges))) + (should (eq (length folding-ranges) 2)) + ;; LSP line numbers are 0-based, Emacs line numbers are 1-based + ;; henace the +1 + (should (equal (line-number-at-pos + (lsp--folding-range-beg (nth 0 folding-ranges))) + 1)) + (should (equal (line-number-at-pos + (lsp--folding-range-end (nth 0 folding-ranges))) + 2)) + (should (equal (line-number-at-pos + (lsp--folding-range-beg (nth 1 folding-ranges))) + 2)) + (should (equal (line-number-at-pos + (lsp--folding-range-end (nth 1 folding-ranges))) + 3))))) + +(ert-deftest lsp-mock-server-lsp-caches-folding-ranges () + "Test ensuring that lsp-mode accepts correct locations for folding ranges." + (lsp-mock-run-with-mock-server + (should (eq (lsp--get-folding-ranges) nil)) + (lsp-test-schedule-response + "textDocument/foldingRange" + [(:kind "region" :startLine 0 :startCharacter 10 :endLine 1)]) + ;; Folding ranges are cached from the first request + (should (eq (lsp--get-folding-ranges) nil)))) + +(defun lsp-test-all-overlays (tag) + "Return all overlays tagged TAG in the current buffer." + (let ((overlays (overlays-in (point-min) (point-max)))) + (seq-filter (lambda (overlay) + (overlay-get overlay tag)) + overlays))) + +(defun lsp-test-all-overlays-as-ranges (tag) + "Return all overlays tagged TAG in the current buffer as ranges. + +Tagged overlays have the property TAG set to t." + (let ((to-range + (lambda (overlay) + (let* ((beg (overlay-start overlay)) + (end (overlay-end overlay)) + (beg-line (line-number-at-pos beg)) + (end-line (line-number-at-pos end)) + (beg-col (progn (goto-char beg) (current-column))) + (end-col (progn (goto-char end) (current-column)))) + (should (equal beg-line end-line)) + (list :line (- beg-line 1) :from beg-col :to end-col))))) + (save-excursion + (mapcar to-range (lsp-test-all-overlays tag))))) + +(defun lsp-test-make-highlights (contents word) + "Come up with a list of highlights of WORD in CONTENTS. + +Scan CONTENTS for all occurences of WORD and compose a list of highlights." + (let ((add-uri (lambda (range) `(:kind 1 :range ,range)))) + (vconcat (mapcar add-uri (lsp-test-find-all-words contents word))))) + +(defun lsp-mock-with-temp-window (buffer-name test-fn) + "Create a temporary window displaying BUFFER-NAME and call TEST-FN. +BUFFER-NAME is the name of the buffer to display. +TEST-FN is a function to call with the temporary window." + (let ((original-window (selected-window)) + (temp-window (split-window))) + (unwind-protect + (progn + ;; Display the buffer in the temporary window + (set-window-buffer temp-window buffer-name) + ;; Switch to the temporary window + (select-window temp-window) + ;; Call the test function + (funcall test-fn)) + ;; Clean up: Delete the temporary window and select the original window + (delete-window temp-window) + (select-window original-window)))) + +(ert-deftest lsp-mock-server-provides-symbol-highlights () + "Test ensuring that lsp-mode accepts correct locations for highlights." + (lsp-mock-run-with-mock-server + (lsp-test-schedule-response + "textDocument/documentHighlight" + (lsp-test-make-highlights (buffer-string) "here")) + ;; The highlight overlays are created only if visible in a window + (lsp-mock-with-temp-window + (current-buffer) + (lambda () + (lsp-document-highlight) + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (lsp-test-all-overlays-as-ranges + 'lsp-highlight))) + (let ((highlights (lsp-test-all-overlays-as-ranges 'lsp-highlight))) + (should (eq (length highlights) 3)) + (should (equal (nth 0 highlights) + (lsp-test-range-make (buffer-string) + "line 2 unique word normalw common here" + " ^^^^"))) + (should (equal (nth 1 highlights) + (lsp-test-range-make (buffer-string) + "line 3 words here and here" + " ^^^^ "))) + (should (equal (nth 2 highlights) + (lsp-test-range-make (buffer-string) + "line 3 words here and here" + " ^^^^")))))))) + +(defun lsp-test-index-to-pos (idx) + "Convert 0-based integer IDX to a position in the corrent buffer. + +Retruns the position p-list." + (lsp-point-to-position (1+ idx))) + +(defun lsp-test-make-edits (marked-up) + "Create a list of edits to transform current buffer according to MARKED-UP. + +MARKED-UP string uses a simple markup syntax to indicate +insertions and deletions. The function returns a list of edits +each in the form `(:range .. :newText ..)' + +The markup syntax is as follows: +- - indicates an insertion of the text `word' +- ####### - indicates a deletion of the text that was in place of each `#' + +All edits must be single line: deletion must not cross a line break +and insertion must not contain a line break." + (let ((edits nil) + (original (buffer-string)) + (orig-idx 0) + (marked-idx 0)) + (while (and (< orig-idx (length original)) + (< marked-idx (length marked-up))) + (let ((orig-char (aref original orig-idx)) + (marked-char (aref marked-up marked-idx))) + (cond + ((eq marked-char ?<) ; Insertion + (let ((marked-idx-start marked-idx)) + (while (and (< marked-idx (length marked-up)) + (not (eq (aref marked-up marked-idx) ?>))) + (setq marked-idx (1+ marked-idx))) + (should (< marked-idx (length marked-up))) + (push `(:range (:start ,(lsp-test-index-to-pos orig-idx) + :end ,(lsp-test-index-to-pos orig-idx)) + :newText ,(substring marked-up (1+ marked-idx-start) marked-idx)) + edits) + (setq marked-idx (1+ marked-idx)) ; Skip the closing > + )) + ((eq marked-char ?#) ; Deletion + (let ((orig-idx-start orig-idx)) + (while (and (< marked-idx (length marked-up)) + (< orig-idx (length original)) + (eq (aref marked-up marked-idx) ?#)) + (setq orig-idx (1+ orig-idx)) + (setq marked-idx (1+ marked-idx))) + (should (and (< marked-idx (length marked-up)) + (< orig-idx (length original)))) + (push `(:range (:start ,(lsp-test-index-to-pos orig-idx-start) + :end ,(lsp-test-index-to-pos orig-idx)) + :newText "") + edits))) + (t (should (eq orig-char marked-char)) + (setq orig-idx (1+ orig-idx)) + (setq marked-idx (1+ marked-idx)))))) + (should (and (= orig-idx (length original)) + (= marked-idx (length marked-up)))) + (vconcat (reverse edits)))) + +(ert-deftest lsp-mock-make-edits-sane () + "Check the test-utility function `lsp-mock-make-edits'." + (with-temp-buffer + (insert "line 0 common deleted common") + (should (equal (lsp-test-make-edits + "line 0 common deleted common") + [(:range (:start (:line 0 :character 0) + :end (:line 0 :character 0)) + :newText "inserted") + ])) + (should (equal (lsp-test-make-edits + "line 0 common deleted common") + [(:range (:start (:line 0 :character 1) + :end (:line 0 :character 1)) + :newText "inserted") + ])) + (should (equal (lsp-test-make-edits + "line 0 common ####### common") + [(:range (:start (:line 0 :character 7) + :end (:line 0 :character 7)) + :newText "inserted") + (:range (:start (:line 0 :character 14) + :end (:line 0 :character 21)) + :newText "")])))) + +(ert-deftest lsp-mock-server-formats-with-edits () + "Test ensuring that lsp-mode requests and applies formatting correctly." + (lsp-mock-run-with-mock-server + (lsp-test-schedule-response + "textDocument/formatting" + (lsp-test-make-edits + "Line 0 ###### word fegam and common +line 1 unique word ######### common +line 2 unique word #ormalw common here +line 3 words here and here +")) + (lsp-format-buffer) + (should (equal (buffer-string) + "Line 0 word fegam and common +line 1 unique doubleword common +line 2 unique word ormalw common here +line 3 words here and here +")))) + +(ert-deftest lsp-mock-server-suggests-action-with-simple-changes () + "Test ensuring that lsp-mode applies code action simple edits correctly." + (lsp-mock-run-with-mock-server + (lsp-test-schedule-response + "textDocument/codeAction" + (vconcat (list `(:title "Some edits" + :kind "quickfix" + :isPreferred t + :edit + (:changes + ((,(concat "file://" lsp-test-sample-file) + . + ,(lsp-test-make-edits + "Line 0 unique word ######### common +line # unique word broming + common +line # unique word normalw common here +line #<81> words here and here +")))))))) + (lsp-execute-code-action-by-kind "quickfix") + (should (equal (buffer-string) + "Line 0 unique word common +line unique word broming + common +line unique word normalw common here +line 81 words here and here +")))) + +(ert-deftest lsp-mock-server-suggests-action-with-doc-changes () + "Test ensuring that lsp-mode applies code action document edits correctly." + (lsp-mock-run-with-mock-server + (let ((docChanges + (vconcat (list `(:textDocument + (:version 0 ; document was never changed + :uri ,(concat "file://" lsp-test-sample-file)) + :edits + ,(lsp-test-make-edits + "Line 0 ########### ######### common +line 1<00> unique word broming + common +line # ###### word normalw common here +line #<81> words here and here +")))))) + (lsp-test-schedule-response + "textDocument/codeAction" + (vconcat (list `(:title "Some edits" + :kind "quickfix" + :isPreferred t + :edit + (:changes #s(hash-table data ()) ; empty obj + :documentChanges ,docChanges))))) + (lsp-execute-code-action-by-kind "quickfix") + (should (equal (buffer-string) + "Line 0 common +line 100 unique word broming + common +line word normalw common here +line 81 words here and here +"))))) + +(ert-deftest lsp-mock-doc-changes-wrong-version () + "Test ensuring that lsp-mode applies code action document edits correctly." + (lsp-mock-run-with-mock-server + (let ((docChanges + (vconcat (list `(:textDocument + (:version 1 ; This version does not exist + :uri ,(concat "file://" lsp-test-sample-file)) + :edits []))))) + (lsp-test-schedule-response + "textDocument/codeAction" + (vconcat (list `(:title "Some edits" + :kind "quickfix" + :isPreferred t + :edit + (:changes #s(hash-table data ()) ; empty obj + :documentChanges ,docChanges))))) + (should-error (lsp-execute-code-action-by-kind "quickfix"))))) + +;; Some actions are executed partially by the server: +;; after the user selects the action, lsp-mode sends a request +;; to exute the associated command. +;; Only after that, server sends a request to perform edits +;; in the editor. +;; This test simulates only the last bit. +(ert-deftest lsp-mock-server-request-edits () + "Test ensuring that lsp-mode honors server's request for edits." + (lsp-mock-run-with-mock-server + (let ((initial-content (buffer-string))) + (lsp-test-send-command-to-mock-server + (format "(princ (json-rpc-string '(:id 1 :method \"workspace/applyEdit\" + :params (:edit + (:changes + ((%S . %S)))))))" + (concat "file://" lsp-test-sample-file) + (lsp-test-make-edits + "#### <8>0 unique word fegam and common +line 1 unique word broming + common +line 2 unique word normalw common here +line 3 words here and here +"))) + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (not (equal initial-content (buffer-string))))) + (should (equal (buffer-string) + " 80 unique word fegam and common +line 1 unique word broming + common +line 2 unique word normalw common here +line 3 words here and here +"))))) + +(ert-deftest lsp-mock-server-no-declaration-found () + "Test checking that lsp-mode reports when server returns no declaration." + (lsp-mock-run-with-mock-server + (should (string-match-p "not found" (lsp-find-declaration))))) + +(ert-deftest lsp-mock-server-goto-declaration () + "Test checking that lsp-mode can follow the symbol declaration." + (lsp-mock-run-with-mock-server + (let ((decl-range (lsp-test-range-make + (buffer-string) + "line 1 unique word broming + common" + " ^^^^^^^ "))) + (lsp-test-schedule-response + "textDocument/declaration" + (vconcat (list `(:uri ,(concat "file://" lsp-test-sample-file) + :range ,(lsp-test-full-range decl-range))))) + (lsp-find-declaration) + ;; 1+ to convert 0-based LSP line number to 1-based Emacs line number + (should (equal (1+ (plist-get decl-range :line)) (line-number-at-pos))) + (should (equal (plist-get decl-range :from) (current-column)))))) + +(ert-deftest lsp-mock-server-goto-definition () + "Test checking that lsp-mode can follow the symbol definition." + (lsp-mock-run-with-mock-server + (let ((decl-range (lsp-test-range-make + (buffer-string) + "line 3 words here and here" + " ^^^^^^^ "))) + (lsp-test-schedule-response + "textDocument/definition" + (vconcat (list `(:uri ,(concat "file://" lsp-test-sample-file) + :range ,(lsp-test-full-range decl-range))))) + (lsp-find-definition) + ;; 1+ to convert 0-based LSP line number to 1-based Emacs line number + (should (equal (1+ (plist-get decl-range :line)) (line-number-at-pos))) + (should (equal (plist-get decl-range :from) (current-column)))))) + +(ert-deftest lsp-mock-server-provides-inlay-hints () + "lsp-mode accepts inlay hints from the server and displays them." + (let ((lsp-inlay-hint-enable t) + (hint-line 2) + (hint-col 10)) + (lsp-mock-run-with-mock-server + (lsp-mock-with-temp-window + (current-buffer) + (lambda () + (lsp-test-schedule-response + "textDocument/inlayHint" + (vconcat (list `(:kind 2 + :position (:line ,hint-line :character ,hint-col) + :paddingLeft () + :label "my hint")))) + ;; Lsp will update inlay hints on idling + (run-hooks 'lsp-on-idle-hook) + (lsp-test-sync-wait (progn (should (lsp-workspaces)) + (lsp-test-all-overlays 'lsp-inlay-hint))) + (let ((hints (lsp-test-all-overlays 'lsp-inlay-hint))) + (should (eq (length hints) 1)) + (should (equal (overlay-get (car hints) 'before-string) "my hint")) + (goto-char (overlay-start (car hints))) + ; 1+ to convert 0-based LSP line number to 1-based Emacs line number + (should (equal (line-number-at-pos) (1+ hint-line))) + (should (equal (current-column) hint-col)))))))) + +(ert-deftest lsp-mock-server-provides-code-lens () + "lsp-mode accepts code lenses from the server and displays them." + (let ((line 2)) + (lsp-test-schedule-response + "textDocument/codeLens" + (vconcat (list `(:range (:start (:line ,line :character 0) + :end (:line ,line :character 1)) + :command (:title "My command" + :command "myCommand"))))) + (lsp-mock-run-with-mock-server + (lsp-test-sync-wait (lsp-test-all-overlays 'lsp-lens)) + (let ((lenses (lsp-test-all-overlays 'lsp-lens))) + (should (eq (length lenses) 1)) + (message "%s" (overlay-properties (car lenses))) + (should (string-match-p "My command" + (overlay-get (car lenses) 'after-string))) + (goto-char (overlay-start (car lenses))) + (should (equal (line-number-at-pos) (- line 1))))))) + +;;; lsp-mock-server-test.el ends here diff --git a/test/lsp-protocol-test.el b/test/lsp-protocol-test.el index 9a5adc2c65a..7b5a422cf34 100644 --- a/test/lsp-protocol-test.el +++ b/test/lsp-protocol-test.el @@ -81,28 +81,34 @@ (lsp-make-my-position :line 30 :character 40 :camelCase nil) :specialProperty 42))) (should (pcase particular-range - ((MyRange :start (MyPosition :line start-line :character start-char :camel-case start-camelcase) - :end (MyPosition :line end-line :character end-char :camel-case end-camelCase)) + ((lsp-interface MyRange + :start (lsp-interface MyPosition + :line start-line :character start-char :camel-case start-camelcase) + :end (lsp-interface MyPosition + :line end-line :character end-char :camel-case end-camelCase)) t) (_ nil))) (should (pcase particular-extended-range - ((MyExtendedRange) + ((lsp-interface MyExtendedRange) t) (_ nil))) ;; a subclass can be matched by a pattern for a parent class (should (pcase particular-extended-range - ((MyRange :start (MyPosition :line start-line :character start-char :camel-case start-camelcase) - :end (MyPosition :line end-line :character end-char :camel-case end-camelCase)) + ((lsp-interface MyRange + :start (lsp-interface MyPosition + :line start-line :character start-char :camel-case start-camelcase) + :end (lsp-interface MyPosition + :line end-line :character end-char :camel-case end-camelCase)) t) (_ nil))) ;; the new patterns should be able to be used with existing ones (should (pcase (list particular-range particular-extended-range) - ((seq (MyRange) - (MyExtendedRange)) + ((seq (lsp-interface MyRange) + (lsp-interface MyExtendedRange)) t) (_ nil))) @@ -110,8 +116,8 @@ ;; not in the order specified by the inner patterns (should-not (pcase (list particular-range particular-extended-range) - ((seq (MyExtendedRange) - (MyRange)) + ((seq (lsp-interface MyExtendedRange) + (lsp-interface MyRange)) t) (_ nil))) @@ -122,8 +128,11 @@ ;; and the second instance is an equality check against the other ;; :character value, which is different. (should-not (pcase particular-range - ((MyRange :start (MyPosition :line start-line :character :camel-case start-camelcase) - :end (MyPosition :line end-line :character :camel-case end-camelCase)) + ((lsp-interface MyRange + :start (lsp-interface MyPosition + :line start-line :character :camel-case start-camelcase) + :end (lsp-interface MyPosition + :line end-line :character :camel-case end-camelCase)) t) (_ nil))) @@ -131,7 +140,7 @@ ;; should still match if the required stuff matches. Missing ;; optional properties are bound to nil. (should (pcase particular-range - ((MyRange :start (MyPosition :optional?)) + ((lsp-interface MyRange :start (lsp-interface MyPosition :optional?)) (null optional?)) (_ nil))) @@ -139,23 +148,23 @@ ;; the interface, even if the expr-val has all the types specified ;; by the interface. This is a programmer error. (should-error (pcase particular-range - ((MyRange :something-unrelated) + ((lsp-interface MyRange :something-unrelated) t) (_ nil))) ;; we do not use camelCase at this stage. This is a programmer error. (should-error (pcase particular-range - ((MyRange :start (MyPosition :camelCase)) + ((lsp-interface MyRange :start (lsp-interface MyPosition :camelCase)) t) (_ nil))) (should (pcase particular-range - ((MyRange :start (MyPosition :camel-case)) + ((lsp-interface MyRange :start (lsp-interface MyPosition :camel-case)) t) (_ nil))) ;; :end is missing, so we should fail to match the interface. (should-not (pcase (lsp-make-my-range :start (lsp-make-my-position :line 10 :character 20 :camelCase nil)) - ((MyRange) + ((lsp-interface MyRange) t) (_ nil))))) diff --git a/test/mock-lsp-server.el b/test/mock-lsp-server.el new file mode 100644 index 00000000000..aa2d690952e --- /dev/null +++ b/test/mock-lsp-server.el @@ -0,0 +1,204 @@ +;;; mock-lsp-server.el --- Mock LSP server -*- lexical-binding: t; -*- + +;; Copyright (C) 2024-2024 emacs-lsp maintainers + +;; Author: Arseniy Zaostrovnykh +;; Package-Requires: ((emacs "27.1")) +;; Version: 0.1.0 +;; License: GPL-3.0-or-later + +;; URL: https://github.com/emacs-lsp/lsp-mode +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; A mock implementation of a Language Server Protocol server for testing +;; of the LSP client in lsp-mode. +;; +;; The server reads commands from a file `mock-server-commands.el` in the same +;; directory as this file. The commands are elisp code that is loaded and +;; executed by the server. It deletes the file after executing the command +;; indicating readiness for the next one. +;; +;; Due to `emacs --script` limitations, the server cannot watch two inputs +;; concurrently: +;; - stdin for the client input +;; - the mock-server-commands.el file for the commands +;; +;; Therefore, the server alternates between the two. It waits for the next +;; client input, and only after processing one can take and execute a command. +;; As a consequence, you should make sure to send a notification from the client +;; once you have created the command file with a command. + +;;; Code: + +(require 'json) + +;; To ease debugging, print the stack trace on failure +(setq debug-on-error t) + +(defconst command-file + (expand-file-name "mock-server-commands.el" + (file-name-directory load-file-name)) + "Path to the file where the server expects external commands.") + +(defun run-command-from-file-if-any () + "If there is the `command-file', execute and delete it." + (if (file-exists-p command-file) + (progn + (load command-file) + (delete-file command-file)))) + +(defun json-rpc-string (body) + "Format BODY p-list as a JSON RPC message suitable for LSP." + ;; 1+ - extra new-line at the end + (let* ((encoded-body (json-encode `(:jsonrpc "2.0" ,@body))) + (content-length-header + (format "Content-Length: %d" (1+ (string-bytes encoded-body)))) + (content-type-header + "Content-Type: application/vscode-jsonrpc; charset=utf8")) + (concat content-length-header "\r\n" + content-type-header "\r\n\r\n" + encoded-body "\n"))) + +(defconst server-info + '(:name "mockS" :version "0.1.0") + "Basic server information: name and version.") + + +(defconst server-capabilities '(:referencesProvider t + :foldingRangeProvider t + :documentHighlightProvider t + :documentFormattingProvider t + :codeActionProvider t + :declarationProvider t + :definitionProvider t + :inlayHintProvider t + :codeLensProvider (:resolveProvider ())) + "Capabilities of the server.") + +(defun greeting (id) + "Compose the greeting message in response to `initialize' request with id ID." + (json-rpc-string `(:id ,id :result (:capabilities ,server-capabilities + :serverInfo ,server-info)))) + +(defun respond (id result) + "Acknowledge a request with id ID." + (json-rpc-string `(:id ,id :result ,result))) + +(defun publish-diagnostics (diagnostics) + "Send JSON RPC message textDocument/PublishDiagnostics with DAGNOSTICS. + +DIAGNOSICS must be a p-list (:path PATH :diags DIAGS), +where DIAGS is a list of p-lists in the form +(:source .. :code .. :range .. :message .. :severity ..)." + (princ + (json-rpc-string `(:method "textDocument/publishDiagnostics" + :params ,diagnostics)))) + +(defun get-id (input) + "Extract request id from INPUT JSON message." + (when (string-match "\"id\":\\([0-9]+\\)" input) + (string-to-number (match-string 1 input)))) + +(defconst notification-methods + '("\"method\":\"initialized\"" + "\"method\":\"textDocument/didOpen\"" + "\"method\":\"textDocument/didClose\"" + "\"method\":\"$/setTrace\"" + "\"method\":\"workspace/didChangeConfiguration\"") + "Expected notification methods that require no acknowledgement.") + +(defun is-notification (input) + "Check if INPUT is a notification message. + +See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage" + (catch 'found + (dolist (n notification-methods) + (when (string-match-p n input) + (throw 'found t))) + nil)) + +(defvar scheduled-responses (make-hash-table :test 'equal) + "Keep the planned response for the requiest of the given method. + +Can contain only one planned response per method. +Key is the method, and value is the `result' field in the response.") + +(defun schedule-response (method result) + "Next time request of METHOD comes respond with `result' RESULT. + +This function is useful for external commands, +allowing control over the server responses. + +You can schedule only one response for a method for the entire session." + (when (gethash method scheduled-responses) + (error "Response for method %s is already scheduled" method)) + (puthash method result scheduled-responses)) + +(defun get-method (input) + "Retrieve the method of the request in INPUT. + +Returns nil if no method is found." + (when (string-match "\"method\":\"\\([^\"]+\\)\"" input) + (match-string 1 input))) + +(defun get-response-for-request (method) + "Find the scheduled response for METHOD request. + +Returns empty array if not found: + empty array is the usual representation of empty result. + +The response is not removed to cover for potential plural requests." + (if-let ((response (gethash method scheduled-responses))) + response + [])) + +(defun handle-lsp-client-input () + "Read and handle one line of te input from the LSP client." + (let ((line (read-string ""))) + (cond + ((string-match "method\":\"initialize\"" line) + (princ (greeting (get-id line)))) + ((string-match "method\":\"exit" line) + (kill-emacs 0)) + ((string-match "method\":\"shutdown" line) + (princ (respond (get-id line) nil))) + ((is-notification line) + ;; No need to acknowledge a notification + ) + ((get-id line) + ;; It has an id, probably some request + ;; Acknowledge that it is received + (princ (respond + (get-id line) + (get-response-for-request (get-method line))))) + ((or (string-match "Content-Length" line) + (string-match "Content-Type" line)) + ;; Ignore header + ) + ((or (string-match "^\r$" line) + (string-match "^$" line)) + ;; Ignore empty lines and header/content separators + ) + (t (error "unexpected input '%s'" line))))) + +;; Keep alternating from executing a command to handling client input. +;; If emacs --script had concurrency support, +;; it would have been executed concurrently. +(while t + (run-command-from-file-if-any) + (handle-lsp-client-input)) + +;;; mock-lsp-server.el ends here