From 39d17b9a3f666704616719b353e4b81486f4a0bd Mon Sep 17 00:00:00 2001 From: Nathan Weir Date: Sat, 11 Apr 2026 15:36:29 -0800 Subject: [PATCH] Consolidate server state onto a single server-context struct Previously CLEF's mutable state was scattered across defparameters in three packages: clef-lsp/server (*documents*, *workspace-root*, *handlers*, *client-capabilities*, *initialized*), clef-symbols (*lexical-scopes-by-file*, *symbol-refs-by-file*, *workspace-symbol-index*, *document-line-offsets*, *global-scope*), and clef-lsp/lifecycle (*loaded-systems*, *file-to-system*, *asd-files*). Resetting between tests meant juggling every one of them, and new handlers reaching across modules had to know which package owned each global. All of that persistent state now lives on a single SERVER-CONTEXT struct in a new clef-context package, bound to clef-context:*server*. Short symbol-macro aliases (ctx:documents, ctx:workspace-root, ctx:handlers, ctx:lexical-scopes, ctx:loaded-systems, ...) expand to struct-accessor reads on *server* so call sites read and write them as ordinary places, including with setf. Shutdown and test setup now replace *server* with a fresh context in one call via ctx:reset-context. The transient walker state in clef-symbols (*current-scope*, *current-package*) stays as dynamic specials -- it only matters during one build-file-symbol-map call and doesn't belong on the shared context. Handler registration for textDocument/documentHighlight, textDocument/signatureHelp, and workspace/symbol has moved out of inline sethandler calls at the bottom of each handler file and into register-handlers, matching the rest of the endpoints. All 63 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 42 ++++++--- clef.asd | 1 + src/context.lisp | 132 +++++++++++++++++++++++++++ src/lsp/document/diagnostic.lisp | 2 +- src/lsp/document/did-change.lisp | 8 +- src/lsp/document/did-open.lisp | 4 +- src/lsp/document/did-save.lisp | 10 +- src/lsp/document/formatting.lisp | 2 +- src/lsp/document/highlight.lisp | 8 +- src/lsp/document/hover.lisp | 4 +- src/lsp/document/references.lisp | 6 +- src/lsp/document/signature-help.lisp | 6 +- src/lsp/lifecycle/initialize.lisp | 63 ++++++------- src/lsp/lifecycle/initialized.lisp | 2 +- src/lsp/server.lisp | 64 ++++++------- src/lsp/workspace/symbol.lisp | 6 +- src/packages.lisp | 103 ++++++++++++++------- src/symbols/init.lisp | 107 ++++++++++------------ test/document-tests.lisp | 8 +- test/lifecycle-tests.lisp | 10 +- 20 files changed, 377 insertions(+), 211 deletions(-) create mode 100644 src/context.lisp diff --git a/CLAUDE.md b/CLAUDE.md index 2722f0f..fc20754 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,16 +53,17 @@ To add a new test: ### Communication Flow 1. Editor sends JSON-RPC requests via stdio 2. `read-lsp-message` (jsonrpc/messages.lisp) parses HTTP-like headers + JSON body -3. Requests dispatch to handlers registered in `*handlers*` hash table -4. Handlers access global state (`*documents*`, `*workspace-root*`, symbol tables) +3. Requests dispatch to handlers registered on the server context (`ctx:handlers`) +4. Handlers access shared state through `clef-context` accessors (`ctx:documents`, `ctx:workspace-root`, symbol tables, ...) 5. Responses convert to JSON-RPC and write to stdout ### Key Source Modules (src/) | Module | Purpose | |--------|---------| +| `context.lisp` | Central `server-context` struct + `*server*` — all persistent state lives here | | `jsonrpc/` | JSON-RPC protocol implementation | -| `lsp/server.lisp` | Main server loop, handler registration, state management | +| `lsp/server.lisp` | Main server loop, handler dispatch | | `lsp/lifecycle/` | Initialize/initialized/shutdown handlers | | `lsp/document/` | Document handlers (completion, definition, hover, formatting, diagnostics) | | `lsp/workspace/` | Workspace-level handlers | @@ -73,15 +74,32 @@ To add a new test: | `packages.lisp` | Package definitions and namespace exports | | `main.lisp` | Entry point (`clef-root:start-server`) | -### Global State Variables - -- `*documents*` - Hash table of open files (path → full text) -- `*handlers*` - Hash table mapping LSP methods to handler functions -- `*lexical-scopes-by-file*` - Maps files to interval trees of lexical scopes -- `*symbol-refs-by-file*` - Maps files to symbol references with location info -- `*workspace-root*` - Root directory of project -- `*client-capabilities*` - What the client editor supports -- `*initialized*` - Boolean for LSP lifecycle state +### Server Context (`clef-context`) + +All persistent server state lives on a single `server-context` struct held in +`clef-context:*server*`. Short symbol-macro aliases (`ctx:documents`, +`ctx:workspace-root`, `ctx:handlers`, ...) expand to struct-accessor reads on +`*server*`, so call sites read and write them as if they were ordinary +variables, including with `setf`. + +Fields on the context include: + +- `ctx:documents` — hash table of open files (URI → full text) +- `ctx:handlers` — hash table mapping LSP methods to handler functions +- `ctx:workspace-root` — project workspace root URI +- `ctx:client-capabilities` — client capabilities reported at initialize time +- `ctx:initialized` / `ctx:shutdown-received` — lifecycle flags +- `ctx:output-stream` — stream for outbound LSP notifications +- `ctx:lexical-scopes` / `ctx:symbol-refs` — per-file interval trees +- `ctx:workspace-symbol-index` — cross-file symbol lookup table +- `ctx:document-line-offsets` — per-file byte offset caches +- `ctx:global-scope` — root lexical-scope (builtins + external packages) +- `ctx:loaded-systems` / `ctx:file-to-system` / `ctx:asd-files` — ASDF state + +Shutdown and exit handlers call `ctx:reset-context` to atomically replace +`*server*` with a fresh context, which also gives tests a clean slate between +runs. No CLEF package should define its own mutable `defparameter` for +server state — put new fields on the struct in `src/context.lisp` instead. ### Symbol Resolution diff --git a/clef.asd b/clef.asd index f445445..7a41d07 100644 --- a/clef.asd +++ b/clef.asd @@ -19,6 +19,7 @@ :components ((:file "packages") (:file "util") (:file "log") + (:file "context") (:file "jsonrpc/types") (:file "jsonrpc/messages") (:file "parser/parser") diff --git a/src/context.lisp b/src/context.lisp new file mode 100644 index 0000000..d4f6fa8 --- /dev/null +++ b/src/context.lisp @@ -0,0 +1,132 @@ +(in-package :clef-context) + +;;; Central server context. +;;; +;;; Historically CLEF scattered its mutable state across many defparameters in +;;; several packages (clef-lsp/server, clef-symbols, clef-lsp/lifecycle). That +;;; made it hard to reset between test runs, impossible to host more than one +;;; server in a single image, and awkward when new handlers needed to reach +;;; across module boundaries to touch state. +;;; +;;; This file consolidates everything onto a single SERVER-CONTEXT struct held +;;; in the special variable *SERVER*. Each field corresponds to one of the old +;;; globals. Short symbol-macro aliases expand to field access on *SERVER*, +;;; so call sites look like ordinary variable references: +;;; +;;; (gethash uri (documents)) ; reads (server-context-documents *server*) +;;; (setf (workspace-root) path) ; writes (server-context-workspace-root *server*) +;;; +;;; Tests and shutdown handlers can swap *SERVER* with a fresh context to reset +;;; all state atomically. + +(defstruct server-context + "All persistent state for a running CLEF LSP server. + +Fields are grouped by subsystem: + +Lifecycle: + initialized -- set once the client sends the `initialized' notification + shutdown-received -- set when the client sends `shutdown'; used by `exit' + client-capabilities -- hash table of capabilities from `initialize' + workspace-root -- URI of the project root + output-stream -- stream for outbound LSP messages (notifications) + +Handlers & documents: + handlers -- method-name -> handler-function + documents -- document URI -> full text string + +Symbol analysis (formerly in clef-symbols): + lexical-scopes -- file path -> interval tree of lexical-scope's + symbol-refs -- file path -> interval tree of symbol-reference's + workspace-symbol-index -- symbol-name -> list of symbol-definitions (cross-file) + document-line-offsets -- file path -> vector of per-line byte offsets + global-scope -- root lexical-scope holding builtins + externals + +ASDF systems (formerly in clef-lsp/lifecycle): + loaded-systems -- system name -> system-info + file-to-system -- absolute file path -> owning system name + asd-files -- list of all discovered .asd pathnames" + (initialized nil :type boolean) + (shutdown-received nil :type boolean) + (client-capabilities nil) + (workspace-root nil) + (output-stream nil) + (handlers (make-hash-table :test 'equal)) + (documents (make-hash-table :test 'equal)) + (lexical-scopes (make-hash-table :test 'equal)) + (symbol-refs (make-hash-table :test 'equal)) + (workspace-symbol-index (make-hash-table :test 'equal)) + (document-line-offsets (make-hash-table :test 'equal)) + (global-scope nil) + (loaded-systems (make-hash-table :test 'equal)) + (file-to-system (make-hash-table :test 'equal)) + (asd-files nil)) + +(defparameter *server* (make-server-context) + "The current CLEF LSP server context. + +Bound to a freshly constructed SERVER-CONTEXT at image load, replaced on +RESET-CONTEXT, and rebindable with LET for isolated tests.") + +(defun reset-context () + "Replace *SERVER* with a fresh context, discarding all server state. +Called from shutdown and exit handlers and from test setup." + (setf *server* (make-server-context))) + +;;; Symbol-macro aliases. +;;; +;;; These let callers write `(documents)' or `(setf (workspace-root) x)' as +;;; if they were plain functions/places, without the noise of threading +;;; *server* through every call. Setf works transparently because each +;;; expansion bottoms out in a struct accessor, which has a real setf +;;; expander. + +(defmacro define-context-accessor (short-name field-accessor docstring) + "Define SHORT-NAME as a symbol-macro that reads/writes (FIELD-ACCESSOR *SERVER*)." + (declare (ignore docstring)) + `(define-symbol-macro ,short-name (,field-accessor *server*))) + +(define-context-accessor initialized server-context-initialized + "Whether the client has confirmed initialization.") + +(define-context-accessor shutdown-received server-context-shutdown-received + "Whether shutdown has been requested.") + +(define-context-accessor client-capabilities server-context-client-capabilities + "Client capabilities reported at initialize time.") + +(define-context-accessor workspace-root server-context-workspace-root + "Project workspace root URI.") + +(define-context-accessor output-stream server-context-output-stream + "Outbound LSP message stream.") + +(define-context-accessor handlers server-context-handlers + "Method name -> handler function.") + +(define-context-accessor documents server-context-documents + "Document URI -> current text.") + +(define-context-accessor lexical-scopes server-context-lexical-scopes + "File path -> interval tree of lexical scopes.") + +(define-context-accessor symbol-refs server-context-symbol-refs + "File path -> interval tree of symbol references.") + +(define-context-accessor workspace-symbol-index server-context-workspace-symbol-index + "Symbol name -> list of symbol-definitions, across all files.") + +(define-context-accessor document-line-offsets server-context-document-line-offsets + "File path -> vector of byte offsets for each line.") + +(define-context-accessor global-scope server-context-global-scope + "Root lexical-scope for the workspace (holds builtins + externals).") + +(define-context-accessor loaded-systems server-context-loaded-systems + "System name -> system-info for ASDF systems discovered in the workspace.") + +(define-context-accessor file-to-system server-context-file-to-system + "Absolute file path -> system name that owns it.") + +(define-context-accessor asd-files server-context-asd-files + "List of every .asd file discovered in the workspace.") diff --git a/src/lsp/document/diagnostic.lisp b/src/lsp/document/diagnostic.lisp index fd827bb..51b06d3 100644 --- a/src/lsp/document/diagnostic.lisp +++ b/src/lsp/document/diagnostic.lisp @@ -58,7 +58,7 @@ (let* ((document-uri (href (clef-jsonrpc/types:request-params message) "text-document" "uri")) - (document-text (gethash document-uri clef-lsp/server:*documents*)) + (document-text (gethash document-uri ctx:documents)) (syntax-errors (get-syntax-errors document-text)) (compile-errors (debounced-sb-collect-diagnostics document-text document-uri)) (items (append syntax-errors compile-errors))) diff --git a/src/lsp/document/did-change.lisp b/src/lsp/document/did-change.lisp index 635cc69..9b25b29 100644 --- a/src/lsp/document/did-change.lisp +++ b/src/lsp/document/did-change.lisp @@ -31,21 +31,21 @@ ;; Unpack the params into the document uri and range/text data (let* ((params (clef-jsonrpc/types:request-params message)) (document-uri (href params "text-document" "uri")) - (content-changes (href params "content-changes"))) + (content-changes (href params "content-changes")) + (documents ctx:documents)) (slog :debug "[textDocument/didChange] Document: ~A" document-uri) - ;; (slog :debug "[textDocument/didChange] File found: ~A" (nth-value 1 (gethash document-uri clef-lsp/server:*documents*))) (dotimes (i (length content-changes)) (let* ((content-change (aref content-changes i)) (new-document-text (href content-change "text"))) - (setf (gethash document-uri clef-lsp/server:*documents*) new-document-text))) + (setf (gethash document-uri documents) new-document-text))) ;; Reprocess the symbol-map. This is terribly jank and inefficient to do on every single change; needs debounced at the very least (slog :debug "[textDocument/didChange] Rebuilding symbol map for document: ~A..." document-uri) (let ((start-time (get-internal-real-time))) (clef-symbols:build-file-symbol-map (clef-util:cleanup-path document-uri) - (gethash document-uri clef-lsp/server:*documents*)) + (gethash document-uri documents)) (slog :debug "[textDocument/didChange] Rebuilt symbol map in ~A ms." (/ (- (get-internal-real-time) start-time) 1000.0))))) diff --git a/src/lsp/document/did-open.lisp b/src/lsp/document/did-open.lisp index 88635a3..ae32faa 100644 --- a/src/lsp/document/did-open.lisp +++ b/src/lsp/document/did-open.lisp @@ -4,6 +4,4 @@ (let* ((params-hash (clef-jsonrpc/types:request-params message)) (document-uri (href params-hash "text-document" "uri")) (document-text (href params-hash "text-document" "text"))) - - ;; (slog :debug "opened text: ~A" document-text) - (setf (gethash document-uri clef-lsp/server:*documents*) document-text))) + (setf (gethash document-uri ctx:documents) document-text))) diff --git a/src/lsp/document/did-save.lisp b/src/lsp/document/did-save.lisp index 04935fa..b113a9d 100644 --- a/src/lsp/document/did-save.lisp +++ b/src/lsp/document/did-save.lisp @@ -9,19 +9,19 @@ (reload-asd-file document-uri)) ;; Rebuild symbol map for the saved file (let ((document-text (gethash (format nil "file://~A" document-uri) - clef-lsp/server:*documents*))) + ctx:documents))) (when document-text (clef-symbols:build-file-symbol-map document-uri document-text))))) (defun reload-asd-file (asd-path) "Re-parse an .asd file and reload any changed systems." (slog :debug "Reloading .asd file: ~A" asd-path) - (let ((new-systems (clef-lsp/lifecycle::parse-asd-file asd-path))) + (let ((new-systems (clef-lsp/lifecycle:parse-asd-file asd-path))) (dolist (sys new-systems) (let ((name (clef-symbols:system-info-name sys))) ;; Update or add the system info - (setf (gethash name clef-lsp/lifecycle::*loaded-systems*) sys) + (setf (gethash name ctx:loaded-systems) sys) ;; Reload the system - (clef-lsp/lifecycle::load-system-with-info sys))) + (clef-lsp/lifecycle:load-system-with-info sys))) ;; Rebuild file mapping - (clef-lsp/lifecycle::build-file-to-system-mapping))) + (clef-lsp/lifecycle:build-file-to-system-mapping))) diff --git a/src/lsp/document/formatting.lisp b/src/lsp/document/formatting.lisp index 643b1b9..b32000a 100644 --- a/src/lsp/document/formatting.lisp +++ b/src/lsp/document/formatting.lisp @@ -17,7 +17,7 @@ (defun handle-text-document-formatting (message) (let* ((params (clef-jsonrpc/types:request-params message)) (file-uri (href params "text-document" "uri")) - (document-text (gethash file-uri clef-lsp/server:*documents*)) + (document-text (gethash file-uri ctx:documents)) (pos (href params "position")) (options (href params "options"))) ;; Format the entire file with cl-indentify diff --git a/src/lsp/document/highlight.lisp b/src/lsp/document/highlight.lisp index a98686d..6e982fb 100644 --- a/src/lsp/document/highlight.lisp +++ b/src/lsp/document/highlight.lisp @@ -38,7 +38,7 @@ Returns all occurrences of the symbol under cursor in the current document." ;; Find all occurrences in this file (let ((highlights '())) ;; Add all references in this file - (let ((refs-tree (gethash file-path clef-symbols:*symbol-refs-by-file*))) + (let ((refs-tree (gethash file-path ctx:symbol-refs))) (when refs-tree (let ((all-refs (get-all-intervals-from-tree refs-tree))) (dolist (interval all-refs) @@ -51,7 +51,7 @@ Returns all occurrences of the symbol under cursor in the current document." highlights))))))) ;; Add definitions in this file - (let ((scopes-tree (gethash file-path clef-symbols:*lexical-scopes-by-file*))) + (let ((scopes-tree (gethash file-path ctx:lexical-scopes))) (when scopes-tree (let ((all-scopes (get-all-intervals-from-tree scopes-tree))) (dolist (scope-interval all-scopes) @@ -72,7 +72,3 @@ Returns all occurrences of the symbol under cursor in the current document." "Create an LSP DocumentHighlight dict from a tree-sitter node." (dict "range" (node-to-lsp-range node) "kind" kind)) - -;; Register the handler -(setf (gethash "textDocument/documentHighlight" clef-lsp/server:*handlers*) - #'handle-text-document-highlight) diff --git a/src/lsp/document/hover.lisp b/src/lsp/document/hover.lisp index bafb885..a09308c 100644 --- a/src/lsp/document/hover.lisp +++ b/src/lsp/document/hover.lisp @@ -46,10 +46,10 @@ (hover-line (href params "position" "line")) (hover-char (href params "position" "character")) (symbol-at-pos (find-symbol-at-position - (href clef-lsp/server:*documents* document-uri) + (gethash document-uri ctx:documents) hover-line hover-char)) - (document-text (href clef-lsp/server:*documents* document-uri)) + (document-text (gethash document-uri ctx:documents)) (tree (clef-parser/parser:parse-string document-text)) (symbol-pkg (or (clef-parser/utils:find-package-declaration tree document-text) *package*)) diff --git a/src/lsp/document/references.lisp b/src/lsp/document/references.lisp index 0cf9284..42d8b2e 100644 --- a/src/lsp/document/references.lisp +++ b/src/lsp/document/references.lisp @@ -55,10 +55,10 @@ Returns all locations where the symbol at the given position is referenced." This handles the case where the cursor is on a function/variable name in a definition (e.g., the 'foo' in '(defun foo ...)'), rather than on a usage." (let* ((file-path (clef-util:cleanup-path document-uri)) - (offset (clef-symbols::line-char-to-byte-offset file-path line character))) + (offset (clef-symbols:line-char-to-byte-offset file-path line character))) ;; Get the lexical scope at this position (let ((scopes (interval:find-all - (gethash file-path clef-symbols:*lexical-scopes-by-file*) + (gethash file-path ctx:lexical-scopes) offset))) ;; Check each scope (from innermost to outermost) for definitions at this position (dolist (scope-interval scopes) @@ -100,7 +100,7 @@ Returns a list of LSP Location dicts." ;; Walk all intervals in the tree to find matching symbol names (let ((file-refs (find-refs-in-tree refs-tree symbol-name file-path))) (setf locations (nconc locations file-refs))))) - clef-symbols:*symbol-refs-by-file*) + ctx:symbol-refs) locations)) (defun find-refs-in-tree (refs-tree symbol-name file-path) diff --git a/src/lsp/document/signature-help.lisp b/src/lsp/document/signature-help.lisp index 6b52e2e..1851551 100644 --- a/src/lsp/document/signature-help.lisp +++ b/src/lsp/document/signature-help.lisp @@ -8,7 +8,7 @@ Returns signature information for the function call at the cursor position." (position (href params "position")) (line (href position "line")) (character (href position "character")) - (document-text (gethash document-uri clef-lsp/server:*documents*))) + (document-text (gethash document-uri ctx:documents))) (slog :debug "[textDocument/signatureHelp] Document: ~A" document-uri) (slog :debug "[textDocument/signatureHelp] Position: line ~A, char ~A" line character) @@ -214,7 +214,3 @@ Tries workspace index first, then falls back to sb-introspect for loaded functio (dict "signatures" (vector signature) "activeSignature" 0 "activeParameter" (or active-param 0)))) - -;; Register the handler -(setf (gethash "textDocument/signatureHelp" clef-lsp/server:*handlers*) - #'handle-text-document-signature-help) diff --git a/src/lsp/lifecycle/initialize.lisp b/src/lsp/lifecycle/initialize.lisp index 9501f57..3915ad4 100644 --- a/src/lsp/lifecycle/initialize.lisp +++ b/src/lsp/lifecycle/initialize.lisp @@ -1,15 +1,11 @@ (in-package :clef-lsp/lifecycle) -;;; State for tracking multiple ASDF systems in the workspace - -(defparameter *loaded-systems* (make-hash-table :test 'equal) - "Hash table mapping system names (strings) to system-info structs.") - -(defparameter *file-to-system* (make-hash-table :test 'equal) - "Hash table mapping absolute file paths to the system name they belong to.") - -(defparameter *asd-files* nil - "List of all discovered .asd file paths in the workspace.") +;;; Workspace ASDF system discovery and loading. +;;; +;;; The loaded-systems / file-to-system / asd-files tables formerly defined +;;; here as defparameters now live on CLEF-CONTEXT:*SERVER* so that all +;;; CLEF state resets atomically on shutdown. This file just reads and +;;; writes them through the CTX: aliases. ;; TODO: I need to move all of the asd/system loading into a thread for resiliency @@ -246,10 +242,11 @@ (defun compute-system-load-order () "Compute topological order for loading systems based on inter-project dependencies. Systems with no local dependencies are loaded first." - (let ((local-system-names (loop for name being the hash-keys of *loaded-systems* - collect name)) - (no-local-deps '()) - (with-local-deps '())) + (let* ((systems ctx:loaded-systems) + (local-system-names (loop for name being the hash-keys of systems + collect name)) + (no-local-deps '()) + (with-local-deps '())) ;; Partition systems by whether they have local dependencies (maphash (lambda (name sys-info) (let* ((deps (clef-symbols:system-info-dependencies sys-info)) @@ -262,7 +259,7 @@ Systems with no local dependencies are loaded first." (if (null local-deps) (push name no-local-deps) (push name with-local-deps)))) - *loaded-systems*) + systems) ;; Return systems without local deps first, then those with deps (append (nreverse no-local-deps) (nreverse with-local-deps)))) @@ -295,12 +292,13 @@ Systems with no local dependencies are loaded first." (slog :warn "Failed to load system ~A: ~A" system-name e))))) (defun build-file-to-system-mapping () - "Populate *file-to-system* from loaded system info." - (clrhash *file-to-system*) - (maphash (lambda (system-name sys-info) - (dolist (file-path (clef-symbols:system-info-source-files sys-info)) - (setf (gethash file-path *file-to-system*) system-name))) - *loaded-systems*)) + "Populate the file-to-system table on *SERVER* from loaded system info." + (let ((mapping ctx:file-to-system)) + (clrhash mapping) + (maphash (lambda (system-name sys-info) + (dolist (file-path (clef-symbols:system-info-source-files sys-info)) + (setf (gethash file-path mapping) system-name))) + ctx:loaded-systems))) (defun load-all-workspace-systems (root-uri) "Discover and load all ASDF systems in the workspace." @@ -308,12 +306,12 @@ Systems with no local dependencies are loaded first." (if (null asd-files) (slog :warn "No .asd files found in workspace: ~A" root-uri) (progn - (setf *asd-files* asd-files) + (setf ctx:asd-files asd-files) (slog :info "Discovered ~A .asd file(s) in workspace" (length asd-files)) ;; Clear previous state - (clrhash *loaded-systems*) - (clrhash *file-to-system*) + (clrhash ctx:loaded-systems) + (clrhash ctx:file-to-system) ;; Phase 1: Parse all .asd files to discover systems (dolist (asd-path asd-files) @@ -321,7 +319,7 @@ Systems with no local dependencies are loaded first." (let ((systems (parse-asd-file asd-path))) (dolist (sys systems) (slog :debug "Found system: ~A" (clef-symbols:system-info-name sys)) - (setf (gethash (clef-symbols:system-info-name sys) *loaded-systems*) sys)))) + (setf (gethash (clef-symbols:system-info-name sys) ctx:loaded-systems) sys)))) ;; Phase 2: Determine load order based on dependencies (let ((load-order (compute-system-load-order))) @@ -329,25 +327,25 @@ Systems with no local dependencies are loaded first." ;; Phase 3: Load each system in dependency order (dolist (system-name load-order) - (let ((sys-info (gethash system-name *loaded-systems*))) + (let ((sys-info (gethash system-name ctx:loaded-systems))) (when sys-info (load-system-with-info sys-info))))) ;; Phase 4: Build file-to-system mapping (build-file-to-system-mapping) (slog :info "Loaded ~A system(s), mapped ~A file(s)" - (hash-table-count *loaded-systems*) - (hash-table-count *file-to-system*)))))) + (hash-table-count ctx:loaded-systems) + (hash-table-count ctx:file-to-system)))))) ;;; Utility functions for querying system state (defun get-file-system (file-path) "Get the system name that a file belongs to, or nil if unknown." - (gethash (namestring file-path) *file-to-system*)) + (gethash (namestring file-path) ctx:file-to-system)) (defun list-workspace-systems () "Return a list of all discovered system names." - (loop for name being the hash-keys of *loaded-systems* + (loop for name being the hash-keys of ctx:loaded-systems collect name)) (defun handle-initialize (request) @@ -359,7 +357,7 @@ Systems with no local dependencies are loaded first." ;; We currently assume one does exist and it's the first value (let ((workspace-root (href (aref (href params-hash "workspace-folders") 0) "uri"))) (slog :info "Client workspace root: ~A" workspace-root) - (setf clef-lsp/server:*workspace-root* workspace-root) + (setf ctx:workspace-root workspace-root) ;; Load all .asd files (root + test directories) (load-all-workspace-systems workspace-root) (let ((start-time (get-internal-real-time))) @@ -374,8 +372,7 @@ Systems with no local dependencies are loaded first." ;; behavior, and notify the client. (slog :error "Failed to get client workspace root: ~A" e))) - - (setf clef-lsp/server:*client-capabilities* capabilities) + (setf ctx:client-capabilities capabilities) ;; TODO: use *server-capabilities* ;; https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeResult diff --git a/src/lsp/lifecycle/initialized.lisp b/src/lsp/lifecycle/initialized.lisp index ef57a36..a3e67e8 100644 --- a/src/lsp/lifecycle/initialized.lisp +++ b/src/lsp/lifecycle/initialized.lisp @@ -2,6 +2,6 @@ (defun handle-initialized (request) (declare (ignore request)) - (setf clef-lsp/server:*initialized* t) + (setf ctx:initialized t) ;; Send no response nil) diff --git a/src/lsp/server.lisp b/src/lsp/server.lisp index 45b0cfc..86bc5b7 100644 --- a/src/lsp/server.lisp +++ b/src/lsp/server.lisp @@ -1,22 +1,11 @@ (in-package :clef-lsp/server) -(defparameter *initialized* nil - "Whether the client has responded with initialized. The server only responds -with ServerNotInitialized = -32002 before this occurs.") - -(defparameter *handlers* (make-hash-table :test 'equal) - "A hash table mapping LSP endpoint names to their handler functions.") - -(defparameter *client-capabilities* nil - "Stores a hash table of the client's capabilities as reported during initialization. -Seehttps://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities ") - -(defvar *documents* (make-hash-table :test 'equal) - "Document objects kept in memory and maintained by textDocument/didOpen, didChange, didClose requests. -Top level keys are file paths/URIs, values are the full text of the documents as strings. Newline chars. are preserved with -(currently) no consideration of cross-platform differences.") - -(defvar *workspace-root* nil "The path to the root of the project workspace. Set in the initialize handler.") +;;; LSP server loop and handler dispatch. +;;; +;;; All persistent state lives on the CLEF-CONTEXT:SERVER-CONTEXT struct held +;;; in CLEF-CONTEXT:*SERVER*. This file used to own several defparameters +;;; (*initialized*, *documents*, *workspace-root*, ...) that have been moved +;;; there; see src/context.lisp for the canonical definitions. (defun before-handle-request (request) "Hook to run before handling any request." @@ -24,7 +13,7 @@ Top level keys are file paths/URIs, values are the full text of the documents as ;; Error if server not initialized, unless these are requests to the endpoints that handle initialization (when (and (not (string= endpoint-name "initialize")) (not (string= endpoint-name "initialized")) - (not *initialized*)) + (not ctx:initialized)) (slog :error "Server not initialized yet.") (error 'clef-lsp/types/base:server-not-initialized-error)))) @@ -41,7 +30,7 @@ Top level keys are file paths/URIs, values are the full text of the documents as (declare (ignore e)) (setf captured-backtrace (capture-backtrace))))) (let* ((endpoint-name (clef-jsonrpc/types:request-method request))) - (let ((handler (gethash endpoint-name *handlers*))) + (let ((handler (gethash endpoint-name ctx:handlers))) (if handler (let ((message (funcall handler request))) (if (null message) @@ -71,6 +60,7 @@ Top level keys are file paths/URIs, values are the full text of the documents as (defun run-lsp-server-stdio (&key (input *standard-input*) (output *standard-output*)) "Run LSP server over stdio" + (setf ctx:output-stream output) (loop (let ((request (clef-jsonrpc/messages:read-lsp-message input))) (when request @@ -80,21 +70,32 @@ Top level keys are file paths/URIs, values are the full text of the documents as (when response (clef-jsonrpc/messages:write-lsp-message response output))))))) +(defun send-notification (method params) + "Send an LSP notification (a message with no id that doesn't expect a response)." + (let ((stream ctx:output-stream)) + (when stream + (let ((notification (serapeum:dict + "jsonrpc" "2.0" + "method" method + "params" params))) + (clef-jsonrpc/messages:write-lsp-message notification stream))))) + +(defun publish-diagnostics (uri diagnostics) + "Publish diagnostics for a document using textDocument/publishDiagnostics notification." + (send-notification "textDocument/publishDiagnostics" + (serapeum:dict "uri" uri + "diagnostics" (or diagnostics #())))) + (defun sethandler (endpoint-name handler-lambda) "Defines an LSP handler for the given endpoint name." (slog :debug "Defining LSP handler for endpoint: ~A" endpoint-name) - (setf (gethash endpoint-name *handlers*) + (setf (gethash endpoint-name ctx:handlers) (lambda (request) - ;; From src/lsp/server.lisp (before-handle-request request) - ;; (slog :debug "[~A] →" endpoint-name) (funcall handler-lambda request)))) -;; TODO: It'd be cool to make a macro for registering handlers and not requiring exporting them + doing -;; setup there... but probably no real point. (defun register-handlers () - "Registers all LSP handlers from *handlers*" - ;; For now, just a bunch of manual sethandler calls. Need to reconsider this later + "Registers all LSP handlers on the current context." (sethandler "initialize" 'clef-lsp/lifecycle:handle-initialize) (sethandler "initialized" 'clef-lsp/lifecycle:handle-initialized) (sethandler "textDocument/completion" 'clef-lsp/document:handle-text-document-completion) @@ -106,16 +107,17 @@ Top level keys are file paths/URIs, values are the full text of the documents as (sethandler "textDocument/formatting" 'clef-lsp/document:handle-text-document-formatting) (sethandler "textDocument/diagnostic" 'clef-lsp/document:handle-text-document-diagnostic) (sethandler "textDocument/hover" 'clef-lsp/document:handle-text-document-hover) + (sethandler "textDocument/documentHighlight" 'clef-lsp/document:handle-text-document-highlight) + (sethandler "textDocument/signatureHelp" 'clef-lsp/document:handle-text-document-signature-help) (sethandler "workspace/diagnostic" 'clef-lsp/workspace:handle-workspace-diagnostic) (sethandler "workspace/didChangeConfiguration" 'clef-lsp/workspace:handle-workspace-did-change-configuration) + (sethandler "workspace/symbol" 'clef-lsp/workspace:handle-workspace-symbol) (sethandler "shutdown" 'clef-lsp/misc:handle-shutdown) (sethandler "exit" 'clef-lsp/misc:handle-exit)) (defun reset () - "Resets all server state, to be called when the server is asked to shutdown or exit" - (setf *initialized* nil) - (setf *client-capabilities* nil) - (setf *documents* (make-hash-table :test 'equal)) + "Discard all server state by installing a fresh context." + (ctx:reset-context) (slog :info "CLEF LSP server state has been reset.")) (defun start (&key (input *standard-input*) (output *standard-output*) (log-mode :file)) @@ -124,8 +126,6 @@ Top level keys are file paths/URIs, values are the full text of the documents as ;; Controls verbosity and whether to output logs to console or a file (clef-log:init log-mode) - ;; TODO: Spawn the server in a new thread, watch for crashes, and restart if that occurs. - ;; Also listen for & handle LSP messages to shut down / restart the server (slog :debug "Starting CLEF LSP server...") (slog :debug "Registering handlers...") (register-handlers) diff --git a/src/lsp/workspace/symbol.lisp b/src/lsp/workspace/symbol.lisp index 4fb295d..bcc8b86 100644 --- a/src/lsp/workspace/symbol.lisp +++ b/src/lsp/workspace/symbol.lisp @@ -44,7 +44,7 @@ Returns symbols matching the query from across the workspace." (search query-upcase (string-upcase symbol-name))) (dolist (def defs) (push (symbol-def-to-symbol-info def symbol-name) results)))) - clef-symbols:*workspace-symbol-index*) + ctx:workspace-symbol-index) (slog :debug "[workspace/symbol] Found ~A matching symbols" (length results)) @@ -86,7 +86,3 @@ Returns symbols matching the query from across the workspace." "character" (clef-parser/parser:node-start-point-column node)) "end" (dict "line" (clef-parser/parser:node-end-point-row node) "character" (clef-parser/parser:node-end-point-column node))))) - -;; Register the handler -(setf (gethash "workspace/symbol" clef-lsp/server:*handlers*) - #'handle-workspace-symbol) diff --git a/src/packages.lisp b/src/packages.lisp index 1249b3e..c32151d 100644 --- a/src/packages.lisp +++ b/src/packages.lisp @@ -14,6 +14,51 @@ *log-file-path* init)) +(defpackage :clef-context + (:use :cl) + (:documentation "Central server context. Holds all persistent CLEF LSP +state on a single SERVER-CONTEXT struct bound to *SERVER*. All other CLEF +packages reach shared state through the accessors exported here rather than +through their own defparameters.") + (:export :server-context + :make-server-context + :server-context-p + :*server* + :reset-context + ;; Struct field accessors (the generated ones, for when the + ;; symbol-macro aliases can't be used) + :server-context-initialized + :server-context-shutdown-received + :server-context-client-capabilities + :server-context-workspace-root + :server-context-output-stream + :server-context-handlers + :server-context-documents + :server-context-lexical-scopes + :server-context-symbol-refs + :server-context-workspace-symbol-index + :server-context-document-line-offsets + :server-context-global-scope + :server-context-loaded-systems + :server-context-file-to-system + :server-context-asd-files + ;; Symbol-macro aliases (short form, preferred at call sites) + :initialized + :shutdown-received + :client-capabilities + :workspace-root + :output-stream + :handlers + :documents + :lexical-scopes + :symbol-refs + :workspace-symbol-index + :document-line-offsets + :global-scope + :loaded-systems + :file-to-system + :asd-files)) + (defpackage :clef-root (:use :cl :clef-log) (:export :start-server)) @@ -66,12 +111,11 @@ (defpackage :clef-symbols (:use :cl :clef-log :clef-parser/parser) (:local-nicknames + (:ctx :clef-context) (:ts :cl-tree-sitter/high-level) (:ts-ll :cl-tree-sitter/low-level)) (:export build-project-symbol-map build-file-symbol-map - *lexical-scopes-by-file* - *symbol-refs-by-file* get-ref-for-doc-pos lexical-scope-kind lexical-scope-symbol-definitions @@ -96,31 +140,28 @@ system-info-dependencies system-info-source-files system-info-loaded-p - ;; workspace symbol index for cross-file go-to-definition - *workspace-symbol-index* + ;; Workspace symbol index management (operates on context) clear-workspace-symbol-index remove-file-from-workspace-index add-to-workspace-index - lookup-in-workspace-index)) + lookup-in-workspace-index + ;; Byte offset helpers (used by some handlers) + line-char-to-byte-offset)) (defpackage :clef-lsp/server (:use :cl :clef-log) + (:local-nicknames + (:ctx :clef-context)) (:import-from :serapeum :dict) (:export :start - *handlers* - *initialized* - *documents* - *client-capabilities* - *server-capabilities-json* - *workspace-root* + :sethandler + :register-handlers :before-handle-request - *server* - :reset)) - -;; (defpackage :clef-lsp/defhandler -;; (:use :cl :clef-log) -;; (:import-from :clef-lsp/server *handlers* :before-handle-request) -;; (:export :defhandler)) + :handle-lsp-request + :send-notification + :publish-diagnostics + :reset + *server-capabilities-json*)) (defpackage :clef-lsp/types/base (:use :cl :clef-log) @@ -169,17 +210,10 @@ :position-line :position-character)) -;; (defpackage :clef-lsp/types/lifecycle -;; (:use :cl :clef-lsp/types/base :schemata) -;; (:export :initialize-params -;; :initialize-params-process-id -;; :initialize-params-root-uri -;; :initialize-params-capabilities -;; :workspace-folder -;; :client-capabilities)) - (defpackage :clef-lsp/lifecycle (:use :cl :clef-log) + (:local-nicknames + (:ctx :clef-context)) (:import-from :serapeum :dict :href) (:export handle-initialize handle-initialized @@ -187,19 +221,20 @@ load-workspace-asd load-asd ;; Multi-ASD support - *loaded-systems* - *file-to-system* - *asd-files* discover-asd-files load-all-workspace-systems get-file-system - list-workspace-systems)) + list-workspace-systems + parse-asd-file + load-system-with-info + build-file-to-system-mapping)) (defpackage :clef-lsp/document (:use :cl :clef-log :clef-symbols) - (:import-from :serapeum :dict :href) (:local-nicknames + (:ctx :clef-context) (:ts :cl-tree-sitter/high-level)) + (:import-from :serapeum :dict :href) (:export handle-text-document-completion handle-text-document-definition @@ -215,6 +250,8 @@ (defpackage :clef-lsp/workspace (:use :cl :clef-log) + (:local-nicknames + (:ctx :clef-context)) (:import-from :serapeum :dict :href) (:export handle-workspace-diagnostic handle-workspace-did-change-configuration @@ -222,6 +259,8 @@ (defpackage :clef-lsp/misc (:use :cl :clef-log) + (:local-nicknames + (:ctx :clef-context)) (:import-from :serapeum :dict) (:export handle-shutdown handle-exit)) diff --git a/src/symbols/init.lisp b/src/symbols/init.lisp index 997b00b..af1c1dd 100644 --- a/src/symbols/init.lisp +++ b/src/symbols/init.lisp @@ -1,57 +1,49 @@ (in-package :clef-symbols) -(defparameter *lexical-scopes-by-file* (make-hash-table) - "A hash-table mapping file paths to interval trees of lexical-scope's") - -(defparameter *symbol-refs-by-file* (make-hash-table) - "A hash-table mapping file paths to interval trees of symbol-reference's") +;;; Symbol analysis. +;;; +;;; Persistent state (scope trees, symbol-ref trees, the workspace-wide symbol +;;; index, per-file line offset caches, the global scope) all live on +;;; CLEF-CONTEXT:*SERVER*. Transient state used while walking a parse tree -- +;;; the current scope and current package -- remain as dynamic specials in +;;; this package; they are only meaningful inside a single +;;; BUILD-FILE-SYMBOL-MAP call and don't belong in the shared context. (defparameter *current-scope* nil "The current lexical scope that is the context in which the current processed node is occurring") -;; TODO: This should be universal to the LSP and not specific to this package -(defparameter *document-line-lengths* (make-hash-table) - "A hash-table mapping file paths to vectors of line lengths for that document.") -(defparameter *document-line-offsets* (make-hash-table) - "A hash-table mapping file paths to vectors of line offsets (starting byte offset) for that document.") - (defparameter *current-package* nil "The name of the current package encountered when processing the file") -(defparameter *global-scope* nil - "The global lexical scope for the entire workspace. Should the root of every scope tree") - -(defparameter *workspace-symbol-index* (make-hash-table :test 'equal) - "Hash table mapping symbol names (strings) to lists of symbol-definition's across all files. -Used for cross-file go-to-definition.") - ;;; Workspace symbol index management (defun clear-workspace-symbol-index () "Clear the entire workspace symbol index." - (clrhash *workspace-symbol-index*)) + (clrhash ctx:workspace-symbol-index)) (defun remove-file-from-workspace-index (file-path) "Remove all symbol definitions from FILE-PATH from the workspace index." - (maphash (lambda (symbol-name defs) - (let ((filtered (remove-if (lambda (def) - (let ((loc (symbol-definition-location def))) - (and loc (string= (location-file-path loc) file-path)))) - defs))) - (if filtered - (setf (gethash symbol-name *workspace-symbol-index*) filtered) - (remhash symbol-name *workspace-symbol-index*)))) - *workspace-symbol-index*)) + (let ((index ctx:workspace-symbol-index)) + (maphash (lambda (symbol-name defs) + (let ((filtered (remove-if (lambda (def) + (let ((loc (symbol-definition-location def))) + (and loc (string= (location-file-path loc) file-path)))) + defs))) + (if filtered + (setf (gethash symbol-name index) filtered) + (remhash symbol-name index)))) + index))) (defun add-to-workspace-index (symbol-def) "Add a symbol definition to the workspace index for cross-file lookup." (let* ((name (symbol-definition-symbol-name symbol-def)) - (existing (gethash name *workspace-symbol-index*))) - (setf (gethash name *workspace-symbol-index*) + (index ctx:workspace-symbol-index) + (existing (gethash name index))) + (setf (gethash name index) (cons symbol-def existing)))) (defun lookup-in-workspace-index (symbol-name) "Look up a symbol by name in the workspace index. Returns a list of matching definitions." - (gethash symbol-name *workspace-symbol-index*)) + (gethash symbol-name ctx:workspace-symbol-index)) ;; No longer needed since we set these on the global scope directly ;; (defparameter *built-in-symbol-defs* nil @@ -66,9 +58,9 @@ Note that symbol-ref can be nil if none is at the location" ;; (slog :debug ">>>>>>>>: ~A ~A ~A" file-path line char) (let* ((path (clef-util:cleanup-path file-path)) (offset (line-char-to-byte-offset path line char)) - (symbol-refs (interval:find-all (gethash path *symbol-refs-by-file*) offset)) + (symbol-refs (interval:find-all (gethash path ctx:symbol-refs) offset)) ;; Also get the lexical scope by position, as symbol-refs may be nil - (scopes (interval:find-all (gethash path *lexical-scopes-by-file*) offset))) + (scopes (interval:find-all (gethash path ctx:lexical-scopes) offset))) ;; (slog :debug "Found symbol-defs at line ~A char ~A (offset ~A): ~A" line char offset symbol-defs) ;; (slog :debug ">>>> scope intervals found: ~A" scopes) ;; (values nil nil))) @@ -88,7 +80,7 @@ Note that symbol-ref can be nil if none is at the location" ;; the nodes the high-level API creates (defun line-char-to-byte-offset (file-path line char) "Converts a line and character position to a byte offset." - (let* ((line-offsets (gethash file-path *document-line-offsets*)) + (let* ((line-offsets (gethash file-path ctx:document-line-offsets)) (line-index line) ;; Convert to 0-based (char-index char)) ;; Already 0-based ;; Add the char offset to the pre-calculated line offset @@ -102,7 +94,7 @@ Note that symbol-ref can be nil if none is at the location" ;; Long-term we need to either utility-ize this kind of text seeking, or find a different way to use tree-sitter that actually exposes ;; the byte offsets directly. (defun fast-node-text (node source file-path) - (let* ((line-offsets (gethash file-path *document-line-offsets*)) + (let* ((line-offsets (gethash file-path ctx:document-line-offsets)) (start-row (node-start-point-row node)) (start-col (node-start-point-column node)) (end-row (node-end-point-row node)) @@ -168,18 +160,18 @@ Note that symbol-ref can be nil if none is at the location" (clear-workspace-symbol-index) ;; Init the global scope and load in builtins + externals - (setf *global-scope* (make-lexical-scope - :kind :workspace - :location nil - :parent-scope nil - :symbol-definitions '() - ;; Should never actually receive values - :symbol-references (make-hash-table) - :child-scopes '() - :node nil)) - - (load-common-lisp-builtin-symbols *global-scope*) - (load-asd-external-packages *global-scope*) + (setf ctx:global-scope (make-lexical-scope + :kind :workspace + :location nil + :parent-scope nil + :symbol-definitions '() + ;; Should never actually receive values + :symbol-references (make-hash-table) + :child-scopes '() + :node nil)) + + (load-common-lisp-builtin-symbols ctx:global-scope) + (load-asd-external-packages ctx:global-scope) (slog :debug "Building symbol map at ~A" project-root) ;; Discover every .lisp file recursively under the root @@ -205,13 +197,13 @@ Note that symbol-ref can be nil if none is at the location" (setf *current-package* nil) ;; Calculate and store line lengths for this document - (setf (gethash file-path *document-line-offsets*) + (setf (gethash file-path ctx:document-line-offsets) (calculate-line-offsets file-source)) ;; Init the interval trees - (setf (gethash file-path *symbol-refs-by-file*) + (setf (gethash file-path ctx:symbol-refs) (interval:make-tree)) - (setf (gethash file-path *lexical-scopes-by-file*) + (setf (gethash file-path ctx:lexical-scopes) (interval:make-tree)) ;; Parse the file with tree-sitter and then walk the output tree to find @@ -226,14 +218,14 @@ Note that symbol-ref can be nil if none is at the location" :file-path file-path :start 0 :end (length file-source)) - :parent-scope *global-scope* + :parent-scope ctx:global-scope :symbol-definitions '() :symbol-references (make-hash-table) :child-scopes '() :node parse-tree)) ;; Append as a child-scope of the global scope - (push *current-scope* (lexical-scope-child-scopes *global-scope*)) + (push *current-scope* (lexical-scope-child-scopes ctx:global-scope)) ;; Store the document scope on the interval tree so it can be found by find-all (store-scope-on-interval-tree *current-scope* file-path) (labels ((walk (n) @@ -347,7 +339,8 @@ those packages' members into the symbol map" "Retrieves a combined list of library names from all loaded systems. Aggregates :depends-on from all discovered .asd files and filters out local system names." (let ((all-deps '()) - (local-system-names '())) + (local-system-names '()) + (systems ctx:loaded-systems)) ;; Collect all local system names and their dependencies (maphash (lambda (name sys-info) (push name local-system-names) @@ -357,12 +350,12 @@ Aggregates :depends-on from all discovered .asd files and filters out local syst (let ((dep-str (string-downcase (if (stringp dep) dep (symbol-name dep))))) (pushnew dep-str all-deps :test #'string-equal))))) - clef-lsp/lifecycle::*loaded-systems*) + systems) ;; Filter out local system names (don't try to load our own systems as external) (let ((external-deps (set-difference all-deps local-system-names :test #'string-equal))) (slog :debug "[symbol init] Found ~A external dependencies from ~A system(s)" (length external-deps) - (hash-table-count clef-lsp/lifecycle::*loaded-systems*)) + (hash-table-count systems)) ;; Convert back to symbols for compatibility with existing code (mapcar (lambda (s) (intern (string-upcase s) :keyword)) external-deps)))) @@ -473,7 +466,7 @@ symbol-definitions. Returns the created lexical-scope if applicable, nil otherwi (defun store-scope-on-interval-tree (scope file-path) "Stores the given lexical SCOPE into the interval tree for FILE-PATH." - (let ((scopes-tree (gethash file-path *lexical-scopes-by-file*)) + (let ((scopes-tree (gethash file-path ctx:lexical-scopes)) (new-interval (make-clef-interval :start (location-start (lexical-scope-location scope)) :end (location-end (lexical-scope-location scope))))) @@ -594,7 +587,7 @@ interval tree if so." :location (location-for-node file-path node) :usage-scope *current-scope* :node node))) - (let ((refs-tree (gethash file-path *symbol-refs-by-file*)) + (let ((refs-tree (gethash file-path ctx:symbol-refs)) (new-interval (make-clef-interval :start (location-start (symbol-reference-location symbol-reference)) :end (location-end (symbol-reference-location symbol-reference))))) diff --git a/test/document-tests.lisp b/test/document-tests.lisp index 62c7b3a..436cae4 100644 --- a/test/document-tests.lisp +++ b/test/document-tests.lisp @@ -49,7 +49,7 @@ "text" *simple-lisp-code*)) :id nil) (assert-equal *simple-lisp-code* - (gethash "file:///tmp/test.lisp" clef-lsp/server:*documents*) + (gethash "file:///tmp/test.lisp" clef-context:documents) "Document text should be stored"))) (deftest test-did-open-multiple-documents @@ -69,9 +69,9 @@ "text" "(defun b () 2)")) :id nil) (assert-equal "(defun a () 1)" - (gethash "file:///tmp/a.lisp" clef-lsp/server:*documents*)) + (gethash "file:///tmp/a.lisp" clef-context:documents)) (assert-equal "(defun b () 2)" - (gethash "file:///tmp/b.lisp" clef-lsp/server:*documents*)))) + (gethash "file:///tmp/b.lisp" clef-context:documents)))) ;;; textDocument/didChange tests @@ -93,7 +93,7 @@ "contentChanges" (vector (dict "text" "(defun new () t)"))) :id nil) (assert-equal "(defun new () t)" - (gethash "file:///tmp/test.lisp" clef-lsp/server:*documents*) + (gethash "file:///tmp/test.lisp" clef-context:documents) "Document should be updated"))) ;;; textDocument/hover tests diff --git a/test/lifecycle-tests.lisp b/test/lifecycle-tests.lisp index e73bb55..e5253fa 100644 --- a/test/lifecycle-tests.lisp +++ b/test/lifecycle-tests.lisp @@ -24,7 +24,7 @@ (with-direct-handler-test (call-handler "initialize" (make-minimal-initialize-params)) (assert-equal "file:///tmp/test-workspace" - clef-lsp/server:*workspace-root* + clef-context:workspace-root "Workspace root should be set"))) (deftest test-initialize-stores-client-capabilities @@ -36,7 +36,7 @@ "workspaceFolders" (vector (dict "uri" "file:///tmp/test" "name" "test"))))) (call-handler "initialize" params) - (assert-not-nil clef-lsp/server:*client-capabilities* + (assert-not-nil clef-context:client-capabilities "Client capabilities should be stored")))) (deftest test-initialized-sets-flag @@ -46,7 +46,7 @@ (call-handler "initialize" (make-minimal-initialize-params)) ;; Then send initialized notification (call-handler "initialized" (dict) :id nil) - (assert-true clef-lsp/server:*initialized* + (assert-true clef-context:initialized "Server should be marked as initialized"))) (deftest test-server-not-initialized-error @@ -68,11 +68,11 @@ (call-handler "initialize" (make-minimal-initialize-params)) (call-handler "initialized" (dict) :id nil) ;; Verify initialized - (assert-true clef-lsp/server:*initialized*) + (assert-true clef-context:initialized) ;; Shutdown (call-handler "shutdown" (dict)) ;; State should be reset - (assert-nil clef-lsp/server:*initialized* + (assert-nil clef-context:initialized "Server should not be initialized after shutdown"))) (deftest test-capabilities-include-expected-providers