sigil-lsp: offer import quick-fixes on cursor, not just on diagnostics
Previously code actions only fired when a diagnostic with a message starting with "Unbound"/"Undefined" was present at the cursor, and sigil-lsp doesn't emit those diagnostics today. That meant M-x eglot-code-actions on a bare call-with-input-file returned nothing.
compute-code-actions now also runs a cursor-based pass: it reads the symbol under the request's range.start, searches the docs index for modules that export that exact name, filters out any module already imported in the buffer (either via a top-level (import ...) form or a (define-library ... (import ...)) clause), and emits one "Import X from M" quick-fix per remaining candidate.
Verified on socket.sgl: adding a new function that calls call-with-input-file surfaces a single action importing (sigil io).
src/sigil/lsp/code-actions.sgl | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 95 insertions(+), 8 deletions(-)src/sigil/lsp/code-actions.sglmodified
(define-library (sigil lsp code-actions) (import (sigil core) (sigil string) (sigil io) (sigil dict) (sigil docs search) (sigil lsp positions) (define (compute-code-actions store uri range-dict context-diagnostics) (let* ((doc (doc-store-get store uri)) (text (and doc (document-text doc))) (diags (or context-diagnostics '())) (actions '())) (when text (for-each (lambda (d) (let ((as (diagnostic-actions uri text d))) (set! actions (append actions as)))) (as-list diags))) (diags (or context-diagnostics '()))) (if (not text) '() (append (all-diagnostic-actions uri text diags) (cursor-actions uri text range-dict))))) (define (all-diagnostic-actions uri text diags) (let ((actions '())) (for-each (lambda (d) (let ((as (diagnostic-actions uri text d))) (set! actions (append actions as)))) (as-list diags)) actions)) ;; Cursor-position actions: even when there's no diagnostic, if the ;; symbol under point is exported by a module that isn't yet ;; imported in this file, offer one "Import X from M" action per ;; candidate module. (define (cursor-actions uri text range-dict) (let* ((start (and range-dict (dict-ref-any range-dict 'start))) (line0 (and start (dict-ref-any start 'line))) (char0 (and start (dict-ref-any start 'character)))) (if (or (not (integer? line0)) (not (integer? char0))) '() (let ((name (word-at-position text line0 char0))) (if (or (not name) (string=? name "")) '() (import-suggestions-for-symbol uri text name)))))) (define (import-suggestions-for-symbol uri text name) (let* ((results (guard (e (else '())) (search-docs name))) (exact (filter (lambda (r) (and (string? (search-result-name r)) (string=? (search-result-name r) name))) results)) (imported (existing-imports text))) (filter-map (lambda (r) (let ((mod (search-result-module r))) (if (or (not mod) (member mod imported)) #f (make-import-action-for uri text r name)))) (limit 8 exact)))) (define (make-import-action-for uri text result name) (let* ((mod (search-result-module result)) (import-line (string-append "(import " mod ")\n")) (insert-at (insert-import-position text))) `((title . ,(format "Import ~a from ~a" name mod)) (kind . ,kind-quickfix) (edit . ,(workspace-edit uri (list `((range . ,(range (position insert-at 0) (position insert-at 0))) (newText . ,import-line))))) (isPreferred . #t)))) ;; Collect module-name strings from `(import ...)` clauses in TEXT. ;; Matches both top-level `(import ...)` forms and imports inside a ;; `(define-library ...)` form. (define (existing-imports text) (guard (e (else '())) (let ((port (open-input-string text))) (let loop ((acc '())) (let ((form (read port))) (cond ((eof-object? form) acc) ((and (pair? form) (eq? (car form) 'import)) (loop (append (import-modules (cdr form)) acc))) ((and (pair? form) (eq? (car form) 'define-library)) (loop (append (library-import-modules (cddr form)) acc))) (else (loop acc)))))))) (define (library-import-modules clauses) (cond ((null? clauses) '()) ((and (pair? (car clauses)) (eq? (caar clauses) 'import)) (append (import-modules (cdar clauses)) (library-import-modules (cdr clauses)))) (else (library-import-modules (cdr clauses))))) ;; Convert a list of import-set exprs to their module-name strings ;; (matching what `search-result-module` returns). (define (import-modules specs) (map (lambda (spec) (format "~a" (unwrap-import-set spec))) specs)) (define (unwrap-import-set spec) (cond ((and (pair? spec) (memq (car spec) '(only except prefix rename))) (unwrap-import-set (cadr spec))) (else spec))) (define (as-list v) (cond ((null? v) '())