From 012a0ed3ea6620caeb045d75e146f39e5740724d Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Thu, 10 Oct 2024 12:23:26 -0700 Subject: [PATCH] Coalton LSP server --- .github/workflows/ci.yml | 35 + .gitignore | 5 + Makefile | 9 + README.md | 44 +- coalton-lsp | 17 + coalton-lsp.asd | 39 + coalton-mode.el | 270 +- resources/README.md | 29 + resources/clj/deps.edn | 1 + resources/clj/src/main.clj | 7 + resources/fib.coal | 14 + resources/rpc/client_registerCapability.json | 1 + .../rpc/client_registerCapability_result.json | 1 + resources/rpc/initialize.json | 1 + resources/rpc/initialize_result.json | 1 + resources/rpc/initialized.json | 1 + resources/rpc/textDocument_didOpen.json | 1 + .../rpc/textDocument_documentHighlight.json | 1 + ...textDocument_documentHighlight_result.json | 1 + resources/rpc/textDocument_hover.json | 1 + resources/rpc/textDocument_hover_result.json | 1 + .../rpc/textDocument_publishDiagnostics.json | 1 + resources/rpc/textDocument_signatureHelp.json | 1 + .../textDocument_signatureHelp_result.json | 1 + .../rpc/workspace_didChangeConfiguration.json | 1 + resources/spec.ts | 2260 +++++++++++++++++ slime-coalton.el | 95 - src/lib/json.lisp | 77 + src/lib/list.lisp | 4 + src/lib/log.lisp | 89 + src/lib/message.lisp | 211 ++ src/lib/name.lisp | 20 + src/lib/process.lisp | 86 + src/lib/rpc.lisp | 151 ++ src/lib/uri.lisp | 51 + src/package.lisp | 7 + src/protocol.lisp | 538 ++++ src/server.lisp | 109 + src/session.lisp | 310 +++ swank-coalton.lisp | 35 - test/types.coal | 95 - tests/json-tests.lisp | 7 + tests/lsp-tests.lisp | 9 + tests/message-tests.lisp | 15 + tests/mock.lisp | 13 + tests/package.lisp | 51 + tests/protocol-tests.lisp | 6 + tests/rpc-tests.lisp | 17 + tests/session-tests.lisp | 57 + 49 files changed, 4309 insertions(+), 488 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100755 coalton-lsp create mode 100644 coalton-lsp.asd create mode 100644 resources/README.md create mode 100644 resources/clj/deps.edn create mode 100644 resources/clj/src/main.clj create mode 100644 resources/fib.coal create mode 100644 resources/rpc/client_registerCapability.json create mode 100644 resources/rpc/client_registerCapability_result.json create mode 100644 resources/rpc/initialize.json create mode 100644 resources/rpc/initialize_result.json create mode 100644 resources/rpc/initialized.json create mode 100644 resources/rpc/textDocument_didOpen.json create mode 100644 resources/rpc/textDocument_documentHighlight.json create mode 100644 resources/rpc/textDocument_documentHighlight_result.json create mode 100644 resources/rpc/textDocument_hover.json create mode 100644 resources/rpc/textDocument_hover_result.json create mode 100644 resources/rpc/textDocument_publishDiagnostics.json create mode 100644 resources/rpc/textDocument_signatureHelp.json create mode 100644 resources/rpc/textDocument_signatureHelp_result.json create mode 100644 resources/rpc/workspace_didChangeConfiguration.json create mode 100644 resources/spec.ts delete mode 100644 slime-coalton.el create mode 100644 src/lib/json.lisp create mode 100644 src/lib/list.lisp create mode 100644 src/lib/log.lisp create mode 100644 src/lib/message.lisp create mode 100644 src/lib/name.lisp create mode 100644 src/lib/process.lisp create mode 100644 src/lib/rpc.lisp create mode 100644 src/lib/uri.lisp create mode 100644 src/package.lisp create mode 100644 src/protocol.lisp create mode 100644 src/server.lisp create mode 100644 src/session.lisp delete mode 100644 swank-coalton.lisp delete mode 100644 test/types.coal create mode 100644 tests/json-tests.lisp create mode 100644 tests/lsp-tests.lisp create mode 100644 tests/message-tests.lisp create mode 100644 tests/mock.lisp create mode 100644 tests/package.lisp create mode 100644 tests/protocol-tests.lisp create mode 100644 tests/rpc-tests.lisp create mode 100644 tests/session-tests.lisp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2bbdc65 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + pull_request: + branches: [master] + +jobs: + test: + name: ${{matrix.lisp}} on ${{matrix.os}} + strategy: + matrix: + lisp: [sbcl-bin] # only one cool lisp + os: [ubuntu-latest] + fail-fast: false + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v2 + - name: Install Roswell + shell: bash + env: + LISP: ${{matrix.lisp}} + run: curl -L https://raw.githubusercontent.com/roswell/roswell/master/scripts/install-for-ci.sh | sh -x + + - name: Get Lisp info + continue-on-error: true + shell: bash + run: | + ros -e '(format t "Lisp: ~a ~a on ~a~%" (lisp-implementation-type) (lisp-implementation-version) (machine-type))' + ros -e '(ql:quickload "trivial-features" :silent t)' -e '(format t "Features: ~s~%" *features*)' + + - name: Run tests + shell: bash + run: | + ros -e '(setf *debugger-hook* (lambda (&rest ignorable) (declare (ignore ignorable)) (uiop:quit -1)))' -e '(ql:quickload "coalton-lsp/tests")' -e '(unless (fiasco:all-tests) (uiop:quit -1))' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ca7e44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*~ +*# +*.fasl +.clj-kondo +.lsp \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..748443b --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +# Run unit tests, failing on the first error. + +.PHONY: test +test: + sbcl --noinform --non-interactive \ + --eval "(asdf:load-system :coalton-lsp/tests)" \ + --eval "(fiasco:run-package-tests \ + :packages '(:coalton-lsp/tests) \ + :interactive nil)" diff --git a/README.md b/README.md index 44a3556..ccbe92f 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,43 @@ -This directory contains the source code for an Emacs mode that supports -working with Coalton code. +This is an Emacs mode that supports working with Coalton. Most of the +logic is implemented as an LSP server. ## Requirements -This mode requires Emacs version > 29.1 because it relies on -tree-sitter. You can check that your copy of Emacs was built with -support for tree sitter by evaluating: +This mode has been developed using `eglot`, which is included with +Emacs since version 29. The mode can be installed with +`M-x package-install RET eglot RET` in Emacs 26.3 and later. - (treesit-available-p) +## Current Status -## Installation +Supported LSP features: -In your emacs init file (probably `~/.emacs.d/init.el` or `~/.emacs`), -add this directory to your load-path, and require the mode: +- diagnostics: whenever a .coal file is saved, the contents are + compiled, and diagnostics are published to the client. + +## Usage + +In the Emacs init file (e.g. `~/.emacs.d/init.el`), add this +directory to the load-path, and require the mode: ;; Coalton (add-to-list 'load-path "~/git/coalton-mode") (require 'coalton-mode) -## Usage +Start the server in slime with: -There is an example file, `types.coal` in the `test/` directory. + SLIME 2.30.git + CL-USER> (asdf:load-system "coalton-lsp") + ;; COALTON starting in development mode + ;; COALTON starting with specializations enabled + ;; COALTON starting with heuristic inlining disabled + ;; COALTON will emit type annotations + T + CL-USER> (coalton-lsp::start-server) + ;; :INFO coalton-mode: start # + # -The first time you open a `.coal` file, Emacs will ask you to approve -the installation of a parser component: +In Emacs, open a .coal file (e.g. resources/fibonacci.coal), and +enable eglot mode: - tree-sitter-coalton is not installed. Clone, build and install it? - -(Answer 'yes') + M-x eglot diff --git a/coalton-lsp b/coalton-lsp new file mode 100755 index 0000000..bbd376c --- /dev/null +++ b/coalton-lsp @@ -0,0 +1,17 @@ +#!/usr/bin/env sh +# +# Run a Coalton server +# +# Usage: +# +# coalton-lsp [ port ] +# +# (port defaults to 7887) + +port=${1:-7887} + +sbcl \ + --noinform \ + --load coalton-lsp.asd \ + --eval "(asdf:load-system \"coalton-lsp\")" \ + --eval "(coalton-lsp:main :port ${port})" diff --git a/coalton-lsp.asd b/coalton-lsp.asd new file mode 100644 index 0000000..04e3920 --- /dev/null +++ b/coalton-lsp.asd @@ -0,0 +1,39 @@ +(in-package :asdf-user) + +(defsystem #:coalton-lsp + :depends-on (#:alexandria + #:bordeaux-threads + #:coalton + #:com.inuoe.jzon + #:usocket) + :pathname "src/" + :serial t + :components ((:file "package") + ;; 'lib' contains general purpose code + (:module "lib" + :serial t + :components ((:file "log") + (:file "list") + (:file "name") + (:file "rpc") + (:file "process") + (:file "message") + (:file "json") + (:file "uri"))) + (:file "session") + (:file "protocol") + (:file "server"))) + +(defsystem #:coalton-lsp/tests + :depends-on (#:coalton-lsp + #:fiasco) + :pathname "tests/" + :serial t + :components ((:file "package") + (:file "mock") + (:file "json-tests") + (:file "lsp-tests") + (:file "message-tests") + (:file "protocol-tests") + (:file "rpc-tests") + (:file "session-tests"))) diff --git a/coalton-mode.el b/coalton-mode.el index 0f29eae..32bf856 100644 --- a/coalton-mode.el +++ b/coalton-mode.el @@ -1,29 +1,29 @@ -;;; coalton-mode.el --- Major mode for working with Coalton -*- lexical-binding: t; -*- -;; -;; URL: http://github.com/coalton-lang/coaltom -;; Keywords: languages coalton lisp -;; Version: 1.0.0 -;; Package-Requires: ((emacs "29.1")) -;; -;; This file contains functions for in-Emacs structural operations on -;; Coalton code, including syntax highlighting, indentation and -;; navigation, and command integration with the in-CL operations -;; defined in `slime-coalton.el'. +;;;; coalton-mode.el --- Major mode for working with Coalton -*- lexical-binding: t; -*- +;;;; Commentary: +;;;; +;;;; URL: http://github.com/coalton-lang/coaltom +;;;; Keywords: languages Coalton Lisp +;;;; Version: 1.0.0 +;;;; Package-Requires: ((Emacs "26.3")) +;;;; +;;;; This file contains functions for in-Emacs structural operations on +;;;; Coalton code, including syntax highlighting, indentation and +;;;; navigation, and command integration with the in-CL operations +;;;; defined in `slime-coalton.el'. -(require 'treesit) (require 'lisp-mnt) -(require 'slime) -(require 'slime-coalton) +(require 'eglot) -(add-to-list 'slime-contribs 'slime-coalton) +;;; Code: + +(add-to-list 'eglot-server-programs + ;; '(coalton-mode . ("coalton-lsp")) + '(coalton-mode . ("localhost" 7887))) (defconst coalton-mode-version (eval-when-compile (lm-version (or load-file-name buffer-file-name)))) -(defvar coalton-ts-repo - "https://github.com/coalton-lang/tree-sitter-coalton.git") - (defvar coalton-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c C-a") 'coalton-ast) @@ -55,248 +55,24 @@ table)) (defvar coalton--debug nil - "Enable debugging. - -Displays current tree-sitter node in mode line, useful for nav and highlighting testing.") - - -;; Fontification - -(defconst coalton--builtins - '("cond" - "declare" - "define" - "define-class" - "define-instance" - "define-struct" - "define-type" - "do" - "export" - "fn" - "for" - "if" - "import" - "import-from" - "let" - "lisp" - "lisp-toplevel" - "match" - "monomorphize" - "package" - "progn" - "repr" - "return" - "specialize" - "shadow" - "the" - "unless" - "when" - "while" - "=" - "=>" - "->" - "<-")) - -(defconst coalton--builtin-symbol - (eval-and-compile - (concat "^" (regexp-opt coalton--builtins) "$"))) - -(defun coalton--font-lock-settings () - "Return settings for `treesit-font-lock-settings'." - (treesit-font-lock-rules - :feature 'builtin - :language 'coalton - `(((list :anchor (symbol (symbol_name) @font-lock-keyword-face)) - (:match ,coalton--builtin-symbol @font-lock-keyword-face))) - - :feature 'number - :language 'coalton - '((number) @font-lock-number-face) - - :feature 'string - :language 'coalton - '((string) @font-lock-string-face) - - :feature 'paren - :language 'coalton - '((["(" ")"]) @font-lock-bracket-face) - - :feature 'comment - :language 'coalton - '((comment) @font-lock-comment-face))) - - -;; Indentation - -(defun coalton--indent-rules () - "Return rules for `treesit-simple-indent-rules'." - `((coalton - ((parent-is "list") parent 2)))) - - -;; Indexing and navigation - -(defun coalton--node-type-p (type node) - "Does NODE have tree-sitter TYPE?" - (string-equal type (treesit-node-type node))) - -(defun coalton--list-p (node) - "Is NODE a list?" - (coalton--node-type-p "list" node)) - -(defun coalton--symbol-p (node) - "Is NODE a symbol?" - (coalton--node-type-p "symbol" node)) - -(defun coalton--symbol-name (node) - "If NODE is a symbol, return its name." - (when (coalton--symbol-p node) - (treesit-node-text node t))) - -(defun coalton--symbol-name-p (name node) - "Is NODE a symbol named NAME?" - (and (coalton--symbol-p node) - (string-equal name (coalton--symbol-name node)))) - -(defun coalton--definition-type (node) - "If NODE is a definition, return the definition's type." - (when (coalton--list-p node) - (let ((node (treesit-node-child node 0 t))) - (when (coalton--symbol-p node) - (coalton--symbol-name node))))) - -(defun coalton--definition-p (type node) - "Is NODE a definition of type TYPE?" - (string-equal type (coalton--definition-type node))) - -(defun coalton--definition-name (node) - "If NODE is a definition, return its name." - (when (coalton--list-p node) - (let ((node (treesit-node-child node 1 t))) - (cond ((coalton--list-p node) - (let ((node (treesit-node-child node 0 t))) - (when (coalton--symbol-p node) - (coalton--symbol-name node)))) - ((coalton--symbol-p node) - (coalton--symbol-name node)))))) - - -;; Easy menu - -(defvar coalton-easy-menu - (let ((C '(coalton-available-p))) - `("Coalton" - ("Debug" - [ "Show AST" coalton-ast ,C ]) - ("Compile" - [ "Compile File" coalton-compile ,C ])))) - -(easy-menu-define menubar-coalton coalton-mode-map "Coalton" coalton-easy-menu) - - -;; Imenu - -(defun coalton--type-definition-p (node) - "Does NODE represent a type definition?" - (coalton--definition-p "define-type" node)) - -(defun coalton--instance-definition-p (node) - "Does NODE represent an instanclue definition?" - (coalton--definition-p "define-instance" node)) - -(defun coalton--function-definition-p (node) - "Does NODE represent a function definition?" - (coalton--definition-p "define" node)) - -(defvar coalton--imenu-settings - '(("Type" "list" - coalton--type-definition-p - coalton--definition-name) - ("Instance" "list" - coalton--instance-definition-p - coalton--definition-name) - ("Function" "list" - coalton--function-definition-p - coalton--definition-name)) - "The value for `treesit-simple-imenu-settings'.") + "Enable debugging.") ;; Initialization -(defun coalton--load-grammar () - "Install grammar." - (let ((treesit-language-source-alist - `((coalton ,coalton-ts-repo "main")))) - (unless (treesit-language-available-p 'coalton nil) - (when (yes-or-no-p "treesitter-coalton is not installed. Clone, build and install it?") - (treesit-install-language-grammar 'coalton))))) - (defun coalton-mode-variables () "Initialize buffer-local vars." - (setq-local comment-start "; ") - (setq-local treesit-simple-imenu-settings - coalton--imenu-settings) - (setq-local treesit-font-lock-settings - (coalton--font-lock-settings)) - (setq-local treesit-font-lock-feature-list - ;; Amount of decoration, from least to most, cumulative, - ;; controlled by `treesit-font-lock-level'. - '((comment) ; 1 - () ; 2 - (number string builtin) ; 3 - (paren))) ; 4 - (setq-local treesit-simple-indent-rules - (coalton--indent-rules))) + (setq-local comment-start "; ")) ;;;###autoload (define-derived-mode coalton-mode prog-mode "Coalton" "Major mode for working with Coalton. \\{coalton-mode-map}" - :syntax-table coalton-mode-syntax-table - (coalton--load-grammar) - (when (treesit-ready-p 'coalton) - (treesit-parser-create 'coalton) - (coalton-mode-variables) - (when coalton--debug - (setq-local treesit--indent-verbose t) - (setq-local treesit--font-lock-verbose t) - (treesit-inspect-mode)) - (treesit-major-mode-setup))) + :syntax-table coalton-mode-syntax-table) (add-to-list 'auto-mode-alist '("\\.coal\\'" . coalton-mode)) -(defvar coalton--query-package - (treesit-query-compile - 'coalton - '(((program (list - :anchor (symbol name: (symbol_name) @package) - :anchor (symbol name: (symbol_name) @package-name))) - (:equal @package "package"))))) - -(defun coalton-package () - (let ((nodes (treesit-query-capture 'coalton coalton--query-package))) - (treesit-node-text (cdr (assoc 'package-name nodes)) t))) - -(defun coalton--find-parent (node pred) - "Find first parent of NODE matching PRED." - (cond ((null node) - nil) - ((funcall pred node) - node) - (t - (coalton--find-parent (treesit-node-parent node) pred)))) - -(defun coalton--toplevel-form-p (node) - "Is NODE a toplevel program element?" - (and (coalton--list-p node) - (string-equal "program" (treesit-node-type - (treesit-node-parent node))))) - -(defun coalton-toplevel-form () - "Return the text of the toplevel form at point." - (when-let ((node (coalton--find-parent (treesit-node-at (point)) - #'coalton--toplevel-form-p))) - (treesit-node-text node t))) - (provide 'coalton-mode) + +;;; coalton-mode.el ends here diff --git a/resources/README.md b/resources/README.md new file mode 100644 index 0000000..169b868 --- /dev/null +++ b/resources/README.md @@ -0,0 +1,29 @@ +What's here: + +### rpc + +LSP JSON-RPC messages collected from eglot, for protocol testing + +### clj + +A small clojure project, for protocol and UI comparison. To run, +install the lsp server: + +``` sh +brew install clojure-lsp +``` + +Then open `clj/src/main.clj` and enable eglot mode in emacs:d + +``` emacs +M-x eglot +``` + +### fib.coal + +A small coalton program for testing + +### spec.ts + +All of the TypeScript code extracted from https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ + diff --git a/resources/clj/deps.edn b/resources/clj/deps.edn new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/resources/clj/deps.edn @@ -0,0 +1 @@ +{} diff --git a/resources/clj/src/main.clj b/resources/clj/src/main.clj new file mode 100644 index 0000000..08159b5 --- /dev/null +++ b/resources/clj/src/main.clj @@ -0,0 +1,7 @@ +(ns main) + +; Doc + +(defn -one "right" + [] + jldsj iuwejosvdnl) diff --git a/resources/fib.coal b/resources/fib.coal new file mode 100644 index 0000000..f44e66b --- /dev/null +++ b/resources/fib.coal @@ -0,0 +1,14 @@ +(package fibonacci + (import + coalton-prelude) + (export + fob)) + +(declare fib (Integer -> Integer)) +(define (fib n) + "Compute the nth Fibonacci number" + (match n + (0 0) + (1 1) + (_ (+ (fib (- n 1)) + (fib (- n 2)))))) diff --git a/resources/rpc/client_registerCapability.json b/resources/rpc/client_registerCapability.json new file mode 100644 index 0000000..e8bb2c6 --- /dev/null +++ b/resources/rpc/client_registerCapability.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","method":"client/registerCapability","params":{"registrations":[{"id":"id","method":"workspace/didChangeWatchedFiles","registerOptions":{"watchers":[{"globPattern":"**/*.{clj,cljs,cljc,cljd,edn,bb,clj_kondo}"}]}}]},"id":1} diff --git a/resources/rpc/client_registerCapability_result.json b/resources/rpc/client_registerCapability_result.json new file mode 100644 index 0000000..cbfcf36 --- /dev/null +++ b/resources/rpc/client_registerCapability_result.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":1,"result":null} diff --git a/resources/rpc/initialize.json b/resources/rpc/initialize.json new file mode 100644 index 0000000..4aec027 --- /dev/null +++ b/resources/rpc/initialize.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":22848,"clientInfo":{"name":"Eglot","version":"1.17"},"rootPath":"/Users/jlbouwman/git/coalton-mode/","rootUri":"file:///Users/jlbouwman/git/coalton-mode","initializationOptions":{},"capabilities":{"workspace":{"applyEdit":true,"executeCommand":{"dynamicRegistration":false},"workspaceEdit":{"documentChanges":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":false},"configuration":true,"workspaceFolders":true},"textDocument":{"synchronization":{"dynamicRegistration":false,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":false,"completionItem":{"snippetSupport":false,"deprecatedSupport":true,"resolveSupport":{"properties":["documentation","details","additionalTextEdits"]},"tagSupport":{"valueSet":[1]}},"contextSupport":true},"hover":{"dynamicRegistration":false,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":false,"signatureInformation":{"parameterInformation":{"labelOffsetSupport":true},"documentationFormat":["markdown","plaintext"],"activeParameterSupport":true}},"references":{"dynamicRegistration":false},"definition":{"dynamicRegistration":false,"linkSupport":true},"declaration":{"dynamicRegistration":false,"linkSupport":true},"implementation":{"dynamicRegistration":false,"linkSupport":true},"typeDefinition":{"dynamicRegistration":false,"linkSupport":true},"documentSymbol":{"dynamicRegistration":false,"hierarchicalDocumentSymbolSupport":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"documentHighlight":{"dynamicRegistration":false},"codeAction":{"dynamicRegistration":false,"resolveSupport":{"properties":["edit","command"]},"dataSupport":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"isPreferredSupport":true},"formatting":{"dynamicRegistration":false},"rangeFormatting":{"dynamicRegistration":false},"rename":{"dynamicRegistration":false},"inlayHint":{"dynamicRegistration":false},"publishDiagnostics":{"relatedInformation":false,"codeDescriptionSupport":false,"tagSupport":{"valueSet":[1,2]}}},"window":{"showDocument":{"support":true},"workDoneProgress":true},"general":{"positionEncodings":["utf-32","utf-8","utf-16"]},"experimental":{}},"workspaceFolders":[{"uri":"file:///Users/jlbouwman/git/coalton-mode","name":"~/git/coalton-mode/"}]}} diff --git a/resources/rpc/initialize_result.json b/resources/rpc/initialize_result.json new file mode 100644 index 0000000..f0e9100 --- /dev/null +++ b/resources/rpc/initialize_result.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"documentSymbolProvider":true,"textDocumentSync":{"openClose":true,"change":1,"save":{"includeText":true}},"declarationProvider":true,"semanticTokensProvider":{"legend":{"tokenTypes":["namespace","type","function","macro","keyword","class","variable","method","event","interface"],"tokenModifiers":["definition","defaultLibrary","implementation"]},"range":true,"full":true},"linkedEditingRangeProvider":true,"workspaceSymbolProvider":true,"experimental":{"testTree":true,"projectTree":true,"cursorInfo":true,"serverInfo":true,"clojuredocs":true},"codeActionProvider":{"codeActionKinds":["quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]},"documentHighlightProvider":true,"completionProvider":{"resolveProvider":true,"triggerCharacters":[":","/"]},"workspace":{"fileOperations":{"willRename":{"filters":[{"scheme":"file","pattern":{"glob":"**/*.{clj,cljs,cljc,cljd,edn,bb,clj_kondo}","matches":"file"}}]},"didRename":{"filters":[{"scheme":"file","pattern":{"glob":"**/*.{clj,cljs,cljc,cljd,edn,bb,clj_kondo}","matches":"file"}}]}}},"implementationProvider":true,"signatureHelpProvider":{"triggerCharacters":[]},"documentRangeFormattingProvider":true,"executeCommandProvider":{"commands":["add-import-to-namespace","add-missing-import","add-missing-libspec","add-require-suggestion","backward-barf","backward-slurp","change-coll","clean-ns","create-function","create-test","cursor-info","cycle-coll","cycle-keyword-auto-resolve","cycle-privacy","demote-fn","destructure-keys","drag-backward","drag-forward","drag-param-backward","drag-param-forward","expand-let","extract-function","extract-to-def","forward-barf","forward-slurp","get-in-all","get-in-less","get-in-more","get-in-none","inline-symbol","introduce-let","kill-sexp","move-coll-entry-down","move-coll-entry-up","move-form","move-to-let","promote-fn","raise-sexp","replace-refer-all-with-alias","replace-refer-all-with-refer","resolve-macro-as","restructure-keys","server-info","sort-clauses","sort-map","suppress-diagnostic","thread-first","thread-first-all","thread-last","thread-last-all","unwind-all","unwind-thread"]},"referencesProvider":true,"codeLensProvider":{"resolveProvider":true},"foldingRangeProvider":true,"callHierarchyProvider":true,"documentFormattingProvider":true,"renameProvider":{"prepareProvider":true},"definitionProvider":true,"hoverProvider":true}}} diff --git a/resources/rpc/initialized.json b/resources/rpc/initialized.json new file mode 100644 index 0000000..8a9ebd2 --- /dev/null +++ b/resources/rpc/initialized.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","method":"initialized","params":{}} diff --git a/resources/rpc/textDocument_didOpen.json b/resources/rpc/textDocument_didOpen.json new file mode 100644 index 0000000..970413b --- /dev/null +++ b/resources/rpc/textDocument_didOpen.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///Users/jbouwman/git/coalton-mode/resources/clj/src/main.clj","version":0,"languageId":"clojure","text":"(ns main)\n\n; Doc\n\n(defn -one \"right\"\n []\n jldsj iuwejosvdnl)\n"}}} diff --git a/resources/rpc/textDocument_documentHighlight.json b/resources/rpc/textDocument_documentHighlight.json new file mode 100644 index 0000000..3ef06b2 --- /dev/null +++ b/resources/rpc/textDocument_documentHighlight.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":3,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"file:///Users/jbouwman/git/coalton-mode/resources/clj/src/main.clj"},"position":{"line":0,"character":0}}} diff --git a/resources/rpc/textDocument_documentHighlight_result.json b/resources/rpc/textDocument_documentHighlight_result.json new file mode 100644 index 0000000..0158781 --- /dev/null +++ b/resources/rpc/textDocument_documentHighlight_result.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":3,"result":[]} diff --git a/resources/rpc/textDocument_hover.json b/resources/rpc/textDocument_hover.json new file mode 100644 index 0000000..516907c --- /dev/null +++ b/resources/rpc/textDocument_hover.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":2,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///Users/jbouwman/git/coalton-mode/resources/clj/src/main.clj"},"position":{"line":0,"character":0}}} diff --git a/resources/rpc/textDocument_hover_result.json b/resources/rpc/textDocument_hover_result.json new file mode 100644 index 0000000..b38a16b --- /dev/null +++ b/resources/rpc/textDocument_hover_result.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":2,"result":{"contents":[]}} diff --git a/resources/rpc/textDocument_publishDiagnostics.json b/resources/rpc/textDocument_publishDiagnostics.json new file mode 100644 index 0000000..e8702f2 --- /dev/null +++ b/resources/rpc/textDocument_publishDiagnostics.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:///Users/jbouwman/git/coalton-mode/resources/clj/src/main.clj","diagnostics":[{"range":{"start":{"line":6,"character":2},"end":{"line":6,"character":7}},"tags":[],"message":"Unused value: jldsj","code":"unused-value","langs":[],"severity":2,"source":"clj-kondo"},{"range":{"start":{"line":6,"character":2},"end":{"line":6,"character":7}},"tags":[],"message":"Unresolved symbol: jldsj","code":"unresolved-symbol","langs":[],"severity":1,"source":"clj-kondo"},{"range":{"start":{"line":6,"character":8},"end":{"line":6,"character":19}},"tags":[],"message":"Unresolved symbol: iuwejosvdnl","code":"unresolved-symbol","langs":[],"severity":1,"source":"clj-kondo"}]}} diff --git a/resources/rpc/textDocument_signatureHelp.json b/resources/rpc/textDocument_signatureHelp.json new file mode 100644 index 0000000..ff1440c --- /dev/null +++ b/resources/rpc/textDocument_signatureHelp.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":4,"method":"textDocument/signatureHelp","params":{"textDocument":{"uri":"file:///Users/jbouwman/git/coalton-mode/resources/clj/src/main.clj"},"position":{"line":0,"character":0}}} diff --git a/resources/rpc/textDocument_signatureHelp_result.json b/resources/rpc/textDocument_signatureHelp_result.json new file mode 100644 index 0000000..266d4f6 --- /dev/null +++ b/resources/rpc/textDocument_signatureHelp_result.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":4,"result":null} diff --git a/resources/rpc/workspace_didChangeConfiguration.json b/resources/rpc/workspace_didChangeConfiguration.json new file mode 100644 index 0000000..822b43e --- /dev/null +++ b/resources/rpc/workspace_didChangeConfiguration.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{}}} diff --git a/resources/spec.ts b/resources/spec.ts new file mode 100644 index 0000000..72e40c8 --- /dev/null +++ b/resources/spec.ts @@ -0,0 +1,2260 @@ +// Language Server Protocol 3.17 + +type integer = number; + +type uinteger = number; + +type decimal = number; + +type LSPAny = LSPObject | LSPArray | string | integer | uinteger | + decimal | boolean | null; + +type LSPObject = { [key: string]: LSPAny }; + +type LSPArray = LSPAny[]; + +interface Message { + jsonrpc: string; +} + +interface RequestMessage extends Message { + id: integer | string; + method: string; + params?: array | object; +} + +interface ResponseMessage extends Message { + id: integer | string | null; + result?: LSPAny; + error?: ResponseError; +} + +interface ResponseError { + code: integer; + message: string; + data?: LSPAny; +} + +namespace ErrorCodes { + ParseError: integer = -32700; + InvalidRequest: integer = -32600; + MethodNotFound: integer = -32601; + InvalidParams: integer = -32602; + InternalError: integer = -32603; + jsonrpcReservedErrorRangeStart: integer = -32099; + serverErrorStart: integer = jsonrpcReservedErrorRangeStart; + ServerNotInitialized: integer = -32002; + UnknownErrorCode: integer = -32001; + jsonrpcReservedErrorRangeEnd = -32000; + serverErrorEnd: integer = jsonrpcReservedErrorRangeEnd; + lspReservedErrorRangeStart: integer = -32899; + RequestFailed: integer = -32803; + ServerCancelled: integer = -32802; + ContentModified: integer = -32801; + RequestCancelled: integer = -32800; + lspReservedErrorRangeEnd: integer = -32800; +} + +interface NotificationMessage extends Message { + method: string; + params?: array | object; +} + +interface CancelParams { + id: integer | string; +} + +type ProgressToken = integer | string; + +interface ProgressParams& lt; T & gt; { + token: ProgressToken; + value: T; +} + +interface HoverParams { + textDocument: string; position: { line: uinteger; character: uinteger; }; +} + +interface HoverResult { + value: string; +} + +type DocumentUri = string; + +type URI = string; + +interface RegularExpressionsClientCapabilities { + engine: string; + version?: string; +} +EOL: string[] = ['\n', '\r\n', '\r']; + +interface Position { + line: uinteger; + character: uinteger; +} + +type PositionEncodingKind = string; + +namespace PositionEncodingKind { + UTF8: PositionEncodingKind = 'utf-8'; + UTF16: PositionEncodingKind = 'utf-16'; + UTF32: PositionEncodingKind = 'utf-32'; +} + +interface Range { + start: Position; + end: Position; +} + +interface TextDocumentItem { + uri: DocumentUri; + languageId: string; + version: integer; + text: string; +} + +interface TextDocumentIdentifier { + uri: DocumentUri; +} + +interface VersionedTextDocumentIdentifier extends TextDocumentIdentifier { + version: integer; +} + +interface OptionalVersionedTextDocumentIdentifier extends TextDocumentIdentifier { + version: integer | null; +} + +interface TextDocumentPositionParams { + textDocument: TextDocumentIdentifier; + position: Position; +} + +interface DocumentFilter { + language?: string; + scheme?: string; + pattern?: string; +} + +type DocumentSelector = DocumentFilter[]; + +interface TextEdit { + range: Range; + newText: string; +} + +interface ChangeAnnotation { + label: string; + needsConfirmation?: boolean; + description?: string; +} + +type ChangeAnnotationIdentifier = string; + +interface AnnotatedTextEdit extends TextEdit { + annotationId: ChangeAnnotationIdentifier; +} + +interface TextDocumentEdit { + textDocument: OptionalVersionedTextDocumentIdentifier; + edits: (TextEdit | AnnotatedTextEdit)[]; +} + +interface Location { + uri: DocumentUri; + range: Range; +} + +interface LocationLink { + originSelectionRange?: Range; + targetUri: DocumentUri; + targetRange: Range; + targetSelectionRange: Range; +} + +interface Diagnostic { + range: Range; + severity?: DiagnosticSeverity; + code?: integer | string; + codeDescription?: CodeDescription; + source?: string; + message: string; + tags?: DiagnosticTag[]; + relatedInformation?: DiagnosticRelatedInformation[]; + data?: LSPAny; +} + +namespace DiagnosticSeverity { + Error: 1 = 1; + Warning: 2 = 2; + Information: 3 = 3; + Hint: 4 = 4; +} + +type DiagnosticSeverity = 1 | 2 | 3 | 4; + +namespace DiagnosticTag { + Unnecessary: 1 = 1; + Deprecated: 2 = 2; +} + +type DiagnosticTag = 1 | 2; + +interface DiagnosticRelatedInformation { + location: Location; + message: string; +} + +interface CodeDescription { + href: URI; +} + +interface Command { + title: string; + command: string; + arguments?: LSPAny[]; +} + +namespace MarkupKind { + PlainText: 'plaintext' = 'plaintext'; + Markdown: 'markdown' = 'markdown'; +} + +type MarkupKind = 'plaintext' | 'markdown'; + +interface MarkupContent { + kind: MarkupKind; + value: string; +} + +interface MarkdownClientCapabilities { + parser: string; + version?: string; + allowedTags?: string[]; +} + +interface CreateFileOptions { + overwrite?: boolean; + ignoreIfExists?: boolean; +} + +interface CreateFile { + kind: 'create'; + uri: DocumentUri; + options?: CreateFileOptions; + annotationId?: ChangeAnnotationIdentifier; +} + +interface RenameFileOptions { + overwrite?: boolean; + ignoreIfExists?: boolean; +} + +interface RenameFile { + kind: 'rename'; + oldUri: DocumentUri; + newUri: DocumentUri; + options?: RenameFileOptions; + annotationId?: ChangeAnnotationIdentifier; +} + +interface DeleteFileOptions { + recursive?: boolean; + ignoreIfNotExists?: boolean; +} + +interface DeleteFile { + kind: 'delete'; + uri: DocumentUri; + options?: DeleteFileOptions; + annotationId?: ChangeAnnotationIdentifier; +} + +interface WorkspaceEdit { + changes?: { [uri: DocumentUri]: TextEdit[]; }; + documentChanges?: ( + TextDocumentEdit[] | + (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[] + ); + changeAnnotations?: { + [id: string /* ChangeAnnotationIdentifier */]: ChangeAnnotation; + }; +} + +interface WorkspaceEditClientCapabilities { + documentChanges?: boolean; + resourceOperations?: ResourceOperationKind[]; + failureHandling?: FailureHandlingKind; + normalizesLineEndings?: boolean; + changeAnnotationSupport?: { + groupsOnLabel?: boolean; + }; +} + +type ResourceOperationKind = 'create' | 'rename' | 'delete'; + +namespace ResourceOperationKind { + Create: ResourceOperationKind = 'create'; + Rename: ResourceOperationKind = 'rename'; + Delete: ResourceOperationKind = 'delete'; +} + +type FailureHandlingKind = 'abort' | 'transactional' | 'undo' + | 'textOnlyTransactional'; + +namespace FailureHandlingKind { + Abort: FailureHandlingKind = 'abort'; + Transactional: FailureHandlingKind = 'transactional'; + TextOnlyTransactional: FailureHandlingKind + = 'textOnlyTransactional'; + Undo: FailureHandlingKind = 'undo'; +} + +interface WorkDoneProgressBegin { + kind: 'begin'; + title: string; + cancellable?: boolean; + message?: string; + percentage?: uinteger; +} + +interface WorkDoneProgressReport { + kind: 'report'; + cancellable?: boolean; + message?: string; + percentage?: uinteger; +} + +interface WorkDoneProgressEnd { + kind: 'end'; + message?: string; +} + +interface WorkDoneProgressParams { + workDoneToken?: ProgressToken; +} + +interface WorkDoneProgressOptions { + workDoneProgress?: boolean; +} + +interface PartialResultParams { + partialResultToken?: ProgressToken; +} + +type TraceValue = 'off' | 'messages' | 'verbose'; + +interface InitializeParams extends WorkDoneProgressParams { + processId: integer | null; + clientInfo?: { + name: string; + version?: string; + }; + locale?: string; + rootPath?: string | null; + rootUri: DocumentUri | null; + initializationOptions?: LSPAny; + capabilities: ClientCapabilities; + trace?: TraceValue; + workspaceFolders?: WorkspaceFolder[] | null; +} + +interface TextDocumentClientCapabilities { + synchronization?: TextDocumentSyncClientCapabilities; + completion?: CompletionClientCapabilities; + hover?: HoverClientCapabilities; + signatureHelp?: SignatureHelpClientCapabilities; + declaration?: DeclarationClientCapabilities; + definition?: DefinitionClientCapabilities; + typeDefinition?: TypeDefinitionClientCapabilities; + implementation?: ImplementationClientCapabilities; + references?: ReferenceClientCapabilities; + documentHighlight?: DocumentHighlightClientCapabilities; + documentSymbol?: DocumentSymbolClientCapabilities; + codeAction?: CodeActionClientCapabilities; + codeLens?: CodeLensClientCapabilities; + documentLink?: DocumentLinkClientCapabilities; + colorProvider?: DocumentColorClientCapabilities; + formatting?: DocumentFormattingClientCapabilities; + rangeFormatting?: DocumentRangeFormattingClientCapabilities; + onTypeFormatting?: DocumentOnTypeFormattingClientCapabilities; + rename?: RenameClientCapabilities; + publishDiagnostics?: PublishDiagnosticsClientCapabilities; + foldingRange?: FoldingRangeClientCapabilities; + selectionRange?: SelectionRangeClientCapabilities; + linkedEditingRange?: LinkedEditingRangeClientCapabilities; + callHierarchy?: CallHierarchyClientCapabilities; + semanticTokens?: SemanticTokensClientCapabilities; + moniker?: MonikerClientCapabilities; + typeHierarchy?: TypeHierarchyClientCapabilities; + inlineValue?: InlineValueClientCapabilities; + inlayHint?: InlayHintClientCapabilities; + diagnostic?: DiagnosticClientCapabilities; +} + +interface NotebookDocumentClientCapabilities { + synchronization: NotebookDocumentSyncClientCapabilities; +} + +interface ClientCapabilities { + workspace?: { + applyEdit?: boolean; + workspaceEdit?: WorkspaceEditClientCapabilities; + didChangeConfiguration?: DidChangeConfigurationClientCapabilities; + didChangeWatchedFiles?: DidChangeWatchedFilesClientCapabilities; + symbol?: WorkspaceSymbolClientCapabilities; + executeCommand?: ExecuteCommandClientCapabilities; + workspaceFolders?: boolean; + configuration?: boolean; + semanticTokens?: SemanticTokensWorkspaceClientCapabilities; + codeLens?: CodeLensWorkspaceClientCapabilities; + fileOperations?: { + dynamicRegistration?: boolean; + didCreate?: boolean; + willCreate?: boolean; + didRename?: boolean; + willRename?: boolean; + didDelete?: boolean; + willDelete?: boolean; + }; + inlineValue?: InlineValueWorkspaceClientCapabilities; + inlayHint?: InlayHintWorkspaceClientCapabilities; + diagnostics?: DiagnosticWorkspaceClientCapabilities; + }; + textDocument?: TextDocumentClientCapabilities; + notebookDocument?: NotebookDocumentClientCapabilities; + window?: { + workDoneProgress?: boolean; + showMessage?: ShowMessageRequestClientCapabilities; + showDocument?: ShowDocumentClientCapabilities; + }; + general?: { + staleRequestSupport?: { + cancel: boolean; + retryOnContentModified: string[]; + } + regularExpressions?: RegularExpressionsClientCapabilities; + markdown?: MarkdownClientCapabilities; + positionEncodings?: PositionEncodingKind[]; + }; + experimental?: LSPAny; +} + +interface InitializeResult { + capabilities: ServerCapabilities; + serverInfo?: { + name: string; + version?: string; + }; +} + +namespace InitializeErrorCodes { + unknownProtocolVersion: 1 = 1; +} + +type InitializeErrorCodes = 1; + +interface InitializeError { + retry: boolean; +} + +interface ServerCapabilities { + positionEncoding?: PositionEncodingKind; + textDocumentSync?: TextDocumentSyncOptions | TextDocumentSyncKind; + notebookDocumentSync?: NotebookDocumentSyncOptions + | NotebookDocumentSyncRegistrationOptions; + completionProvider?: CompletionOptions; + hoverProvider?: boolean | HoverOptions; + signatureHelpProvider?: SignatureHelpOptions; + declarationProvider?: boolean | DeclarationOptions + | DeclarationRegistrationOptions; + definitionProvider?: boolean | DefinitionOptions; + typeDefinitionProvider?: boolean | TypeDefinitionOptions + | TypeDefinitionRegistrationOptions; + implementationProvider?: boolean | ImplementationOptions + | ImplementationRegistrationOptions; + referencesProvider?: boolean | ReferenceOptions; + documentHighlightProvider?: boolean | DocumentHighlightOptions; + documentSymbolProvider?: boolean | DocumentSymbolOptions; + codeActionProvider?: boolean | CodeActionOptions; + codeLensProvider?: CodeLensOptions; + documentLinkProvider?: DocumentLinkOptions; + colorProvider?: boolean | DocumentColorOptions + | DocumentColorRegistrationOptions; + documentFormattingProvider?: boolean | DocumentFormattingOptions; + documentRangeFormattingProvider?: boolean | DocumentRangeFormattingOptions; + documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions; + renameProvider?: boolean | RenameOptions; + foldingRangeProvider?: boolean | FoldingRangeOptions + | FoldingRangeRegistrationOptions; + executeCommandProvider?: ExecuteCommandOptions; + selectionRangeProvider?: boolean | SelectionRangeOptions + | SelectionRangeRegistrationOptions; + linkedEditingRangeProvider?: boolean | LinkedEditingRangeOptions + | LinkedEditingRangeRegistrationOptions; + callHierarchyProvider?: boolean | CallHierarchyOptions + | CallHierarchyRegistrationOptions; + semanticTokensProvider?: SemanticTokensOptions + | SemanticTokensRegistrationOptions; + monikerProvider?: boolean | MonikerOptions | MonikerRegistrationOptions; + typeHierarchyProvider?: boolean | TypeHierarchyOptions + | TypeHierarchyRegistrationOptions; + inlineValueProvider?: boolean | InlineValueOptions + | InlineValueRegistrationOptions; + inlayHintProvider?: boolean | InlayHintOptions + | InlayHintRegistrationOptions; + diagnosticProvider?: DiagnosticOptions | DiagnosticRegistrationOptions; + workspaceSymbolProvider?: boolean | WorkspaceSymbolOptions; + workspace?: { + workspaceFolders?: WorkspaceFoldersServerCapabilities; + fileOperations?: { + didCreate?: FileOperationRegistrationOptions; + willCreate?: FileOperationRegistrationOptions; + didRename?: FileOperationRegistrationOptions; + willRename?: FileOperationRegistrationOptions; + didDelete?: FileOperationRegistrationOptions; + willDelete?: FileOperationRegistrationOptions; + }; + }; + experimental?: LSPAny; +} + +interface InitializedParams { +} + +interface Registration { + id: string; + method: string; + registerOptions?: LSPAny; +} + +interface RegistrationParams { + registrations: Registration[]; +} + +interface StaticRegistrationOptions { + id?: string; +} + +interface TextDocumentRegistrationOptions { + documentSelector: DocumentSelector | null; +} + +interface Unregistration { + id: string; + method: string; +} + +interface UnregistrationParams { + unregisterations: Unregistration[]; +} + +interface SetTraceParams { + value: TraceValue; +} + +interface LogTraceParams { + message: string; + verbose?: string; +} + +namespace TextDocumentSyncKind { + None = 0; + Full = 1; + Incremental = 2; +} + +type TextDocumentSyncKind = 0 | 1 | 2; + +interface TextDocumentSyncOptions { + openClose?: boolean; + change?: TextDocumentSyncKind; +} + +interface DidOpenTextDocumentParams { + textDocument: TextDocumentItem; +} + +interface TextDocumentChangeRegistrationOptions + extends TextDocumentRegistrationOptions { + syncKind: TextDocumentSyncKind; +} + +interface DidChangeTextDocumentParams { + textDocument: VersionedTextDocumentIdentifier; + contentChanges: TextDocumentContentChangeEvent[]; +} + +type TextDocumentContentChangeEvent = { + range: Range; + rangeLength?: uinteger; + text: string; +} | { + text: string; +}; + +interface WillSaveTextDocumentParams { + textDocument: TextDocumentIdentifier; + reason: TextDocumentSaveReason; +} + +namespace TextDocumentSaveReason { + Manual = 1; + AfterDelay = 2; + FocusOut = 3; +} + +type TextDocumentSaveReason = 1 | 2 | 3; + +interface SaveOptions { + includeText?: boolean; +} + +interface TextDocumentSaveRegistrationOptions + extends TextDocumentRegistrationOptions { + includeText?: boolean; +} + +interface DidSaveTextDocumentParams { + textDocument: TextDocumentIdentifier; + text?: string; +} + +interface DidCloseTextDocumentParams { + textDocument: TextDocumentIdentifier; +} + +interface TextDocumentSyncClientCapabilities { + dynamicRegistration?: boolean; + willSave?: boolean; + willSaveWaitUntil?: boolean; + didSave?: boolean; +} + +interface TextDocumentSyncOptions { + openClose?: boolean; + change?: TextDocumentSyncKind; + willSave?: boolean; + willSaveWaitUntil?: boolean; + save?: boolean | SaveOptions; +} + +interface NotebookDocument { + uri: URI; + notebookType: string; + version: integer; + metadata?: LSPObject; + cells: NotebookCell[]; +} + +interface NotebookCell { + kind: NotebookCellKind; + document: DocumentUri; + metadata?: LSPObject; + executionSummary?: ExecutionSummary; +} + +namespace NotebookCellKind { + Markup: 1 = 1; + Code: 2 = 2; +} + +interface ExecutionSummary { + executionOrder: uinteger; + success?: boolean; +} + +interface NotebookCellTextDocumentFilter { + notebook: string | NotebookDocumentFilter; + language?: string; +} + +type NotebookDocumentFilter = { + notebookType: string; + scheme?: string; + pattern?: string; +} | { + notebookType?: string; + scheme: string; + pattern?: string; +} | { + notebookType?: string; + scheme?: string; + pattern: string; +}; + +interface NotebookDocumentSyncClientCapabilities { + dynamicRegistration?: boolean; + executionSummarySupport?: boolean; +} + +interface NotebookDocumentSyncOptions { + notebookSelector: ({ + notebook: string | NotebookDocumentFilter; + cells?: { language: string }[]; + } | { + notebook?: string | NotebookDocumentFilter; + cells: { language: string }[]; + })[]; + save?: boolean; +} + +interface NotebookDocumentSyncRegistrationOptions extends + NotebookDocumentSyncOptions, StaticRegistrationOptions { +} + +interface DidOpenNotebookDocumentParams { + notebookDocument: NotebookDocument; + cellTextDocuments: TextDocumentItem[]; +} + +interface DidChangeNotebookDocumentParams { + notebookDocument: VersionedNotebookDocumentIdentifier; + change: NotebookDocumentChangeEvent; +} + +interface VersionedNotebookDocumentIdentifier { + version: integer; + uri: URI; +} + +interface NotebookDocumentChangeEvent { + metadata?: LSPObject; + cells?: { + structure?: { + array: NotebookCellArrayChange; + didOpen?: TextDocumentItem[]; + didClose?: TextDocumentIdentifier[]; + }; + data?: NotebookCell[]; + textContent?: { + document: VersionedTextDocumentIdentifier; + changes: TextDocumentContentChangeEvent[]; + }[]; + }; +} + +interface NotebookCellArrayChange { + start: uinteger; + deleteCount: uinteger; + cells?: NotebookCell[]; +} + +interface DidSaveNotebookDocumentParams { + notebookDocument: NotebookDocumentIdentifier; +} + +interface DidCloseNotebookDocumentParams { + notebookDocument: NotebookDocumentIdentifier; + cellTextDocuments: TextDocumentIdentifier[]; +} + +interface NotebookDocumentIdentifier { + uri: URI; +} + +interface DeclarationClientCapabilities { + dynamicRegistration?: boolean; + linkSupport?: boolean; +} + +interface DeclarationOptions extends WorkDoneProgressOptions { +} + +interface DeclarationRegistrationOptions extends DeclarationOptions, + TextDocumentRegistrationOptions, StaticRegistrationOptions { +} + +interface DeclarationParams extends TextDocumentPositionParams, + WorkDoneProgressParams, PartialResultParams { +} + +interface DefinitionClientCapabilities { + dynamicRegistration?: boolean; + linkSupport?: boolean; +} + +interface DefinitionOptions extends WorkDoneProgressOptions { +} + +interface DefinitionRegistrationOptions extends + TextDocumentRegistrationOptions, DefinitionOptions { +} + +interface DefinitionParams extends TextDocumentPositionParams, + WorkDoneProgressParams, PartialResultParams { +} + +interface TypeDefinitionClientCapabilities { + dynamicRegistration?: boolean; + linkSupport?: boolean; +} + +interface TypeDefinitionOptions extends WorkDoneProgressOptions { +} + +interface TypeDefinitionRegistrationOptions extends + TextDocumentRegistrationOptions, TypeDefinitionOptions, + StaticRegistrationOptions { +} + +interface TypeDefinitionParams extends TextDocumentPositionParams, + WorkDoneProgressParams, PartialResultParams { +} + +interface ImplementationClientCapabilities { + dynamicRegistration?: boolean; + linkSupport?: boolean; +} + +interface ImplementationOptions extends WorkDoneProgressOptions { +} + +interface ImplementationRegistrationOptions extends + TextDocumentRegistrationOptions, ImplementationOptions, + StaticRegistrationOptions { +} + +interface ImplementationParams extends TextDocumentPositionParams, + WorkDoneProgressParams, PartialResultParams { +} + +interface ReferenceClientCapabilities { + dynamicRegistration?: boolean; +} + +interface ReferenceOptions extends WorkDoneProgressOptions { +} + +interface ReferenceRegistrationOptions extends + TextDocumentRegistrationOptions, ReferenceOptions { +} + +interface ReferenceParams extends TextDocumentPositionParams, + WorkDoneProgressParams, PartialResultParams { + context: ReferenceContext; +} + +interface ReferenceContext { + includeDeclaration: boolean; +} + +interface CallHierarchyClientCapabilities { + dynamicRegistration?: boolean; +} + +interface CallHierarchyOptions extends WorkDoneProgressOptions { +} + +interface CallHierarchyRegistrationOptions extends + TextDocumentRegistrationOptions, CallHierarchyOptions, + StaticRegistrationOptions { +} + +interface CallHierarchyPrepareParams extends TextDocumentPositionParams, + WorkDoneProgressParams { +} + +interface CallHierarchyItem { + name: string; + kind: SymbolKind; + tags?: SymbolTag[]; + detail?: string; + uri: DocumentUri; + range: Range; + selectionRange: Range; + data?: LSPAny; +} + +interface CallHierarchyIncomingCallsParams extends + WorkDoneProgressParams, PartialResultParams { + item: CallHierarchyItem; +} + +interface CallHierarchyIncomingCall { + from: CallHierarchyItem; + fromRanges: Range[]; +} + +interface CallHierarchyOutgoingCallsParams extends + WorkDoneProgressParams, PartialResultParams { + item: CallHierarchyItem; +} + +interface CallHierarchyOutgoingCall { + to: CallHierarchyItem; + fromRanges: Range[]; +} + +type TypeHierarchyClientCapabilities = { + dynamicRegistration?: boolean; +}; + +interface TypeHierarchyOptions extends WorkDoneProgressOptions { +} + +interface TypeHierarchyRegistrationOptions extends + TextDocumentRegistrationOptions, TypeHierarchyOptions, + StaticRegistrationOptions { +} + +interface TypeHierarchyPrepareParams extends TextDocumentPositionParams, + WorkDoneProgressParams { +} + +interface TypeHierarchyItem { + name: string; + kind: SymbolKind; + tags?: SymbolTag[]; + detail?: string; + uri: DocumentUri; + range: Range; + selectionRange: Range; + data?: LSPAny; +} + +interface TypeHierarchySupertypesParams extends + WorkDoneProgressParams, PartialResultParams { + item: TypeHierarchyItem; +} + +interface TypeHierarchySubtypesParams extends + WorkDoneProgressParams, PartialResultParams { + item: TypeHierarchyItem; +} + +interface DocumentHighlightClientCapabilities { + dynamicRegistration?: boolean; +} + +interface DocumentHighlightOptions extends WorkDoneProgressOptions { +} + +interface DocumentHighlightRegistrationOptions extends + TextDocumentRegistrationOptions, DocumentHighlightOptions { +} + +interface DocumentHighlightParams extends TextDocumentPositionParams, + WorkDoneProgressParams, PartialResultParams { +} + +interface DocumentHighlight { + range: Range; + kind?: DocumentHighlightKind; +} + +namespace DocumentHighlightKind { + Text = 1; + Read = 2; + Write = 3; +} + +type DocumentHighlightKind = 1 | 2 | 3; + +interface DocumentLinkClientCapabilities { + dynamicRegistration?: boolean; + tooltipSupport?: boolean; +} + +interface DocumentLinkOptions extends WorkDoneProgressOptions { + resolveProvider?: boolean; +} + +interface DocumentLinkRegistrationOptions extends + TextDocumentRegistrationOptions, DocumentLinkOptions { +} + +interface DocumentLinkParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; +} + +interface DocumentLink { + range: Range; + target?: URI; + tooltip?: string; + data?: LSPAny; +} + +interface HoverClientCapabilities { + dynamicRegistration?: boolean; + contentFormat?: MarkupKind[]; +} + +interface HoverOptions extends WorkDoneProgressOptions { +} + +interface HoverRegistrationOptions + extends TextDocumentRegistrationOptions, HoverOptions { +} + +interface HoverParams extends TextDocumentPositionParams, + WorkDoneProgressParams { +} + +interface Hover { + contents: MarkedString | MarkedString[] | MarkupContent; + range?: Range; +} + +type MarkedString = string | { language: string; value: string }; + +interface CodeLensClientCapabilities { + dynamicRegistration?: boolean; +} + +interface CodeLensOptions extends WorkDoneProgressOptions { + resolveProvider?: boolean; +} + +interface CodeLensRegistrationOptions extends + TextDocumentRegistrationOptions, CodeLensOptions { +} + +interface CodeLensParams extends WorkDoneProgressParams, PartialResultParams { + textDocument: TextDocumentIdentifier; +} + +interface CodeLens { + range: Range; + command?: Command; + data?: LSPAny; +} + +interface CodeLensWorkspaceClientCapabilities { + refreshSupport?: boolean; +} + +interface FoldingRangeClientCapabilities { + dynamicRegistration?: boolean; + rangeLimit?: uinteger; + lineFoldingOnly?: boolean; + foldingRangeKind?: { + valueSet?: FoldingRangeKind[]; + }; + foldingRange?: { + collapsedText?: boolean; + }; +} + +interface FoldingRangeOptions extends WorkDoneProgressOptions { +} + +interface FoldingRangeRegistrationOptions extends + TextDocumentRegistrationOptions, FoldingRangeOptions, + StaticRegistrationOptions { +} + +interface FoldingRangeParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; +} + +namespace FoldingRangeKind { + Comment = 'comment'; + Imports = 'imports'; + Region = 'region'; +} + +type FoldingRangeKind = string; + +interface FoldingRange { + startLine: uinteger; + startCharacter?: uinteger; + endLine: uinteger; + endCharacter?: uinteger; + kind?: FoldingRangeKind; + collapsedText?: string; +} + +interface SelectionRangeClientCapabilities { + dynamicRegistration?: boolean; +} + +interface SelectionRangeOptions extends WorkDoneProgressOptions { +} + +interface SelectionRangeRegistrationOptions extends + SelectionRangeOptions, TextDocumentRegistrationOptions, + StaticRegistrationOptions { +} + +interface SelectionRangeParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; + positions: Position[]; +} + +interface SelectionRange { + range: Range; + parent?: SelectionRange; +} + +interface DocumentSymbolClientCapabilities { + dynamicRegistration?: boolean; + symbolKind?: { + valueSet?: SymbolKind[]; + }; + hierarchicalDocumentSymbolSupport?: boolean; + tagSupport?: { + valueSet: SymbolTag[]; + }; + labelSupport?: boolean; +} + +interface DocumentSymbolOptions extends WorkDoneProgressOptions { + label?: string; +} + +interface DocumentSymbolRegistrationOptions extends + TextDocumentRegistrationOptions, DocumentSymbolOptions { +} + +interface DocumentSymbolParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; +} + +namespace SymbolKind { + File = 1; + Module = 2; + Namespace = 3; + Package = 4; + Class = 5; + Method = 6; + Property = 7; + Field = 8; + Constructor = 9; + Enum = 10; + Interface = 11; + Function = 12; + Variable = 13; + Constant = 14; + String = 15; + Number = 16; + Boolean = 17; + Array = 18; + Object = 19; + Key = 20; + Null = 21; + EnumMember = 22; + Struct = 23; + Event = 24; + Operator = 25; + TypeParameter = 26; +} + +type SymbolKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26; + +namespace SymbolTag { + Deprecated: 1 = 1; +} + +type SymbolTag = 1; + +interface DocumentSymbol { + name: string; + detail?: string; + kind: SymbolKind; + tags?: SymbolTag[]; + deprecated?: boolean; + range: Range; + selectionRange: Range; + children?: DocumentSymbol[]; +} + +interface SymbolInformation { + name: string; + kind: SymbolKind; + tags?: SymbolTag[]; + deprecated?: boolean; + location: Location; + containerName?: string; +} + +enum SemanticTokenTypes { + namespace = 'namespace', + type = 'type', + class = 'class', + enum = 'enum', + interface = 'interface', + struct = 'struct', + typeParameter = 'typeParameter', + parameter = 'parameter', + variable = 'variable', + property = 'property', + enumMember = 'enumMember', + event = 'event', + function = 'function', + method = 'method', + macro = 'macro', + keyword = 'keyword', + modifier = 'modifier', + comment = 'comment', + string = 'string', + number = 'number', + regexp = 'regexp', + operator = 'operator', + decorator = 'decorator' +} + +enum SemanticTokenModifiers { + declaration = 'declaration', + definition = 'definition', + readonly = 'readonly', + static = 'static', + deprecated = 'deprecated', + abstract = 'abstract', + async = 'async', + modification = 'modification', + documentation = 'documentation', + defaultLibrary = 'defaultLibrary' +} + +namespace TokenFormat { + Relative: 'relative' = 'relative'; +} + +type TokenFormat = 'relative'; + +interface SemanticTokensLegend { + tokenTypes: string[]; + tokenModifiers: string[]; +} + +interface SemanticTokensClientCapabilities { + dynamicRegistration?: boolean; + requests: { + range?: boolean | { + }; + full?: boolean | { + delta?: boolean; + }; + }; + tokenTypes: string[]; + tokenModifiers: string[]; + formats: TokenFormat[]; + overlappingTokenSupport?: boolean; + multilineTokenSupport?: boolean; + serverCancelSupport?: boolean; + augmentsSyntaxTokens?: boolean; +} + +interface SemanticTokensOptions extends WorkDoneProgressOptions { + legend: SemanticTokensLegend; + range?: boolean | { + }; + full?: boolean | { + delta?: boolean; + }; +} + +interface SemanticTokensRegistrationOptions extends + TextDocumentRegistrationOptions, SemanticTokensOptions, + StaticRegistrationOptions { +} + +interface SemanticTokensParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; +} + +interface SemanticTokens { + resultId?: string; + data: uinteger[]; +} + +interface SemanticTokensPartialResult { + data: uinteger[]; +} + +interface SemanticTokensDeltaParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; + previousResultId: string; +} + +interface SemanticTokensDelta { + readonly resultId?: string; + edits: SemanticTokensEdit[]; +} + +interface SemanticTokensEdit { + start: uinteger; + deleteCount: uinteger; + data?: uinteger[]; +} + +interface SemanticTokensDeltaPartialResult { + edits: SemanticTokensEdit[]; +} + +interface SemanticTokensRangeParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; + range: Range; +} + +interface SemanticTokensWorkspaceClientCapabilities { + refreshSupport?: boolean; +} + +interface InlayHintClientCapabilities { + dynamicRegistration?: boolean; + resolveSupport?: { + properties: string[]; + }; +} + +interface InlayHintOptions extends WorkDoneProgressOptions { + resolveProvider?: boolean; +} + +interface InlayHintRegistrationOptions extends InlayHintOptions, + TextDocumentRegistrationOptions, StaticRegistrationOptions { +} + +interface InlayHintParams extends WorkDoneProgressParams { + textDocument: TextDocumentIdentifier; + range: Range; +} + +interface InlayHint { + position: Position; + label: string | InlayHintLabelPart[]; + kind?: InlayHintKind; + textEdits?: TextEdit[]; + tooltip?: string | MarkupContent; + paddingLeft?: boolean; + paddingRight?: boolean; + data?: LSPAny; +} + +interface InlayHintLabelPart { + value: string; + tooltip?: string | MarkupContent; + location?: Location; + command?: Command; +} + +namespace InlayHintKind { + Type = 1; + Parameter = 2; +} + +type InlayHintKind = 1 | 2; + +interface InlayHintWorkspaceClientCapabilities { + refreshSupport?: boolean; +} + +interface InlineValueClientCapabilities { + dynamicRegistration?: boolean; +} + +interface InlineValueOptions extends WorkDoneProgressOptions { +} + +interface InlineValueRegistrationOptions extends InlineValueOptions, + TextDocumentRegistrationOptions, StaticRegistrationOptions { +} + +interface InlineValueParams extends WorkDoneProgressParams { + textDocument: TextDocumentIdentifier; + range: Range; + context: InlineValueContext; +} + +interface InlineValueContext { + frameId: integer; + stoppedLocation: Range; +} + +interface InlineValueText { + range: Range; + text: string; +} + +interface InlineValueVariableLookup { + range: Range; + variableName?: string; + caseSensitiveLookup: boolean; +} + +interface InlineValueEvaluatableExpression { + range: Range; + expression?: string; +} + +type InlineValue = InlineValueText | InlineValueVariableLookup + | InlineValueEvaluatableExpression; + +interface InlineValueWorkspaceClientCapabilities { + refreshSupport?: boolean; +} + +interface MonikerClientCapabilities { + dynamicRegistration?: boolean; +} + +interface MonikerOptions extends WorkDoneProgressOptions { +} + +interface MonikerRegistrationOptions extends + TextDocumentRegistrationOptions, MonikerOptions { +} + +interface MonikerParams extends TextDocumentPositionParams, + WorkDoneProgressParams, PartialResultParams { +} + +enum UniquenessLevel { + document = 'document', + project = 'project', + group = 'group', + scheme = 'scheme', + global = 'global' +} + +enum MonikerKind { + import = 'import', + = 'export', + local = 'local' +} + +interface Moniker { + scheme: string; + identifier: string; + unique: UniquenessLevel; + kind?: MonikerKind; +} + +interface CompletionClientCapabilities { + dynamicRegistration?: boolean; + completionItem?: { + snippetSupport?: boolean; + commitCharactersSupport?: boolean; + documentationFormat?: MarkupKind[]; + deprecatedSupport?: boolean; + preselectSupport?: boolean; + tagSupport?: { + valueSet: CompletionItemTag[]; + }; + insertReplaceSupport?: boolean; + resolveSupport?: { + properties: string[]; + }; + insertTextModeSupport?: { + valueSet: InsertTextMode[]; + }; + labelDetailsSupport?: boolean; + }; + completionItemKind?: { + valueSet?: CompletionItemKind[]; + }; + contextSupport?: boolean; + insertTextMode?: InsertTextMode; + completionList?: { + itemDefaults?: string[]; + } +} + +interface CompletionOptions extends WorkDoneProgressOptions { + triggerCharacters?: string[]; + allCommitCharacters?: string[]; + resolveProvider?: boolean; + completionItem?: { + labelDetailsSupport?: boolean; + } +} + +interface CompletionRegistrationOptions + extends TextDocumentRegistrationOptions, CompletionOptions { +} + +interface CompletionParams extends TextDocumentPositionParams, + WorkDoneProgressParams, PartialResultParams { + context?: CompletionContext; +} + +namespace CompletionTriggerKind { + Invoked: 1 = 1; + TriggerCharacter: 2 = 2; + TriggerForIncompleteCompletions: 3 = 3; +} + +type CompletionTriggerKind = 1 | 2 | 3; + +interface CompletionContext { + triggerKind: CompletionTriggerKind; + triggerCharacter?: string; +} + +interface CompletionList { + isIncomplete: boolean; + itemDefaults?: { + commitCharacters?: string[]; + editRange?: Range | { + insert: Range; + replace: Range; + }; + insertTextFormat?: InsertTextFormat; + insertTextMode?: InsertTextMode; + data?: LSPAny; + } + items: CompletionItem[]; +} + +namespace InsertTextFormat { + PlainText = 1; + Snippet = 2; +} + +type InsertTextFormat = 1 | 2; + +namespace CompletionItemTag { + Deprecated = 1; +} + +type CompletionItemTag = 1; + +interface InsertReplaceEdit { + newText: string; + insert: Range; + replace: Range; +} + +namespace InsertTextMode { + asIs: 1 = 1; + adjustIndentation: 2 = 2; +} + +type InsertTextMode = 1 | 2; + +interface CompletionItemLabelDetails { + detail?: string; + description?: string; +} + +interface CompletionItem { + label: string; + labelDetails?: CompletionItemLabelDetails; + kind?: CompletionItemKind; + tags?: CompletionItemTag[]; + detail?: string; + documentation?: string | MarkupContent; + deprecated?: boolean; + preselect?: boolean; + sortText?: string; + filterText?: string; + insertText?: string; + insertTextFormat?: InsertTextFormat; + insertTextMode?: InsertTextMode; + textEdit?: TextEdit | InsertReplaceEdit; + textEditText?: string; + additionalTextEdits?: TextEdit[]; + commitCharacters?: string[]; + command?: Command; + data?: LSPAny; +} + +namespace CompletionItemKind { + Text = 1; + Method = 2; + Function = 3; + Constructor = 4; + Field = 5; + Variable = 6; + Class = 7; + Interface = 8; + Module = 9; + Property = 10; + Unit = 11; + Value = 12; + Enum = 13; + Keyword = 14; + Snippet = 15; + Color = 16; + File = 17; + Reference = 18; + Folder = 19; + EnumMember = 20; + Constant = 21; + Struct = 22; + Event = 23; + Operator = 24; + TypeParameter = 25; +} + +type CompletionItemKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25; + +interface PublishDiagnosticsClientCapabilities { + relatedInformation?: boolean; + tagSupport?: { + valueSet: DiagnosticTag[]; + }; + versionSupport?: boolean; + codeDescriptionSupport?: boolean; + dataSupport?: boolean; +} + +interface PublishDiagnosticsParams { + uri: DocumentUri; + version?: integer; + diagnostics: Diagnostic[]; +} + +interface DiagnosticClientCapabilities { + dynamicRegistration?: boolean; + relatedDocumentSupport?: boolean; +} + +interface DiagnosticOptions extends WorkDoneProgressOptions { + identifier?: string; + interFileDependencies: boolean; + workspaceDiagnostics: boolean; +} + +interface DiagnosticRegistrationOptions extends + TextDocumentRegistrationOptions, DiagnosticOptions, + StaticRegistrationOptions { +} + +interface DocumentDiagnosticParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; + identifier?: string; + previousResultId?: string; +} + +type DocumentDiagnosticReport = RelatedFullDocumentDiagnosticReport + | RelatedUnchangedDocumentDiagnosticReport; + +namespace DocumentDiagnosticReportKind { + Full = 'full'; + Unchanged = 'unchanged'; +} + +type DocumentDiagnosticReportKind = 'full' | 'unchanged'; + +interface FullDocumentDiagnosticReport { + kind: DocumentDiagnosticReportKind.Full; + resultId?: string; + items: Diagnostic[]; +} + +interface UnchangedDocumentDiagnosticReport { + kind: DocumentDiagnosticReportKind.Unchanged; + resultId: string; +} + +interface RelatedFullDocumentDiagnosticReport extends + FullDocumentDiagnosticReport { + relatedDocuments?: { + [uri: string : + FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; + }; +} + +interface RelatedUnchangedDocumentDiagnosticReport extends + UnchangedDocumentDiagnosticReport { + relatedDocuments?: { + [uri: string : + FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; + }; +} + +interface DocumentDiagnosticReportPartialResult { + relatedDocuments: { + [uri: string : + FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; + }; +} + +interface DiagnosticServerCancellationData { + retriggerRequest: boolean; +} + +interface WorkspaceDiagnosticParams extends WorkDoneProgressParams, + PartialResultParams { + identifier?: string; + previousResultIds: PreviousResultId[]; +} + +interface PreviousResultId { + uri: DocumentUri; + value: string; +} + +interface WorkspaceDiagnosticReport { + items: WorkspaceDocumentDiagnosticReport[]; +} + +interface WorkspaceFullDocumentDiagnosticReport extends + FullDocumentDiagnosticReport { + uri: DocumentUri; + version: integer | null; +} + +interface WorkspaceUnchangedDocumentDiagnosticReport extends + UnchangedDocumentDiagnosticReport { + uri: DocumentUri; + version: integer | null; +}; + +type WorkspaceDocumentDiagnosticReport = + WorkspaceFullDocumentDiagnosticReport + | WorkspaceUnchangedDocumentDiagnosticReport; + +interface WorkspaceDiagnosticReportPartialResult { + items: WorkspaceDocumentDiagnosticReport[]; +} + +interface DiagnosticWorkspaceClientCapabilities { + refreshSupport?: boolean; +} + +interface SignatureHelpClientCapabilities { + dynamicRegistration?: boolean; + signatureInformation?: { + documentationFormat?: MarkupKind[]; + parameterInformation?: { + labelOffsetSupport?: boolean; + }; + activeParameterSupport?: boolean; + }; + contextSupport?: boolean; +} + +interface SignatureHelpOptions extends WorkDoneProgressOptions { + triggerCharacters?: string[]; + retriggerCharacters?: string[]; +} + +interface SignatureHelpRegistrationOptions + extends TextDocumentRegistrationOptions, SignatureHelpOptions { +} + +interface SignatureHelpParams extends TextDocumentPositionParams, + WorkDoneProgressParams { + context?: SignatureHelpContext; +} + +namespace SignatureHelpTriggerKind { + Invoked: 1 = 1; + TriggerCharacter: 2 = 2; + ContentChange: 3 = 3; +} + +type SignatureHelpTriggerKind = 1 | 2 | 3; + +interface SignatureHelpContext { + triggerKind: SignatureHelpTriggerKind; + triggerCharacter?: string; + isRetrigger: boolean; + activeSignatureHelp?: SignatureHelp; +} + +interface SignatureHelp { + signatures: SignatureInformation[]; + activeSignature?: uinteger; + activeParameter?: uinteger; +} + +interface SignatureInformation { + label: string; + documentation?: string | MarkupContent; + parameters?: ParameterInformation[]; + activeParameter?: uinteger; +} + +interface ParameterInformation { + label: string | [uinteger, uinteger]; + documentation?: string | MarkupContent; +} + +interface CodeActionClientCapabilities { + dynamicRegistration?: boolean; + codeActionLiteralSupport?: { + codeActionKind: { + valueSet: CodeActionKind[]; + }; + }; + isPreferredSupport?: boolean; + disabledSupport?: boolean; + dataSupport?: boolean; + resolveSupport?: { + properties: string[]; + }; + honorsChangeAnnotations?: boolean; +} + +interface CodeActionOptions extends WorkDoneProgressOptions { + codeActionKinds?: CodeActionKind[]; + resolveProvider?: boolean; +} + +interface CodeActionRegistrationOptions extends + TextDocumentRegistrationOptions, CodeActionOptions { +} + +interface CodeActionParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; + range: Range; + context: CodeActionContext; +} + +type CodeActionKind = string; + +namespace CodeActionKind { + Empty: CodeActionKind = ''; + QuickFix: CodeActionKind = 'quickfix'; + Refactor: CodeActionKind = 'refactor'; + RefactorExtract: CodeActionKind = 'refactor.extract'; + RefactorInline: CodeActionKind = 'refactor.inline'; + RefactorRewrite: CodeActionKind = 'refactor.rewrite'; + Source: CodeActionKind = 'source'; + SourceOrganizeImports: CodeActionKind = + 'source.organizeImports'; + SourceFixAll: CodeActionKind = 'source.fixAll'; +} + +interface CodeActionContext { + diagnostics: Diagnostic[]; + only?: CodeActionKind[]; + triggerKind?: CodeActionTriggerKind; +} + +namespace CodeActionTriggerKind { + Invoked: 1 = 1; + Automatic: 2 = 2; +} + +type CodeActionTriggerKind = 1 | 2; + +interface CodeAction { + title: string; + kind?: CodeActionKind; + diagnostics?: Diagnostic[]; + isPreferred?: boolean; + disabled?: { + reason: string; + }; + edit?: WorkspaceEdit; + command?: Command; + data?: LSPAny; +} + +interface DocumentColorClientCapabilities { + dynamicRegistration?: boolean; +} + +interface DocumentColorOptions extends WorkDoneProgressOptions { +} + +interface DocumentColorRegistrationOptions extends + TextDocumentRegistrationOptions, StaticRegistrationOptions, + DocumentColorOptions { +} + +interface DocumentColorParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; +} + +interface ColorInformation { + range: Range; + color: Color; +} + +interface Color { + readonly red: decimal; + readonly green: decimal; + readonly blue: decimal; + readonly alpha: decimal; +} + +interface ColorPresentationParams extends WorkDoneProgressParams, + PartialResultParams { + textDocument: TextDocumentIdentifier; + color: Color; + range: Range; +} + +interface ColorPresentation { + label: string; + textEdit?: TextEdit; + additionalTextEdits?: TextEdit[]; +} + +interface DocumentFormattingClientCapabilities { + dynamicRegistration?: boolean; +} + +interface DocumentFormattingOptions extends WorkDoneProgressOptions { +} + +interface DocumentFormattingRegistrationOptions extends + TextDocumentRegistrationOptions, DocumentFormattingOptions { +} + +interface DocumentFormattingParams extends WorkDoneProgressParams { + textDocument: TextDocumentIdentifier; + options: FormattingOptions; +} + +interface FormattingOptions { + tabSize: uinteger; + insertSpaces: boolean; + trimTrailingWhitespace?: boolean; + insertFinalNewline?: boolean; + trimFinalNewlines?: boolean; + [key: string]: boolean | integer | string; +} + +interface DocumentRangeFormattingClientCapabilities { + dynamicRegistration?: boolean; +} + +interface DocumentRangeFormattingOptions extends + WorkDoneProgressOptions { +} + +interface DocumentRangeFormattingRegistrationOptions extends + TextDocumentRegistrationOptions, DocumentRangeFormattingOptions { +} + +interface DocumentRangeFormattingParams extends WorkDoneProgressParams { + textDocument: TextDocumentIdentifier; + range: Range; + options: FormattingOptions; +} + +interface DocumentOnTypeFormattingClientCapabilities { + dynamicRegistration?: boolean; +} + +interface DocumentOnTypeFormattingOptions { + firstTriggerCharacter: string; + moreTriggerCharacter?: string[]; +} + +interface DocumentOnTypeFormattingRegistrationOptions extends + TextDocumentRegistrationOptions, DocumentOnTypeFormattingOptions { +} + +interface DocumentOnTypeFormattingParams { + textDocument: TextDocumentIdentifier; + position: Position; + ch: string; + options: FormattingOptions; +} + +namespace PrepareSupportDefaultBehavior { + Identifier: 1 = 1; +} + +type PrepareSupportDefaultBehavior = 1; + +interface RenameClientCapabilities { + dynamicRegistration?: boolean; + prepareSupport?: boolean; + prepareSupportDefaultBehavior?: PrepareSupportDefaultBehavior; + honorsChangeAnnotations?: boolean; +} + +interface RenameOptions extends WorkDoneProgressOptions { + prepareProvider?: boolean; +} + +interface RenameRegistrationOptions extends + TextDocumentRegistrationOptions, RenameOptions { +} + +interface RenameParams extends TextDocumentPositionParams, + WorkDoneProgressParams { + newName: string; +} + +interface PrepareRenameParams extends TextDocumentPositionParams, WorkDoneProgressParams { +} + +interface LinkedEditingRangeClientCapabilities { + dynamicRegistration?: boolean; +} + +interface LinkedEditingRangeOptions extends WorkDoneProgressOptions { +} + +interface LinkedEditingRangeRegistrationOptions extends + TextDocumentRegistrationOptions, LinkedEditingRangeOptions, + StaticRegistrationOptions { +} + +interface LinkedEditingRangeParams extends TextDocumentPositionParams, + WorkDoneProgressParams { +} + +interface LinkedEditingRanges { + ranges: Range[]; + wordPattern?: string; +} + +interface WorkspaceSymbolClientCapabilities { + dynamicRegistration?: boolean; + symbolKind?: { + valueSet?: SymbolKind[]; + }; + tagSupport?: { + valueSet: SymbolTag[]; + }; + resolveSupport?: { + properties: string[]; + }; +} + +interface WorkspaceSymbolOptions extends WorkDoneProgressOptions { + resolveProvider?: boolean; +} + +interface WorkspaceSymbolRegistrationOptions + extends WorkspaceSymbolOptions { +} + +interface WorkspaceSymbolParams extends WorkDoneProgressParams, + PartialResultParams { + query: string; +} + +interface WorkspaceSymbol { + name: string; + kind: SymbolKind; + tags?: SymbolTag[]; + containerName?: string; + location: Location | { uri: DocumentUri }; + data?: LSPAny; +} + +interface ConfigurationParams { + items: ConfigurationItem[]; +} + +interface ConfigurationItem { + scopeUri?: URI; + section?: string; +} + +interface DidChangeConfigurationClientCapabilities { + dynamicRegistration?: boolean; +} + +interface DidChangeConfigurationParams { + settings: LSPAny; +} + +interface WorkspaceFoldersServerCapabilities { + supported?: boolean; + changeNotifications?: string | boolean; +} + +interface WorkspaceFolder { + uri: URI; + name: string; +} + +interface DidChangeWorkspaceFoldersParams { + event: WorkspaceFoldersChangeEvent; +} + +interface WorkspaceFoldersChangeEvent { + added: WorkspaceFolder[]; + removed: WorkspaceFolder[]; +} + +interface FileOperationRegistrationOptions { + filters: FileOperationFilter[]; +} + +namespace FileOperationPatternKind { + file: 'file' = 'file'; + folder: 'folder' = 'folder'; +} + +type FileOperationPatternKind = 'file' | 'folder'; + +interface FileOperationPatternOptions { + ignoreCase?: boolean; +} + +interface FileOperationPattern { + glob: string; + matches?: FileOperationPatternKind; + options?: FileOperationPatternOptions; +} + +interface FileOperationFilter { + scheme?: string; + pattern: FileOperationPattern; +} + +interface CreateFilesParams { + files: FileCreate[]; +} + +interface FileCreate { + uri: string; +} + +interface RenameFilesParams { + files: FileRename[]; +} + +interface FileRename { + oldUri: string; + newUri: string; +} + +interface DeleteFilesParams { + files: FileDelete[]; +} + +interface FileDelete { + uri: string; +} + +interface DidChangeWatchedFilesClientCapabilities { + dynamicRegistration?: boolean; + relativePatternSupport?: boolean; +} + +interface DidChangeWatchedFilesRegistrationOptions { + watchers: FileSystemWatcher[]; +} + +type Pattern = string; + +interface RelativePattern { + baseUri: WorkspaceFolder | URI; + pattern: Pattern; +} + +type GlobPattern = Pattern | RelativePattern; + +interface FileSystemWatcher { + globPattern: GlobPattern; + kind?: WatchKind; +} + +namespace WatchKind { + Create = 1; + Change = 2; + Delete = 4; +} + +type WatchKind = uinteger; + +interface DidChangeWatchedFilesParams { + changes: FileEvent[]; +} + +interface FileEvent { + uri: DocumentUri; + type: FileChangeType; +} + +namespace FileChangeType { + Created = 1; + Changed = 2; + Deleted = 3; +} + +type FileChangeType = 1 | 2 | 3; + +interface ExecuteCommandClientCapabilities { + dynamicRegistration?: boolean; +} + +interface ExecuteCommandOptions extends WorkDoneProgressOptions { + commands: string[]; +} + +interface ExecuteCommandRegistrationOptions + extends ExecuteCommandOptions { +} + +interface ExecuteCommandParams extends WorkDoneProgressParams { + command: string; + arguments?: LSPAny[]; +} + +interface ApplyWorkspaceEditParams { + label?: string; + edit: WorkspaceEdit; +} + +interface ApplyWorkspaceEditResult { + applied: boolean; + failureReason?: string; + failedChange?: uinteger; +} + +interface ShowMessageParams { + type: MessageType; + message: string; +} + +namespace MessageType { + Error = 1; + Warning = 2; + Info = 3; + Log = 4; + Debug = 5; +} + +type MessageType = 1 | 2 | 3 | 4 | 5; + +interface ShowMessageRequestClientCapabilities { + messageActionItem?: { + additionalPropertiesSupport?: boolean; + }; +} + +interface ShowMessageRequestParams { + type: MessageType; + message: string; + actions?: MessageActionItem[]; +} + +interface MessageActionItem { + title: string; +} + +interface ShowDocumentClientCapabilities { + support: boolean; +} + +interface ShowDocumentParams { + uri: URI; + external?: boolean; + takeFocus?: boolean; + selection?: Range; +} + +interface ShowDocumentResult { + success: boolean; +} + +interface LogMessageParams { + type: MessageType; + message: string; +} + +interface WorkDoneProgressCreateParams { + token: ProgressToken; +} + +interface WorkDoneProgressCancelParams { + token: ProgressToken; +} diff --git a/slime-coalton.el b/slime-coalton.el deleted file mode 100644 index b63171f..0000000 --- a/slime-coalton.el +++ /dev/null @@ -1,95 +0,0 @@ -;;;; slime-coalton.el --- coalton-mode lisp integration -*- lexical-binding: t; -*- -;;;; -;;;; Slime extension via `define-slime-contrib' for interaction with a -;;;; Coalton instance running in a Slime-managed Lisp subprocess. - -(require 'slime) - -;; Ensure that slime is connected, coalton is loaded and swank components are loaded - -(defun check-connection () - (unless (slime-connected-p) - (error "Connect to slime.")) - ;; (eql :loaded (slime-eval `(swank:swank-coalton-status))) - ) - -(cl-defmacro slime-coalton--show ((name) &body body) - (declare (indent 1)) - `(with-current-buffer (get-buffer-create ,name) - (erase-buffer) - (slime-popup-buffer-mode) - ,@body - (display-buffer (current-buffer)) - (current-buffer))) - -(defun slime-coalton--buffer-name (type) - (format "*coalton-%s*" (symbol-name type))) - -(defun slime-coalton--popup-buffer (type) - (let ((name (slime-coalton--buffer-name type))) - (slime-coalton--show (name) - (current-buffer)))) - -(defun slime-coalton--popup (type value) - (pop-to-buffer (slime-coalton--popup-buffer type)) - (read-only-mode -1) - (erase-buffer) - (insert value) - (goto-char (point-min))) - -(defun slime-coalton--eval (sexp cont) - (declare (indent 1)) - (check-connection) - (slime-rex (cont) - (sexp "swank") - ((:ok result) - (when cont - (funcall cont result))) - ((:abort condition) - (message "Evaluation aborted on %s." condition)))) - -(defun coalton-ast () - "Display the AST of the current buffer." - (interactive) - (slime-coalton--eval `(swank:swank-coalton--codegen - ,(buffer-substring-no-properties (point-min) (point-max))) - (lambda (result) - (slime-coalton--popup 'ast result)))) - -(defun coalton-codegen () - "Display the compiled Lisp of the current buffer." - (interactive) - (slime-coalton--eval `(swank:swank-coalton--codegen - ,(buffer-substring-no-properties (point-min) (point-max))) - (lambda (result) - (slime-coalton--popup 'codegen result)))) - -(defun coalton-compile () - "Compile the current buffer." - (interactive) - (slime-coalton--eval `(swank:swank-coalton--compile - ,(buffer-substring-no-properties (point-min) (point-max))) - (lambda (result) - (slime-coalton--popup 'compile result)))) - -(defun coalton-compile-form () - "Redefine the toplevel form containing the current point." - (interactive) - (slime-coalton--eval `(swank:swank-coalton--compile-form - ,(buffer-substring-no-properties (point-min) (point-max)) - (point)) - (lambda (result) - (slime-coalton--popup 'compile result)))) - - -;;; Initialization - -(defun slime-coalton-init () - (message "slime-coalton.el: slime-coalton-init")) - -(define-slime-contrib slime-coalton - "Support Coalton language" - (:authors "Jesse Bouwman ") - (:swank-dependencies swank::swank-coalton)) - -(provide 'slime-coalton) diff --git a/src/lib/json.lisp b/src/lib/json.lisp new file mode 100644 index 0000000..01b1b94 --- /dev/null +++ b/src/lib/json.lisp @@ -0,0 +1,77 @@ +(in-package #:coalton-lsp) + +;;; Decoding JSON strings + +;;; Jzon represents JSON maps as hashtables: import them to alists, so +;;; that messages can use a functional update model. + +(defun decode-json-object (object) + (typecase object + (string object) + (hash-table + (loop :for key :being :the :hash-keys :of object + :for value :being :the :hash-values :of object + :collect (cons key (decode-json-object value)))) + (vector + (loop :for element :across object + :collect (decode-json-object element))) + (t object))) + +(defun decode-json (json) + (decode-json-object (com.inuoe.jzon:parse json))) + +;;; Encoding + +(defun encode-json (value) + (with-output-to-string (stream) + (com.inuoe.jzon:with-writer (writer :stream stream :pretty t) + (com.inuoe.jzon:write-value writer value)))) + +(defun reencode-json (string) + (with-output-to-string (stream) + (com.inuoe.jzon:with-writer (writer :stream stream :pretty t) + (com.inuoe.jzon:write-value writer (com.inuoe.jzon:parse string))))) + +(defgeneric jzon-value (message-class value) + (:documentation "Convert a message to a value that can be directly serialized by jzon.")) + +;;; The values of atoms are all lisp atomic types that jzon serializes +;;; correctly. + +(defmethod jzon-value ((self message-atom) value) + (cond ((atom value) + (or value 'null)) + (t + (error "non-atom value in atom field: check the field type? value: ~s" value)))) + +(defmethod jzon-value ((self message-enum) value) + value) + +;;; Construct a jzon value, considering the 'vector and 'optional +;;; properties of a field. + +(defun jzon-field-value (field value) + (with-slots (class vector) field + (cond (vector + (loop :with result := (make-array (length value)) + :for element :in value + :for i :below (length value) + :do (setf (aref result i) + (jzon-value class element)) + :finally (return result))) + (t + (jzon-value class value))))) + +(defmethod jzon-value ((self message-class) value) + (let ((jzon-map (make-hash-table :test 'equal))) + (loop :for field :across (message-fields self) + :do (with-slots (resolve optional) field + (let ((field-value (cdr (assoc (json-key field) value)))) + (when (or field-value (not optional)) + (setf (gethash (json-key field) jzon-map) + (jzon-field-value field field-value)))))) + jzon-map)) + +(defun to-json (message) + (encode-json (jzon-value (message-class message) + (message-value message)))) diff --git a/src/lib/list.lisp b/src/lib/list.lisp new file mode 100644 index 0000000..7b12c80 --- /dev/null +++ b/src/lib/list.lisp @@ -0,0 +1,4 @@ +(in-package #:coalton-lsp) + +(defun listify (x) + (if (listp x) x (list x))) diff --git a/src/lib/log.lisp b/src/lib/log.lisp new file mode 100644 index 0000000..1af7020 --- /dev/null +++ b/src/lib/log.lisp @@ -0,0 +1,89 @@ +(in-package :coalton-lsp) + +(defun write-timestamp (stream) + (multiple-value-bind (ss mm hh) + (decode-universal-time (get-universal-time)) + (format stream "~2,'0d:~2,'0d:~2,'0d" hh mm ss))) + +(defvar *context* nil) + +(defun add-context (context k f) + (if (some (lambda (e) + (eq k (car e))) + context) + context + (nconc context (list (cons k f))))) + +(defmacro with-logging-context ((k f) &body body) + `(let ((*context* (add-context *context* ,k ,f))) + ,@body)) + +(defun write-context (stream) + (dolist (c *context*) + (funcall (cdr c) stream) + (write-string " : " stream))) + +(defgeneric log-p (destination level)) + +(defgeneric write-log (destination level format format-args metadata)) + +(defclass stream-logger () + ((stream :initarg :stream) + (level :initform :info + :initarg :level + :reader log-level) + (lock :initform (bt:make-lock)))) + +(defvar *levels* + '(:trace :debug :info :warn :error)) + +(defun level<= (a b) + (<= (position a *levels*) + (position b *levels*))) + +(defmethod log-p ((self stream-logger) level) + (level<= (log-level self) level)) + +(defmethod write-log ((self stream-logger) level format format-args metadata) + (when (log-p self level) + (with-slots (stream lock) self + (bt:with-lock-held (lock) + (write-string ";; " stream) + (write-timestamp stream) + (write-string " : " stream) + (princ level stream) + (write-string " : " stream) + (write-context stream) + (apply #'format stream format format-args) + (terpri stream))))) + +(defparameter *logger* + (make-instance 'stream-logger :stream t :level ':info)) + +(defun %log (level message args) + (write-log *logger* level message args nil) + (values)) + +(defun set-log-level (level) + (setf (slot-value *logger* 'level) level)) + +(defun /trace-p () + (log-p *logger* ':trace)) + +(defun /trace (message &rest args) + (%log :trace message args)) + +(defun /debug-p () + (log-p *logger* ':debug)) + +(defun /debug (message &rest args) + (%log :debug message args)) + +(defun /info (message &rest args) + (%log :info message args)) + +(defun /warn (message &rest args) + (%log :warn message args)) + +(defun /error (message &rest args) + (%log :error message args)) diff --git a/src/lib/message.lisp b/src/lib/message.lisp new file mode 100644 index 0000000..295fb8b --- /dev/null +++ b/src/lib/message.lisp @@ -0,0 +1,211 @@ +(in-package #:coalton-lsp) + +(defvar *message-classes* + (make-hash-table)) + +(defclass message-type () + ((name :initarg :name + :reader name))) + +(defclass message-atom (message-type) + ()) + +(defmethod json-value ((self message-atom) value) +i #++ (unless (typep value (slot-value self 'name)) ; FIXME -- defer to rewrite of accept-p + (error "illegal value type for atom ~a: ~a" self value)) + value) + +(defclass message-field (message-type) + ((json :initarg :json + :reader json-key) ; field value's key in a JSON map + (class :initarg :class + :reader message-class) + (optional :initarg :optional + :initform nil) + (vector :initarg :vector + :initform nil))) + +(defun copy-message-field (message-field) + (with-slots (name json class optional vector) message-field + (make-instance 'message-field + :name name + :json json + :class class + :optional optional + :vector vector))) + +(defmethod print-object ((self message-field) stream) + (with-slots (name class) self + (print-unreadable-object (self stream :type t :identity t) + (format stream "~a (~a)" name (name class))))) + +(defmethod json-value (self value) + value) + +(defun parse-field-type (type) + (etypecase type + (list + (destructuring-bind (type &key optional vector) type + (list (get-message-class type) optional vector))) + (symbol + (list (get-message-class type) nil nil)))) + +(defun make-field (spec) + (destructuring-bind (name type) spec + (destructuring-bind (class optional vector) (parse-field-type type) + (make-instance 'message-field + :name name + :json (camel-case name) + :class class + :optional optional + :vector vector)))) + +(defclass message-class (message-type) + ((fields :initform (make-array 0 :adjustable t :fill-pointer t) + :reader message-fields))) + +(defmethod print-object ((self message-class) stream) + (with-slots (name fields) self + (print-unreadable-object (self stream :type t :identity t) + (format stream "~a (~d fields)" name (length fields))))) + +(defun add-field (class field) + (vector-push-extend field (slot-value class 'fields))) + +(defun copy-message-class (class) + (with-slots (name fields) class + (let ((class2 (make-instance 'message-class :name name))) + (loop :for field :across fields + :do (add-field class2 (copy-message-field field))) + class2))) + +(defun set-field-class (class name field-class) + "Replace the definition of a single foield" + (loop :for field :across (slot-value class 'fields) + :do (when (eq (name field) name) + (setf (slot-value field 'class) field-class))) + class) + +(defun %get-field (class name) + (loop :for field :across (slot-value class 'fields) + :when (eq (slot-value field 'name) name) + :do (return-from %get-field field)) + (error "undefined message field: ~a" name)) + +(defmacro define-atom (name) + `(setf (gethash ',name *message-classes*) + (make-instance 'message-atom :name ',name))) + +(defmacro define-message (name parent-classes &body field-defs) + `(let ((message (make-instance 'message-class :name ',name))) + (loop :for class :in ',parent-classes + :do (loop :for field :across (slot-value (get-message-class class) 'fields) + :do (add-field message field))) + (loop :for field-def :in ',field-defs + :do (add-field message (make-field field-def))) + (setf (gethash ',name *message-classes*) message))) + +(defun get-message-class (name) + (or (gethash name *message-classes*) + (error "undefined message class: ~a" name))) + +;;; Enums + +(defclass message-enum (message-type) + ((values :initform (make-hash-table)))) + +(defmethod print-object ((self message-enum) stream) + (with-slots (name values) self + (print-unreadable-object (self stream :type t :identity t) + (format stream "~a" name)))) + +(defmethod json-value ((self message-enum) value) + (with-slots (values) self + (or (gethash value values) + (error "undefined enum value in ~a: ~a" self value)))) + +(defmacro define-enum (name class-args &body value-defs) + (declare (ignore class-args)) + `(let ((message (make-instance 'message-enum :name ',name))) + (loop :for (k v) :in ',value-defs + :do (setf (gethash k (slot-value message 'values)) v)) + (setf (gethash ',name *message-classes*) message))) + +;;; Unions + +(defclass message-union (message-type) + ((classes :initarg :classes))) + +(defmethod print-object ((self message-union) stream) + (with-slots (name) self + (print-unreadable-object (self stream :type t :identity t) + (format stream "~a" name)))) + +(defmethod json-value ((self message-union) value) + (error "fixme: json-value must not signal a condition")) + +(defmacro define-union (name union-classes) + `(setf (gethash ',name *message-classes*) + (make-instance 'message-union + :name ',name + :classes (mapcar #'get-message-class + ',union-classes)))) + +;;; Messages compose a class and a value + +(defclass message () + ((class :initarg :class + :reader message-class) + (value :initarg :value + :initform nil + :reader message-value))) + +(defmethod print-object ((self message) stream) + (with-slots (class value) self + (print-unreadable-object (self stream :type t) + (princ (name class) stream)))) + +(defun make-message (class &optional value) + (assert (symbolp class)) + (make-instance 'message + :class (get-message-class class) + :value value)) + +(defun field-1 (message key) ; FIXME bad name + (let ((field (%get-field (message-class message) key))) + (make-instance 'message + :class (message-class field) + :value (cdr (assoc (json-key field) + (message-value message) + :test #'string=))))) + +(defun field-n (message path) ; FIXME bad name + (reduce #'field-1 (listify path) :initial-value message)) + +(defun set-field-1 (message key value) + (let ((field (%get-field (message-class message) key))) + (make-message (name (message-class message)) + (acons (json-key field) + (json-value (message-class field) value) + (remove (json-key field) + (message-value message) + :key #'car + :test #'string=))))) + +(defun with-field (message path value) + (destructuring-bind (key &rest keys) (listify path) + (set-field-1 message key + (if (null keys) + value + (message-value (with-field (field-1 message key) + keys value)))))) + +;; Message API + +(defun get-field (message path) + (message-value (field-n message (listify path)))) + +(defun set-field (message path value) + (setf (slot-value message 'value) + (slot-value (with-field message (listify path) value) 'value)) + message) diff --git a/src/lib/name.lisp b/src/lib/name.lisp new file mode 100644 index 0000000..a4cbcb7 --- /dev/null +++ b/src/lib/name.lisp @@ -0,0 +1,20 @@ +;;;; Functions related to names and identifiers + +(in-package #:coalton-lsp) + +(defun camel-case (keyword) + "Convert hyphen-delimited keyword identifiers to camel-cased strings." + (with-output-to-string (stream) + (loop :with upper + :for char :across (symbol-name keyword) :while char + :if (char= char #\-) + :do (setf upper t) + :else + :do (write-char (if upper char (char-downcase char)) stream) + (setf upper nil)))) + +#+example + +(camel-case :some-thing) + +;; => "someThing" diff --git a/src/lib/process.lisp b/src/lib/process.lisp new file mode 100644 index 0000000..0dccb6f --- /dev/null +++ b/src/lib/process.lisp @@ -0,0 +1,86 @@ +;;;; Behavior common to classes that manage processes + +(in-package :coalton-lsp) + +(defgeneric start (process) + (:documentation "Start a process. The started process is returned.")) + +(defgeneric name (process) + (:documentation "Process name.") + (:method (process) "Coalton LSP Process")) + +(defgeneric run (process) + (:documentation "Run a process. This function is run by a newly started process. IF the function returns, the process will halt.")) + +(defgeneric stop (process) + (:documentation "Synchronously stop a process and immediately return.")) + +(defclass process () + ((thread :initform nil) + (lock :initform (bt:make-recursive-lock)))) + +(defmacro with-lock-held ((process) &body scope) + `(bt:with-recursive-lock-held ((slot-value ,process 'lock)) + ,@scope)) + +(defmethod start ((self process)) + (with-slots (thread) self + (setf thread (bt:make-thread (lambda () + (run self)) + :name (name self)))) + self) + +(defmethod stop ((self process)) + (with-slots (thread) self + (when thread + (bt:destroy-thread thread) + (setf thread nil))) + self) + +;;; Work queue + +(defvar *worker-poll-interval* 0.250 + "How long to sleep when there is no work to do.") + +(defclass worker (process) + ((fn) + (run :initform t + :reader run-p) + (queue :initform nil))) + +(defun enqueue (worker element) + (with-lock-held (worker) + (with-slots (queue) worker + (setf queue (nconc queue (list element)))))) + +(defun dequeue (worker) + (with-lock-held (worker) + (with-slots (queue) worker + (let ((element (car queue))) + (setf queue (cdr queue)) + element)))) + +(defun empty-p (worker) + (with-lock-held (worker) + (null (slot-value worker 'queue)))) + +(defun service-queue (worker) + (loop :while (and (run-p worker) + (not (empty-p worker))) + :do (let ((element (dequeue worker))) + (/debug "about to process one entry") + (handler-case + (funcall (slot-value worker 'fn) element) + (condition (condition) + (break) + (/error "ignoring error : ~a" condition)))))) + +(defmethod run ((self worker)) + (with-logging-context (:worker (lambda (stream) + (write-string "worker" stream))) + (/debug "starting") + (unwind-protect + (loop :while (run-p self) + :do (service-queue self) + (sleep *worker-poll-interval*)) + (/debug "exiting")))) diff --git a/src/lib/rpc.lisp b/src/lib/rpc.lisp new file mode 100644 index 0000000..9119a85 --- /dev/null +++ b/src/lib/rpc.lisp @@ -0,0 +1,151 @@ +;;;; Read and write JSON-RPC messages +;;;; +;;;; https://www.jsonrpc.org/specification + +(in-package :coalton-lsp) + +(defun read-header (stream) + "Read a HTTP-header-formatted key value pair from STREAM." + (declare (optimize (speed 3))) + (flet ((make-buf () + (make-array 0 :adjustable t :fill-pointer t :element-type 'character))) + (let ((state :begin-line) + (prev-state nil) + (buf (make-buf)) + (k nil)) + (loop :for c := (read-char stream) + :do (ecase state + (:begin-line + (cond ((char= c #\Return) + (setf state :cr prev-state :key)) + (t + (vector-push-extend c buf) + (setf state :key)))) + (:key + (cond ((char= c #\:) + (setf k buf buf (make-buf) state :after-key)) + (t + (vector-push-extend c buf)))) + (:after-key + (cond ((char= c #\Space)) + (t + (vector-push-extend c buf) + (setf state :value)))) + (:value + (cond ((char= c #\Return) + (setf state :cr prev-state :value)) + (t + (vector-push-extend c buf)))) + (:cr + (cond ((char= c #\Newline) + (return)) + (t + (vector-push-extend #\Return buf) + (vector-push-extend c buf) + (setf state prev-state prev-state nil)))))) + (when (< 0 (length k)) + (cons k buf))))) + +(defun read-headers (stream) + (loop :for kv := (read-header stream) :while kv :collect kv)) + +(defun write-crlf (stream) + (write-char #\Return stream) + (write-char #\Newline stream)) + +(defun write-header (stream kv) + "Write a HTTP-header-formatted key value pair to STREAM." + (write-string (car kv) stream) + (write-char #\: stream) + (write-char #\Space stream) + (write-string (princ-to-string (cdr kv)) stream) + (write-crlf stream)) + +(defun write-headers (stream kvs) + (loop :for kv :in kvs :do (write-header stream kv)) + (write-crlf stream)) + +(defun get-header (map key) + (cdr (assoc key map :test #'string-equal))) + +(defun content-length (headers) + (let ((value (get-header headers "Content-Length"))) + (when value + (parse-integer value)))) + +(defclass rpc-message () + ((content :initarg :content + :reader message-content + :documentation "JSON text") + (parsed-content :initarg :parsed + :initform nil))) + +(defun parsed-content (rpc-message) + (with-slots (content parsed-content) rpc-message + (unless parsed-content + (setf parsed-content (decode-json content))) + parsed-content)) + +(defun message-field (rpc-message key) + (cdr (assoc (camel-case key) (parsed-content rpc-message) :test #'string-equal))) + +(defun message-id (rpc-message) + (message-field rpc-message :id)) + +(defun message-type (rpc-message) + (let ((id (message-field rpc-message :id)) + (method (message-field rpc-message :method))) + (cond ((and method id) + 'request-message) + ((and method (not id)) + 'notification-message) + ((and (not method) id) + 'response-message) + (t + nil)))) + +(defun trunc (n string) + (if (< n (length string)) + (concatenate 'string (subseq string 0 (max 0 (- n 3))) "...") + string)) + +(defmethod print-object ((self rpc-message) stream) + (let* ((id (message-field self :id)) + (method (message-field self :method)) + (type (case (message-type self) + (request-message "request") + (notification-message "request") + (response-message "response") + (t "incomplete message")))) + (print-unreadable-object (self stream :type t :identity t) + (format stream "~a ~a id: ~a value: ~s" + type + (or method "none") + (or id "none") + (trunc 16 (message-content self)))))) + +(defun read-rpc (stream) + (let* ((headers (read-headers stream)) + (content-length (content-length headers)) + (content (make-array content-length :element-type 'character))) + (read-sequence content stream :start 0 :end content-length) + (let ((message (make-instance 'rpc-message + :content content))) + (when (/trace-p) + (/trace "rpc/read-rpc ~a" (reencode-json content))) + message))) + +(defun %write-rpc (message stream) + (let ((content (message-content message))) + (write-headers stream + `(("Content-Length" . ,(length content)) + ("Content-Type" . "application/json-rpc"))) + (write-sequence content stream) + (force-output stream))) + +(defun write-rpc (content stream) + (let ((message (make-instance 'rpc-message + :content content))) + (when (/trace-p) + (/trace "rpc/write-rpc~%~a" (reencode-json content))) + (%write-rpc message stream))) diff --git a/src/lib/uri.lisp b/src/lib/uri.lisp new file mode 100644 index 0000000..5f8d96c --- /dev/null +++ b/src/lib/uri.lisp @@ -0,0 +1,51 @@ +;;;; Just enough to parse simple file: scheme URLs + +(defpackage #:lib.uri + (:use + #:cl) + (:export + #:parse + #:input-stream)) + +(in-package #:lib.uri) + +(defstruct uri + scheme + path) + +(defun parse (string) + (cond ((alexandria:starts-with-subseq "file:" string) + (make-uri :scheme "file" + :path (subseq string 7))) + (t + nil))) + +(defvar *stream-providers* + (make-hash-table :test #'equalp)) + +(defstruct stream-provider + input + output) + +(defun define-stream-provider (name ignore in out) + (declare (ignore ignore)) + (setf (gethash name *stream-providers*) + (make-stream-provider :input in + :output out))) + +(define-stream-provider "file" () + (lambda (uri) + (open (uri-path uri) + :direction :input + :element-type 'character)) + (lambda (uri) + (open (uri-path uri) + :direction :output + :element-type 'character))) + +(defun stream-provider (url) + (or (gethash (uri-scheme url) *stream-providers*) + (error "No stream provider for scheme ~A" (uri-scheme url)))) + +(defun input-stream (url) + (funcall (stream-provider-input (stream-provider url)) url)) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..bf0f6c1 --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,7 @@ +(defpackage #:coalton-lsp + (:documentation "An LSP server for the Coalton language") + (:use #:cl) + (:export #:main + #:start + #:stop + #:restart)) diff --git a/src/protocol.lisp b/src/protocol.lisp new file mode 100644 index 0000000..079bd83 --- /dev/null +++ b/src/protocol.lisp @@ -0,0 +1,538 @@ +;;;; Definition of the messages used by LSP using functions defined +;;;; by lib/message.lisp. +;;;; +;;;; See the file resources/spec.ts for the original TypeScript +;;;; definitions. The small message-definition language here is +;;;; intended to support idiomatic Lisp access to message contents, +;;;; (lisp-cased keyword keys, alists for maps, lists for sequences) +;;;; as well as reasonably strict validation of message completeness +;;;; and correctness when encoded as spec-compliant JSON. + +(in-package #:coalton-lsp) + +;;; Atom types: values that aren't encoded as maps or arrays. + +;;; Type 't' indicates that the type of the value is determined by +;;; some other structural feature, such as the value of 'method' at +;;; the top of an RPC request or notification. + +(define-atom t) + +(define-atom string) + +(deftype uri () 'string) + +(define-atom uri) + +(define-atom boolean) + +(define-atom integer) + +(deftype uinteger () '(integer 0 #.(1- (expt 2 31)))) + +(define-atom uinteger) + +;;; Common structural types + +(define-message position () + (:line uinteger) + (:character uinteger)) + +(define-message range () + (:start position) + (:end position)) + +(define-message location () + (:uri uri) + (:range range)) + +(define-union progress-token + (integer string)) + +(define-message work-done-progress-params () + (:work-done-token progress-token)) + +;;; Errors + +(define-enum error-code () + (:unknown-error-code -32001) + (:server-not-initialized -32002) + (:invalid-request -32600) + (:method-not-found -32601) + (:invalid-params -32602) + (:internal-error -32603) + (:parse-error -32700) + (:request-cancelled -32800) + (:content-modified -32801) + (:server-cancelled -32802) + (:request-failed -32803)) + +(define-message response-error () + (:code error-code) + (:message string) + (:data t)) + +;;; Messages + +(define-message message () + (:jsonrpc string)) + +(define-message notification-message (message) + (:method string) + (:params t)) + +(define-message request-message (message) + (:id integer) + (:method string) + (:params t)) + +(define-message response-message (message) + (:id integer) + (:result (t :optional t)) + (:error (response-error :optional t))) + +(define-enum position-encoding-kind () + (:utf8 "utf-8") + (:utf16 "utf-16") + (:utf32 "utf-32")) + +(define-enum text-document-sync-kind () + (:none 0) + (:full 1) + (:incremental 2)) + +(define-message text-document-sync-options () + (:open-close boolean) + (:change (text-document-sync-kind :optional t))) + +(define-message server-info () + (:name string) + (:version (string :optional t))) + +(define-enum resource-operation-kind () + (:create "create") + (:rename "rename") + (:delete "delete")) + +(define-enum failure-handling-kind () + (:abort "abort") + (:transactional "transactional") + (:undo "undo") + (:text-only-transactional "textOnlyTransactional")) + +(define-message workspace-edit-client-capabilities () + (:document-changes boolean) + (:resource-operations (resource-operation-kind :vector t)) + (:failure-handling failure-handling-kind) + (:normalizes-line-endings boolean) + (:change-annotation-support boolean)) + +(define-message did-change-configuration-client-capabilities () + (:dynamic-registration (boolean :optional t))) + +(define-message did-change-watched-files-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:relative-pattern-support (boolean :optional t))) + +(define-enum symbol-kind () + (:file 1) + (:module 2) + (:namespace 3) + (:package 4) + (:class 5) + (:method 6) + (:property 7) + (:field 8) + (:constructor 9) + (:enum 10) + (:interface 11) + (:function 12) + (:variable 13) + (:constant 14) + (:string 15) + (:number 16) + (:boolean 17) + (:array 18) + (:object 19) + (:key 20) + (:null 21) + (:enum-member 22) + (:struct 23) + (:event 24) + (:operator 25) + (:type-parameter 26)) + +(define-message symbol-kind-value-set () + (:value-set (symbol-kind :vector t))) + +(define-enum symbol-tag () + (:deprecated 1)) + +(define-message symbol-tag-value-set () + (:value-set (symbol-tag :vector t))) + +(define-message resolve-support-properties () + (:properties (string :vector t))) + +(define-message workspace-symbol-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:symbol-kind (symbol-kind-value-set :optional t)) + (:tag-support (symbol-tag-value-set :optional t)) + (:resolve-support (resolve-support-properties :optional t))) + +(define-message execute-command-client-capabilities () + (:dynamic-registration (boolean :optional t))) + +(define-message semantic-tokens-workspace-client-capabilities () + (:refresh-support (boolean :optional t))) + +(define-message code-lens-workspace-client-capabilities () + (:refresh-support (boolean :optional t))) + +(define-message workspace-client-capabilities () + (:apply-edit (boolean :optional t)) + (:workspace-edit (workspace-edit-client-capabilities :optional t)) + (:did-change-configuration (did-change-configuration-client-capabilities :optional t)) + (:did-change-watched-files (did-change-watched-files-client-capabilities :optional t)) + (:symbol (workspace-symbol-client-capabilities :optional t)) + (:execute-command (execute-command-client-capabilities :optional t)) + (:workspace-folders (boolean :optional t)) + (:configuration (boolean :optional t)) + (:semantic-tokens (semantic-tokens-workspace-client-capabilities :optional t)) + (:code-lens (code-lens-workspace-client-capabilities :optional t)) + #++ (:file-operations (file-operations-client-capabilities :optional t)) + #++ (:inline-value (inline-value-workspace-client-capabilities :optional t)) + #++ (:inlay-hint (inlay-hint-workspace-client-capabilities :optional t)) + #++ (:diagnostics (diagnostic-workspace-client-capabilities :optional t))) + +(define-message text-document-sync-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:will-save (boolean :optional t)) + (:will-save-wait-until (boolean :optional t)) + (:did-save (boolean :optional t))) + +(define-message declaration-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:link-support (boolean :optional t))) + +(define-message definition-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:link-support (boolean :optional t))) + + +(define-enum insert-text-mode () + (:as-is 1) + (:adjust-indentation 2)) + +(define-enum completion-item-tag () + (:deprecated t)) + +(define-message tag-support-value-set () + (:value-set (completion-item-tag :vector t))) + +(define-message insert-text-mode-value-set () + (:value-set (insert-text-mode :vector t))) + +(define-message resolve-support-properties () + (:properties (string :vector t))) + +(define-enum markup-kind () + (:plaintext "plaintext") + (:markdown "markdown")) + +(define-message completion-item-capabilities () + (:snippet-support (boolean :optional t)) + (:commit-characters-support (boolean :optional t)) + (:documentation-format (markup-kind :optional t :vector t)) + (:deprecated-support (boolean :optional t)) + (:preselect-support (boolean :optional t)) + (:tag-support (tag-support-value-set :optional t)) + (:insert-replace-support (boolean :optional t)) + (:resolve-support (resolve-support-properties :optional t)) + (:insert-text-mode-support (insert-text-mode-value-set :optional t)) + (:label-details-support (boolean :optional t))) + +(define-message completion-list-capabilities () + (:item-defaults (string :optional t :vector t))) + +(define-message completion-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:completion-item completion-item-capabilities) + #++ (:completion-item-kind completion-item-kind-capabilities) + (:context-support (boolean :optional t)) + (:insert-text-mode insert-text-mode) + (:completion-list completion-list-capabilities)) + +(define-message hover-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:content-format (markup-kind :vector t :optional t))) + +(define-message signature-help-client-capabilities () + (:dynamic-registration (boolean :optional t)) + #++ (:signature-information (signature-information-options :optional t)) + (:context-support (boolean :optional t))) + +(define-message type-definition-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:link-support (boolean :optional t))) + +(define-message implementation-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:link-support (boolean :optional t))) + +(define-message reference-client-capabilities () + (:dynamic-registration (boolean :optional t))) + +(define-message document-highlight-client-capabilities () + (:dynamic-registration (boolean :optional t))) + +(define-message document-symbol-client-capabilities () + (:dynamic-registration (boolean :optional t)) + (:symbol-kind (symbol-kind-value-set :optional t)) + (:hierarchical-document-symbol-support (boolean :optional t)) + (:tag-support (symbol-tag-value-set :optional t)) + (:label-support (boolean :optional t))) + +(define-message text-document-client-capabilities () + (:synchronization (text-document-sync-client-capabilities :optional t)) + (:completion (completion-client-capabilities :optional t)) + (:hover (hover-client-capabilities :optional t)) + (:signature-help (signature-help-client-capabilities :optional t)) + (:declaration (declaration-client-capabilities :optional t)) + (:definition (definition-client-capabilities :optional t)) + (:type-definition (type-definition-client-capabilities :optional t)) + (:implementation (implementation-client-capabilities :optional t)) + (:references (reference-client-capabilities :optional t)) + (:document-highlight (document-highlight-client-capabilities :optional t)) + (:document-symbol (document-symbol-client-capabilities :optional t)) + #++ (:code-action (code-action-client-capabilities :optional t)) + #++ (:code-lens (code-lens-client-capabilities :optional t)) + #++ (:document-link (document-link-client-capabilities :optional t)) + #++ (:color-provider (document-color-client-capabilities :optional t)) + #++ (:formatting (document-formatting-client-capabilities :optional t)) + #++ (:range-formatting (document-range-formatting-client-capabilities :optional t)) + #++ (:on-type-formatting (document-on-type-formatting-client-capabilities :optional t)) + #++ (:rename (rename-client-capabilities :optional t)) + #++ (:publish-diagnostics (publish-diagnostics-client-capabilities :optional t)) + #++ (:folding-range (folding-range-client-capabilities :optional t)) + #++ (:selection-range (selection-range-client-capabilities :optional t)) + #++ (:linked-editing-range (linked-editing-range-client-capabilities :optional t)) + #++ (:call-hierarchy (call-hierarchy-client-capabilities :optional t)) + #++ (:semantic-tokens (semantic-tokens-client-capabilities :optional t)) + #++ (:moniker (moniker-client-capabilities :optional t)) + #++ (:type-hierarchy (type-hierarchy-client-capabilities :optional t)) + #++ (:inline-value (inline-value-client-capabilities :optional t)) + #++ (:inlay-hint (inlay-hint-client-capabilities :optional t)) + #++ (:diagnostic (diagnostic-client-capabilities :optional t))) + +(define-message client-capabilities () + (:workspace (workspace-client-capabilities :optional t)) + (:text-document (text-document-client-capabilities :optional t)) + #++ (:notebook-document (notebook-document-client-capabilities :optional t)) + #++ (:window (window-client-capabilities :optional t)) + #++ (:general (general-client-capabilities :optional t)) + #++ (:experimental lsp-any)) + +(define-message client-info () + (:name string) + (:version (string :optional t))) + +(define-message workspace-folder () + (:uri uri) + (:name string)) + +(define-enum trace-value () + (:off "off") + (:messages "messages") + (:verbose "verbose")) + +;;; Session initialization +;;; +;;; The first message sent by the client is 'initialize', containing +;;; 'initialize-params' in the :params field, describing client +;;; capabilities. The server replies with 'initialized', and describes +;;; its capabilities. + +(define-message initialize-params (work-done-progress-params) + (:process-id (integer :optional t)) + (:client-info (client-info :optional t)) + (:locale (string :optional t)) + (:root-path (string :optional t)) + (:root-uri (uri :optional t)) + (:initialization-options t) + (:capabilities client-capabilities) + (:trace (trace-value :optional t)) + (:workspace-folders (workspace-folder :vector t :optional t))) + +(define-message completion-item-options () + (:label-details-support (boolean :optional t))) + +(define-message work-done-progress-options () + (:work-done-progress (boolean :optional t))) + +(define-message declaration-options (work-done-progress-options)) + +(define-message definition-options (work-done-progress-options)) + +(define-message document-link-options (work-done-progress-options) + (:resolve-provider (boolean :optional t))) + +(define-message document-formatting-options (work-done-progress-options)) + +(define-message document-symbol-options (work-done-progress-options) + (:label (string :optional t))) + +(define-message completion-options (work-done-progress-options) + (:trigger-characters (string :optional t :vector t)) + (:all-commit-characters (string :optional t :vector t)) + (:resolve-provider (boolean :optional t)) + (:completion-item (completion-item-options :optional t))) + +(define-message server-capabilities () + (:position-encoding (position-encoding-kind :optional t)) + (:text-document-sync (text-document-sync-options :optional t)) + #++ (:notebook-document-sync (or notebook-document-sync-options notebook-document-sync-registration-options)) + (:completion-provider (completion-options :optional t)) + #++ (:hover-provider (or boolean hover-options)) + #++ (:signature-help-provider (signature-help-options :optional t)) + #++ (:declaration-provider (declaration-options declaration-registration-options)) + (:definition-provider (definition-options :optional t)) + #++ (:type-definition-provider (or boolean type-definition-options type-definition-registration-options)) + #++ (:implementation-provider (or boolean implementation-options implementation-registration-options)) + #++ (:references-provider (or boolean reference-options)) + #++ (:document-highlight-provider (or boolean document-highlight-options)) + (:document-symbol-provider (document-symbol-options :optional t)) + #++ (:code-action-provider (or boolean code-action-options)) + #++ (:code-lens-provider (code-lens-options :optional t)) + (:document-link-provider (document-link-options :optional t)) + #++ (:color-provider (or boolean document-color-options document-color-registration-options)) + (:document-formatting-provider (document-formatting-options :optional t)) + #++ (:document-range-formatting-provider (or boolean document-range-formatting-options)) + #++ (:document-on-type-formatting-provider (document-on-type-formatting-options :optional t)) + #++ (:rename-provider (or boolean rename-options)) + #++ (:folding-range-provider (or boolean folding-range-options folding-range-registration-options)) + #++ (:execute-command-provider (execute-command-options :optional t)) + #++ (:selection-range-provider (or boolean selection-range-options selection-range-registration-options)) + #++ (:linked-editing-range-provider (or boolean linked-editing-range-options linked-editing-range-registration-options)) + #++ (:call-hierarchy-provider (or boolean call-hierarchy-options call-hierarchy-registration-options)) + #++ (:semantic-tokens-provider (or semantic-tokens-options semantic-tokens-registration-options)) + #++ (:moniker-provider (or boolean moniker-options moniker-registration-options)) + #++ (:type-hierarchy-provider (or boolean type-hierarchy-options type-hierarchy-registration-options)) + #++ (:inline-value-provider (or boolean inline-value-options inline-value-registration-options)) + #++ (:inlay-hint-provider (or boolean inlay-hint-options inlay-hint-registration-options)) + #++ (:diagnostic-provider (or diagnostic-options diagnostic-registration-options)) + #++ (:workspace-symbol-provider (boolean workspace-symbol-options)) + #++ (:workspace (workspace-server-capabilities :optional t)) + (:experimental (t :optional t))) + +(define-message initialize-result () + (:capabilities server-capabilities) + (:server-info server-info)) + +(defun handle-initialize (session params) + (initializing-session session (message-value params)) + (let ((result (make-message 'initialize-result))) + (set-field result (list :server-info :name) "Coalton") + (set-field result (list :capabilities :text-document-sync :open-close) t) + (set-field result (list :capabilities :text-document-sync :change) :full) + (set-field result (list :capabilities :definition-provider :work-done-progress) t) + (set-field result (list :capabilities :document-formatting-provider :work-done-progress) t) + (set-field result (list :capabilities :position-encoding) + (position-encoding session)) + result)) + +(define-handler "initialize" + initialize-params + handle-initialize) + +(define-message initialized-params ()) + +(defun handle-initialized (session params) + (declare (ignore params)) + (initialized-session session)) + +(define-handler "initialized" + initialized-params + handle-initialized) + +(define-message did-change-configuration-params () + (:settings t)) + +(defun handle-did-change-configuration (session params) + (update-configuration session (get-field params :settings))) + +(define-handler "workspace/didChangeConfiguration" + did-change-configuration-params + handle-did-change-configuration) + +(define-message text-document-item () + (:uri uri) + (:language-id string) + (:version integer) + (:text string)) + +;; notification: textDocument/didOpen + +(define-message did-open-text-document-params () + (:text-document text-document-item)) + +(defun handle-text-document-did-open (session params) + ;; (/debug "textDocument/didOpen = ~a" (get-field params (list :text-document :uri))) + (open-document session (get-field params :text-document))) + +(define-handler "textDocument/didOpen" + did-open-text-document-params + handle-text-document-did-open) + +;; notification: textDocument/didChange + +(define-message did-change-text-document-params () + (:text-document text-document-item)) ; FIXME this is wrong: use versioned version! + +(defun handle-text-document-did-change (session params) + ;; (/debug "textDocument/didChange = ~a" (get-field params (list :text-document :uri))) + (change-document session (get-field params :text-document))) + +(define-handler "textDocument/didChange" + did-change-text-document-params + handle-text-document-did-change) + +;; end notification handler + +(define-enum diagnostic-severity () + (:error 1) + (:warning 2) + (:information 3) + (:hint 4)) + +(define-enum diagnostic-tag () + (:unnecessary 1) + (:deprecated 2)) + +(define-message diagnostic-related-information () + (:location location) + (:message string)) + +(define-message diagnostic () + (:range range) + (:severity (diagnostic-severity :optional t)) + (:code (string :optional t)) ; FIXME union int | str + (:source (string :optional t)) + (:message string) + (:tags (diagnostic-tag :vector t :optional t)) + (:related-information (diagnostic-related-information :vector t :optional t)) + (:data (t :optional t))) + +;; notification: textDocument/publishDiagnostics + +(define-message text-document-publish-diagnostics-params () + (:uri uri) + (:version (integer :optional t)) + (:diagnostics (diagnostic :vector t))) + +(defun handle-text-document-publish-diagnostics (session params) + (declare (ignorable session params))) + +(define-handler "textDocument/publishDiagnostics" + text-document-publish-diagnostics-params + handle-text-document-publish-diagnostics) diff --git a/src/server.lisp b/src/server.lisp new file mode 100644 index 0000000..df7ab19 --- /dev/null +++ b/src/server.lisp @@ -0,0 +1,109 @@ +;;;; Functions for starting and stoping the LSP server +;;;; +;;;; Use COALTON-LSP:MAIN to run from a shell, use START-SERVER to run +;;;; in a repl or slime process. + +(in-package :coalton-lsp) + +(defclass server (process) + ((session-id :initform 0 + :accessor session-id) + (config :initarg :config) + (listener :initform nil) + (sessions :initform nil)) + (:documentation "A Coalton LSP server")) + +(defun server-address (server) + "Return the string representation of SERVER's network address." + (with-slots (config) server + (destructuring-bind (&key host port &allow-other-keys) config + (format nil "~a:~a" host port)))) + +(defmethod print-object ((self server) stream) + (print-unreadable-object (self stream :type t :identity t) + (write-string (server-address self) stream))) + +;;; Server run function: listen for and accept connections, and create +;;; sessions. + +(defmethod run ((self server)) + (with-slots (listener sessions) self + (handler-case + (loop :do + (usocket:wait-for-input listener) + (with-lock-held (self) + (when (and listener (usocket::state listener)) + (let ((socket (usocket:socket-accept listener + :element-type 'character))) + (/debug "accept connection on ~a" socket) + (create-session self socket))))) + (usocket:bad-file-descriptor-error () + nil)))) + +(defun create-session (server socket) + "Create a new session that communicates over SOCKET, and ad it to SERVER's session list." + (push (start (make-instance 'session + :id (incf (session-id server)) + :server server + :socket socket)) + (slot-value server 'sessions))) + +(defun delete-session (server session) + "Remove a previously stopped SESSION form server." + (with-slots (lock sessions) server + (bt:with-recursive-lock-held (lock) + (delete session sessions)))) + +;;; Open the server port. The next-method will enter RUN. + +(defmethod start ((server server)) + (with-slots (config listener thread) server + (destructuring-bind (&key interface host port &allow-other-keys) config + (setf listener (usocket:socket-listen (or interface host) port :reuse-address t)))) + (call-next-method)) + +(defmethod stop ((self server)) + (with-lock-held (self) + (with-slots (sessions listener) self + (when listener + (usocket:socket-close listener) + (setf listener nil)) + (dolist (session sessions) + (stop session)))) + (call-next-method)) + +(defvar *default-port* 7887 + "The default port of LSP sessions.") + +(defvar *server* nil + "The server process.") + +(defun stop-server () + "Close all sessions and stop the server." + (when (null *server*) + (/warn "server not running") + (return-from stop-server)) + (stop *server*) + (setf *server* nil) + (/info "server halted")) + +(defun start-server (&optional (port *default-port*)) + "Run a Coalton LSP server on PORT." + (when *server* + (/info "halting server at tcp:~a" (server-address *server*)) + (stop-server)) + (setf *server* + (start (make-instance 'server + :config (list :port port + :host "127.0.0.1")))) + (/info "server started at tcp:~a" (server-address *server*))) + +(defun main (&key (port *default-port*)) + "Run a Coalton LSP server on PORT, halting on interrupt." + (start-server port) + (handler-case + (loop (sleep 1)) + (sb-sys:interactive-interrupt () + (/info "server halted") + (terpri) + (cl-user::quit)))) diff --git a/src/session.lisp b/src/session.lisp new file mode 100644 index 0000000..037406c --- /dev/null +++ b/src/session.lisp @@ -0,0 +1,310 @@ +;;;; Per-socket LSP session + +(in-package :coalton-lsp) + +(defclass session (process) + ((id :initarg :id + :reader session-id) + (server :initarg :server) + (socket :initarg :socket) + (event-queue :initform (make-instance 'worker) + :reader event-queue) + (state :accessor session-state + :initform 'uninitialized) + (params :initform nil + :accessor session-params) + (documents :initform (make-hash-table :test #'equal) + :accessor session-documents)) + (:documentation "Per-connection session data & runloop.")) + +(defmacro with-session-context ((session) &body body) + `(with-logging-context (:session (lambda (stream) + (format stream "session ~d" (session-id ,session)))) + ,@body)) + +(defun submit-event (session method value) + (with-session-context (session) + (/debug "submit-event ~a ~a" method value) + (with-slots (event-queue) session + (enqueue event-queue (cons method value))))) + +(defun process-event (session event) + (with-session-context (session) + (destructuring-bind (method . value) event + (/debug "process-event ~a" method) + (funcall method session value)))) + +(defun session-uri (session) + (cdr (assoc "root-uri" (session-params session) :test #'string=))) + +(defun session-address (self) + (with-slots (server id) self + (format nil "~a:~a" (server-address server) id))) + +(defmethod print-object ((self session) stream) + (print-unreadable-object (self stream :type t) + (format stream "~a uri ~a" (session-address self) (session-uri self)))) + +(defun initializing-session (session params) + (setf (session-state session) 'initializing) + (setf (session-params session) params)) + +(defun initialized-session (session) + (with-session-context (session) + (setf (session-state session) 'initialized) + (/info "initialized") + nil)) + +(defun position-encoding (session) + ':utf16) ; TODO consult capabilities + +(defun update-configuration (session config) + (with-session-context (session) + (/info "updated configuration: ~a" config) + nil)) + +;; key open documents by uri + +(defun open-document (session document) + (with-session-context (session) + (let ((uri (cdr (assoc "uri" document :test #'string=)))) + (/info "open ~a" uri) + (cond ((gethash uri (session-documents session)) + (/info "already open ~a" uri)) + (t + (setf (gethash uri (session-documents session)) document) + (submit-event session 'document-opened uri)))))) + +(defun change-document (session document) ; FIXME endpoint + (with-session-context (session) + (let ((uri (cdr (assoc "uri" document :test #'string=)))) + (submit-event session 'document-opened uri)))) + +(defclass uri-source () + ((uri :initarg :uri))) + +(defmethod coalton-impl/source:source-stream ((self uri-source)) + (lib.uri:input-stream (slot-value self 'uri))) + +(defmethod coalton-impl/source:source-available-p ((self uri-source)) + t) + +(defmethod coalton-impl/source:source-name ((self uri-source)) + (lib.uri::uri-path (slot-value self 'uri))) + +(defun export-condition (condition) + "Extract text and position fields from a Coalton source condition." + (with-open-stream (source-stream (coalton-impl/source::condition-stream condition)) + (let ((state (coalton-impl/source::make-printer-state source-stream condition))) + (mapcar (lambda (note) + (list (coalton-impl/source:message condition) + (coalton-impl/source:message note) + (coalton-impl/source::offset-position state + (coalton-impl/source::start-offset note)) + (coalton-impl/source::offset-position state + (coalton-impl/source::end-offset note)))) + (coalton-impl/source::notes condition))))) + +(defun make-diagnostic (s1 e1 s2 e2 message code) + (let ((diagnostic (make-message 'diagnostic))) + (set-field diagnostic (list :range :start :line) (1- s1)) + (set-field diagnostic (list :range :start :character) e1) + (set-field diagnostic (list :range :end :line) (1- s2)) + (set-field diagnostic (list :range :end :character) e2) + (set-field diagnostic (list :message) message) + (set-field diagnostic (list :code) code) + (set-field diagnostic (list :severity) :warning) + (set-field diagnostic (list :source) "coalton") + diagnostic)) + +(defun make-diagnostics (c) + (mapcar (lambda (e) + (let ((coalton-impl/settings:*coalton-print-unicode* nil)) ; -> ? wut + (destructuring-bind (note message start end) e + (message-value + (make-diagnostic (car start) (cdr start) + (car end) (cdr end) + (format nil "~a - ~a" note message) + 1))))) + (export-condition c))) + +(defun compile-uri (ur-uri) + (let ((uri (lib.uri:parse ur-uri))) + (when uri + (let* ((filename (lib.uri::uri-path uri)) + (source (coalton-impl/source:make-source-file filename))) + (handler-case + (progn + (coalton-impl/entry:compile source :load nil) + nil) + (coalton-impl/source::source-condition (c) + (make-diagnostics c))))))) + +(defun document-opened (session uri) + (let* ((diagnostics (compile-uri uri)) + (message (make-message 'text-document-publish-diagnostics-params))) + (set-field message :uri uri) + (set-field message :diagnostics diagnostics) + (let ((notification (make-notification "textDocument/publishDiagnostics" message))) + (submit-event session 'write-message notification)))) + +(defun session-stream (session) + (usocket:socket-stream (slot-value session 'socket))) + +(define-condition session-exit () + ()) + +(defun make-request (rpc-message) + (make-message (message-type rpc-message) + (parsed-content rpc-message))) + +;;; The message class of the 'result' field in a response message is +;;; that of the result message. Build a customized response-message by +;;; using that to set the field type so that the serializer can do its +;;; work. The only other thing needed is the associated request id. + +(defun make-response (id result) + (let ((class (copy-message-class (get-message-class 'response-message)))) + (set-field-class class :result (message-class result)) + (let ((response (make-instance 'message :class class))) + (set-field response :jsonrpc "2.0") + (set-field response :id id) + (set-field response :result (message-value result)) + response))) + +(defun make-error-response (id condition) + (let ((response (make-message 'response-message))) + (set-field response :jsonrpc "2.0") + (set-field response :id id) + (set-field response (list :error :code) (error-code condition)) + (set-field response (list :error :message) (error-message condition)) + response)) + +(defun make-notification (method params) + (let ((class (copy-message-class (get-message-class 'notification-message)))) + (set-field-class class :params (message-class params)) + (let ((notification (make-instance 'message :class class))) + (set-field notification :jsonrpc "2.0") + (set-field notification :method method) + (set-field notification :params (message-value params)) + notification))) + +(defun message-p (message message-class) + (eq (name (slot-value message 'class)) message-class)) + +(define-condition lsp-error (error) + ((code :initarg :code + :accessor error-code) + (message :initarg :message + :accessor error-message))) + +(defun response-error (error-code args) + (apply #'/error args) + (error 'lsp-error + :code error-code + :message (apply #'format nil args))) + +(defun invalid-request (&rest args) + (response-error :invalid-request args)) + +(defun method-not-found (&rest args) + (response-error :method-not-found args)) + +(defun request-method (request) + (let ((rpc-version (get-field request :jsonrpc)) + (method (get-field request :method))) + (cond ((not (string-equal rpc-version "2.0")) + (invalid-request "Bad rpc version ~a" rpc-version)) + ((not method) + (invalid-request "Missing method"))) + method)) + +(defvar *message-handlers* + (make-hash-table :test 'equal)) + +(defstruct message-handler + params + fn) + +(defun get-message-handler (method) + (let ((handler (gethash method *message-handlers*))) + (unless handler + (method-not-found "Unsupported method '~a'" method)) + handler)) + +(defmacro define-handler (method params fn) + `(setf (gethash ,method *message-handlers*) + (make-message-handler :params ',params + :fn ',fn))) + +(defun request-params (request) + "Return REQUEST's params message." + (let* ((method (request-method request)) + (params-message-class (message-handler-params (gethash method *message-handlers*)))) + (when params-message-class + (make-message params-message-class (get-field request :params))))) + +(defun process-notification (session request) + (handler-case + (let* ((handler (get-message-handler (request-method request))) + (params (request-params request))) + (funcall (message-handler-fn handler) session params)) + (lsp-error () + ;; logged when thrown + ))) + +(defun process-request (session request) + (handler-case + (let* ((handler (get-message-handler (request-method request))) + (params (request-params request)) + (result (funcall (message-handler-fn handler) session params))) + (make-response (get-field request :id) result)) + (lsp-error (condition) + (make-error-response (get-field request :id) condition)))) + +(defun process-message (session message) + (let ((request (make-request message))) + (cond ((message-p request 'notification-message) + (process-notification session request)) + (t + (submit-event session 'write-message + (process-request session request)))))) + +(defun write-message (session response) + (let ((json (to-json response))) + (with-lock-held (session) + (write-rpc json (session-stream session))))) + +(defmethod run ((self session)) + (with-slots (event-queue) self + (setf (slot-value event-queue 'fn) + (lambda (event) + (process-event self event))) + (start event-queue)) + (handler-case + (loop :do + (handler-case + (progn + (let ((message (read-rpc (session-stream self)))) + (submit-event self 'process-message message))) + (sb-int:closed-stream-error () + (/info "remote session disconnected (stream closed)") + (signal 'session-exit)) + (end-of-file () + (/info "remote session disconnected (end of file)") + (signal 'session-exit)) + (error (c) + (/error "aborted read: session shutdown: ~a" c) + (signal 'session-exit)))) + (session-exit () + (stop self)))) + +(defmethod stop ((session session)) + (with-session-context (session) + (/info "stopping") + (with-slots (server socket event-queue) session + (stop event-queue) + (usocket:socket-close socket) + (delete-session server session)) + (call-next-method) + session)) diff --git a/swank-coalton.lisp b/swank-coalton.lisp deleted file mode 100644 index 74434c0..0000000 --- a/swank-coalton.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;; swank-coalton.lisp - -(in-package :swank) - -(defun system-loaded-p (system-designator) - (find system-designator (asdf:already-loaded-systems) - :test #'string=)) - -(defun system-available-p (system-designator) - (asdf:find-system system-designator)) - -(defun system-status (system-designator) - (cond ((system-loaded-p system-designator) - :loaded) - ((system-available-p system-designator) - :available) - (t - :unavailable))) - - -(defslimefun swank-coalton-status () - (system-status "coalton")) - -(defslimefun swank-coalton-init () - (asdf:load-system "coalton")) - - -(defslimefun swank-coalton--codegen (text) - (let ((source (coalton-impl/source:make-source-string text))) - (coalton-impl/entry:codegen source))) - -(defslimefun swank-coalton--compile (text) - (let ((source (coalton-impl/source:make-source-string text))) - (coalton-impl/entry:compile source :load t) - "Success")) diff --git a/test/types.coal b/test/types.coal deleted file mode 100644 index 4a21864..0000000 --- a/test/types.coal +++ /dev/null @@ -1,95 +0,0 @@ -(package coalton-library/types - (export - Proxy - proxy-of - as-proxy-of - proxy-inner - LispType - RuntimeRepr - runtime-repr - runtime-repr-of)) - -(repr :enum) -(define-type (Proxy :a) - "Proxy holds no data, but has a phantom type parameter." - Proxy) - -(declare proxy-of (:a -> Proxy :a)) -(define (proxy-of _) - "Returns a Proxy containing the type of the parameter." - Proxy) - -(declare as-proxy-of (:a -> Proxy :a -> :a)) -(define (as-proxy-of x _) - "Returns the parameter, forcing the proxy to have the same type as the parameter." - x) - -(declare proxy-inner (Proxy (:a :b) -> Proxy :b)) -(define (proxy-inner _) - Proxy) - -(repr :native (cl:or cl:symbol cl:list)) -(define-type LispType - "The runtime representation of a Coalton type as a lisp type.") - -(define-class (RuntimeRepr :a) - "Types which have a runtime LispType representation. - -`runtime-repr` corresponds to the type emitted by the Coalton compiler for the type parameter to the given Proxy. - -The compiler will auto-generate instances of `RuntimeRepr` for all defined types." - (runtime-repr (Proxy :a -> LispType))) - -(declare runtime-repr-of (RuntimeRepr :a => :a -> LispType)) -(define (runtime-repr-of x) - "Returns the runtime representation of the type of the given value." - (runtime-repr (proxy-of x))) - -;; Additional RuntimeRepr instances for early-defined types - -(define-instance (RuntimeRepr Boolean) - (define (runtime-repr _) - (lisp LispType () 'cl:boolean))) - -(define-instance (RuntimeRepr Char) - (define (runtime-repr _) - (lisp LispType () 'cl:character))) - -(define-instance (RuntimeRepr Integer) - (define (runtime-repr _) - (lisp LispType () 'cl:integer))) - -(define-instance (RuntimeRepr Single-Float) - (define (runtime-repr _) - (lisp LispType () 'cl:single-float))) - -(define-instance (RuntimeRepr Double-Float) - (define (runtime-repr _) - (lisp LispType () 'cl:double-float))) - -(define-instance (RuntimeRepr String) - (define (runtime-repr _) - (lisp LispType () 'cl:string))) - -(define-instance (RuntimeRepr Fraction) - (define (runtime-repr _) - (lisp LispType () 'cl:rational))) - -(define-instance (RuntimeRepr (:a -> :b)) - (define (runtime-repr _) - (lisp LispType () 'coalton-impl/runtime/function-entry:function-entry))) - -(define-instance (RuntimeRepr (List :a)) - (define (runtime-repr _) - (lisp LispType () 'cl:list))) - -;; The compiler will not auto-generate RuntimeRepr instances for -;; types defined in this file to avoid circular dependencies. - -(define-instance (RuntimeRepr LispType) - (define (runtime-repr _) - (lisp LispType () '(cl:or cl:symbol cl:list)))) - -(define-instance (RuntimeRepr (Proxy :a)) - (define (runtime-repr _) - (lisp LispType () '(cl:member 'proxy/proxy)))) diff --git a/tests/json-tests.lisp b/tests/json-tests.lisp new file mode 100644 index 0000000..1cc12d4 --- /dev/null +++ b/tests/json-tests.lisp @@ -0,0 +1,7 @@ +(in-package #:coalton-lsp/tests) + +(deftest json-tests/decode () + + (is (equalp (lsp::decode-json + "{\"key\": \"value\"}") + '(("key" . "value"))))) diff --git a/tests/lsp-tests.lisp b/tests/lsp-tests.lisp new file mode 100644 index 0000000..b295b9f --- /dev/null +++ b/tests/lsp-tests.lisp @@ -0,0 +1,9 @@ +(in-package #:coalton-lsp/tests) + +(deftest lsp-tests/initialize-result () + (let ((x (lsp::make-message 'lsp::initialize-result))) + (lsp::set-field x (list :capabilities :position-encoding) + :utf32) + (lsp::set-field x (list :server-info :name) + "Emacs") + x)) diff --git a/tests/message-tests.lisp b/tests/message-tests.lisp new file mode 100644 index 0000000..e605ffc --- /dev/null +++ b/tests/message-tests.lisp @@ -0,0 +1,15 @@ +(in-package :coalton-lsp/tests) + +(lsp::define-message test-message () + (:enable boolean) + (:flags (string :vector t)) + (:debug (boolean :optional t))) + +(deftest message-tests/value () + (let ((message (lsp::make-message 'test-message))) + (lsp::set-field message :debug t) + (lsp::set-field message :flags (list "a" "b" "g")) + (is (equalp (lsp::message-value message) + '(("flags" "a" "b" "g") + ("debug" . T)))) + message)) diff --git a/tests/mock.lisp b/tests/mock.lisp new file mode 100644 index 0000000..93e10a0 --- /dev/null +++ b/tests/mock.lisp @@ -0,0 +1,13 @@ +(in-package :coalton-lsp/tests) + +(defun mock-publish-diagnostics () + (lsp::make-notification + "textDocument/publishDiagnostics" + (let ((message (lsp::make-message 'lsp::text-document-publish-diagnostics-params))) + (lsp::set-field message :uri "file:///Users/jlbouwman/git/coalton-mode/resources/fib.coal") + (lsp::set-field message :diagnostics + (list (lsp::message-value + (lsp::make-diagnostic 4 4 4 7 + "export: 'fob' is undefined" + "undefined-export")))) + message))) diff --git a/tests/package.lisp b/tests/package.lisp new file mode 100644 index 0000000..e96489c --- /dev/null +++ b/tests/package.lisp @@ -0,0 +1,51 @@ +(fiasco:define-test-package #:coalton-lsp/tests + (:use + #:cl) + (:local-nicknames + (#:lsp #:coalton-lsp))) + +(in-package #:coalton-lsp/tests) + +(defun is-string= (a b &optional (message "input")) + "If strings A and B differ, signal a failure reporting the first position where this is true." + (let ((compare-len (min (length a) + (length b)))) + (loop :for i :from 0 :below compare-len + :unless (char= (aref a i) + (aref b i)) + :do (is (string= a b) + (format nil "Strings differ at offset ~A of ~A:~%A: ~A~%B: ~A" + i message a b)) + (return-from is-string=)) + (is (= (length a) + (length b)) + (format nil "Strings differ at offset ~A of ~A:~%~A~%~A" + compare-len message a b)))) + +(defun repository-path () + (let ((path (namestring (asdf:system-source-directory "coalton-lsp")))) + (subseq path 0 (1- (length path))))) + +(defun rpc-file (name) + (let ((path (format nil "~a/resources/rpc/~a" (repository-path) "initialize.json"))) + (unless (probe-file path) + (error "RPC example input ~a not found at ~a" name path)) + path)) + +(defun pipe (input output &aux (buflen 8192)) + (let ((buf (make-array buflen :element-type 'character))) + (loop + :for bytes = (read-sequence buf input) + :do (write-sequence buf output :start 0 :end bytes) + :while (= bytes buflen)))) + +(defun read-file (filename) + (with-output-to-string (output) + (with-open-file (input filename :direction ':input) + (pipe input output)))) + +(defun rpc-example (name) + (make-instance 'lsp::rpc-message + :content (read-file (rpc-file name)))) + +#+example (rpc-example "initialize.json") diff --git a/tests/protocol-tests.lisp b/tests/protocol-tests.lisp new file mode 100644 index 0000000..d45244e --- /dev/null +++ b/tests/protocol-tests.lisp @@ -0,0 +1,6 @@ +(in-package #:coalton-lsp/tests) + +(deftest protocol-tests/initialize () + (let ((params (lsp::request-params (lsp::make-request (rpc-example "initialize.json"))))) + (lsp::get-field params :root-uri) + (lsp::get-field params :workspace-folders))) diff --git a/tests/rpc-tests.lisp b/tests/rpc-tests.lisp new file mode 100644 index 0000000..5c98cfe --- /dev/null +++ b/tests/rpc-tests.lisp @@ -0,0 +1,17 @@ +(in-package #:coalton-lsp/tests) + +(deftest rpc-tests/headers () + (let ((parsed-headers + '(("Content-Length" . "2") + ("Content-Type" . "application/json"))) + (serialized-headers + "Content-Length: 2 +Content-Type: application/json + +")) + (is-string= (with-output-to-string (stream) + (lsp::write-headers stream parsed-headers)) + serialized-headers) + (is (equalp (with-input-from-string (stream serialized-headers) + (lsp::read-headers stream)) + parsed-headers)))) diff --git a/tests/session-tests.lisp b/tests/session-tests.lisp new file mode 100644 index 0000000..83c8d3e --- /dev/null +++ b/tests/session-tests.lisp @@ -0,0 +1,57 @@ +(in-package #:coalton-lsp/tests) + +(deftest session-tests/initialize () + (let ((session (make-instance 'lsp::session))) + (is (equalp (lsp::message-value + (lsp::process-request + session (lsp::make-request (rpc-example "initialize.json")))) + '(("result" + ("capabilities" ("positionEncoding" . "utf-16") + ("documentFormattingProvider" ("workDoneProgress" . T)) + ("definitionProvider" ("workDoneProgress" . T)) + ("textDocumentSync" ("change" . 1) ("openClose" . T))) + ("serverInfo" ("name" . "Coalton"))) + ("id" . 1) ("jsonrpc" . "2.0")))))) + +(deftest session-tests/get-field () + (let ((init (lsp::make-request (rpc-example "initialize.json")))) + (is (eq 1 (lsp::get-field init :id))) + + (let ((params (lsp::request-params init))) + (is (eq t + (lsp::get-field params '(:capabilities :workspace + :did-change-watched-files :dynamic-registration))))))) + +(deftest session-tests/set-field () + (let ((params (lsp::make-message 'lsp::initialize-params))) + (lsp::message-value (lsp::set-field-1 params :capabilities 'x))) + (let ((params (lsp::make-message 'lsp::initialize-params))) + (lsp::set-field params '(:capabilities :workspace) 'x))) + +(deftest session-tests/encode-json () + (is-string= (lsp::to-json + (let ((session (make-instance 'lsp::session))) + (lsp::process-request + session (lsp::make-request (rpc-example "initialize.json"))))) + "{ + \"jsonrpc\": \"2.0\", + \"id\": 1, + \"result\": { + \"capabilities\": { + \"positionEncoding\": \"utf-16\", + \"textDocumentSync\": { + \"openClose\": true, + \"change\": 1 + }, + \"definitionProvider\": { + \"workDoneProgress\": true + }, + \"documentFormattingProvider\": { + \"workDoneProgress\": true + } + }, + \"serverInfo\": { + \"name\": \"Coalton\" + } + } +}"))