Commita09184e2Recorded18 Apr 2026Repositorysigil-lsp

sigil-lsp: skip comments and strings when locating the first (import ...)

Message

The previous (string-find text "(import ") would happily match an import that lived inside a ;;; docstring at the top of a file — socket.sgl has an example (import (sigil socket) (sigil io)) in its file-level comment block, so the quick-fix was rewriting the example instead of the real define-library clause.

Replaced the stdlib search with a code-aware scan that steps past ; line comments and "..." string literals before looking for the (import literal. matching-close already skipped them while paren-balancing inside the form; this extends the same handling to the lead-in search.

Manually verified on socket.sgl: the action's target is line 37 (the real (import (sigil string)) clause), not the example buried in the commentary.

Changed
 src/sigil/lsp/code-actions.sgl | 31 +++++++++++++++++++++++++++++--
 1 file changed, 29 insertions(+), 2 deletions(-)
Diff
src/sigil/lsp/code-actions.sglmodified
@@ -99,7 +99,8 @@
99
`((range . ,(range (position 0 0) (position 0 0)))
100
(newText . ,(string-append "(import " mod ")\n"))))))
101
102
;; Scan TEXT for the first `(import ` form. On success returns
+102
;; Scan TEXT for the first `(import ` form at code level (skipping
+103
;; `;` comments and "..." strings). On success returns
104
;; (list CLOSE-OFFSET INDENT-COL) where CLOSE-OFFSET is the offset
105
;; of the final ')' of the (import ...) form, and INDENT-COL is the
106
;; column the next module spec should align to. Returns #f if no
@@ -107,12 +108,38 @@
108
(define (find-first-import-end text)
109
(let* ((marker "(import ")
110
(mlen (string-length marker))
110
(start (string-find text marker)))
+111
(start (find-code-substring text marker)))
112
(and start
113
(let ((close (matching-close text start)))
114
(and close
115
(list close (column-at text (+ start mlen))))))))
116
+117
;; Walk TEXT looking for NEEDLE at a position that isn't inside
+118
;; a `;...\n` line comment or a "..." string literal. Returns the
+119
;; offset of the first such match, or #f.
+120
(define (find-code-substring text needle)
+121
(let ((tlen (string-length text))
+122
(nlen (string-length needle)))
+123
(let loop ((i 0))
+124
(cond
+125
((> (+ i nlen) tlen) #f)
+126
(else
+127
(let ((c (string-ref text i)))
+128
(cond
+129
((char=? c #\;) (loop (skip-line text (+ i 1))))
+130
((char=? c #\") (loop (skip-string text (+ i 1))))
+131
((substring-matches? text i needle nlen) i)
+132
(else (loop (+ i 1))))))))))
+133
+134
(define (substring-matches? text i needle nlen)
+135
(let loop ((k 0))
+136
(cond
+137
((>= k nlen) #t)
+138
((not (char=? (string-ref text (+ i k))
+139
(string-ref needle k)))
+140
#f)
+141
(else (loop (+ k 1))))))
+142
143
;; Given an offset to a '(' in TEXT, return the offset of its
144
;; matching ')', or #f. Skips over nested parens, strings, and
145
;; line comments.