|
272 | 272 | (mapv (fn [[row col]] (row-col->offset source-str row col)) |
273 | 273 | positions)) |
274 | 274 |
|
275 | | -;; Functions for working with docstrings |
276 | | - |
277 | | -(defn find-docstring |
278 | | - "Finds the docstring node of a top-level form. |
279 | | - |
280 | | - Arguments: |
281 | | - - zloc: The zipper location to start searching from |
282 | | - - tag: The form type (e.g., 'defn, 'def) |
283 | | - - name: The name of the form |
284 | | - |
285 | | - Returns a map with: |
286 | | - - :zloc - the zipper positioned at the docstring node (or nil if not found) |
287 | | - - :similar-matches - a vector of potential namespace-qualified matches from find-top-level-form" |
288 | | - [zloc tag name] |
289 | | - (let [find-result (find-top-level-form zloc tag name) |
290 | | - form-zloc (:zloc find-result)] |
291 | | - (if-not form-zloc |
292 | | - find-result ;; Return the result with nil :zloc and any similar-matches |
293 | | - (let [tag-zloc (z/down form-zloc) ;; Move to the tag (defn, def, etc.) |
294 | | - name-zloc (z/right tag-zloc) ;; Move to the name |
295 | | - docstring-candidate (z/right name-zloc) ;; Move to potential docstring |
296 | | - docstring-zloc (when (and docstring-candidate |
297 | | - ;; Check for both :token (single-line) and :multi-line (multi-line) tags |
298 | | - (contains? #{:token :multi-line} (z/tag docstring-candidate)) |
299 | | - (string? (z/sexpr docstring-candidate))) |
300 | | - docstring-candidate)] |
301 | | - {:zloc docstring-zloc |
302 | | - :similar-matches (:similar-matches find-result)})))) |
303 | | - |
304 | | -(defn edit-docstring |
305 | | - "Edit a docstring in a top-level form. |
306 | | - |
307 | | - Arguments: |
308 | | - - zloc: The zipper location to start searching from |
309 | | - - tag: The form type (e.g., 'defn, 'def) |
310 | | - - name: The name of the form |
311 | | - - new-docstring: The new docstring content |
312 | | - |
313 | | - Returns a map with: |
314 | | - - :zloc - the updated zipper (or nil if form/docstring not found) |
315 | | - - :similar-matches - a vector of potential namespace-qualified matches" |
316 | | - [zloc tag name new-docstring] |
317 | | - (let [docstring-result (find-docstring zloc tag name) |
318 | | - docstring-zloc (:zloc docstring-result)] |
319 | | - (if docstring-zloc |
320 | | - ;; Found the docstring, update it |
321 | | - {:zloc (z/replace docstring-zloc (n/string-node new-docstring)) |
322 | | - :similar-matches (:similar-matches docstring-result)} |
323 | | - ;; Couldn't find docstring |
324 | | - docstring-result))) |
325 | | - |
326 | | -;; Functions for working with comments |
327 | | - |
328 | | -(defn is-comment-form? |
329 | | - "Check if a zloc is a (comment ...) form. |
330 | | - |
331 | | - Arguments: |
332 | | - - zloc: The zipper location to check |
333 | | - |
334 | | - Returns true if the form is a comment form." |
335 | | - [zloc] |
336 | | - (try |
337 | | - (and (z/seq? zloc) |
338 | | - (let [first-child (z/down zloc)] |
339 | | - (and first-child |
340 | | - (= (z/sexpr first-child) 'comment)))) |
341 | | - (catch Exception _ false))) |
342 | | - |
343 | | -(defn is-line-comment? |
344 | | - "Check if a zloc is a line comment. |
345 | | - |
346 | | - Arguments: |
347 | | - - zloc: The zipper location to check |
348 | | - |
349 | | - Returns true if the node is a line comment." |
350 | | - [zloc] |
351 | | - (try |
352 | | - (= (-> zloc z/node n/tag) :comment) |
353 | | - (catch Exception _ false))) |
354 | | - |
355 | | -(defn find-comment-block |
356 | | - "Find a comment block (either a 'comment' form or consecutive comment lines) |
357 | | - that contains a specific substring. |
358 | | - |
359 | | - This version properly handles comment blocks at the end of the file. |
360 | | - |
361 | | - Arguments: |
362 | | - - source: The source string to search in |
363 | | - - comment-substring: The substring to look for |
364 | | - |
365 | | - Returns a map with :type, :start, :end, and :content keys, |
366 | | - or nil if no matching comment block is found." |
367 | | - [source comment-substring] |
368 | | - (let [zloc (z/of-string source {:track-position? true}) |
369 | | - lines (str/split-lines source)] |
370 | | - |
371 | | - ;; First, try to find a comment form |
372 | | - (loop [loc zloc] |
373 | | - (cond |
374 | | - ;; No more forms |
375 | | - (nil? loc) |
376 | | - (let [;; Process line by line to find comment blocks |
377 | | - result (reduce |
378 | | - (fn [[blocks current-block] [idx line]] |
379 | | - (cond |
380 | | - ;; If we're already tracking a block and the line is a comment |
381 | | - (and current-block |
382 | | - (str/starts-with? (str/trim line) ";;")) |
383 | | - [blocks (update current-block :lines conj line)] |
384 | | - |
385 | | - ;; If we're tracking a block and hit a non-comment line |
386 | | - current-block |
387 | | - [(conj blocks (assoc current-block :end (dec idx))) nil] |
388 | | - |
389 | | - ;; If this is a new comment line |
390 | | - (str/starts-with? (str/trim line) ";;") |
391 | | - [blocks {:start idx |
392 | | - :lines [line]}] |
393 | | - |
394 | | - ;; Otherwise, continue |
395 | | - :else |
396 | | - [blocks nil])) |
397 | | - [[] nil] |
398 | | - (map-indexed vector lines)) |
399 | | - |
400 | | - ;; Extract the blocks and potential unfinished block |
401 | | - [blocks current-block] result |
402 | | - |
403 | | - ;; Finalize blocks, including any unfinished block at EOF |
404 | | - final-blocks (if current-block |
405 | | - (conj blocks (assoc current-block |
406 | | - :end (+ (:start current-block) |
407 | | - (dec (count (:lines current-block)))))) |
408 | | - blocks) |
409 | | - |
410 | | - ;; Find the first consecutive comment block containing the substring |
411 | | - matching-block (first (filter #(some (fn [line] |
412 | | - (str/includes? line comment-substring)) |
413 | | - (:lines %)) |
414 | | - final-blocks))] |
415 | | - |
416 | | - (when matching-block |
417 | | - {:type :line-comments |
418 | | - :start (:start matching-block) |
419 | | - :end (:end matching-block) |
420 | | - :content (str/join "\n" (:lines matching-block))})) |
421 | | - |
422 | | - ;; Check if current form is a comment form |
423 | | - (is-comment-form? loc) |
424 | | - (let [comment-str (z/string loc)] |
425 | | - (if (str/includes? comment-str comment-substring) |
426 | | - (let [pos (z/position-span loc)] |
427 | | - {:type :comment-form |
428 | | - :start (first pos) ;; [row col] |
429 | | - :end (second pos) ;; [row col] |
430 | | - :content comment-str |
431 | | - :zloc loc}) |
432 | | - (recur (z/right loc)))) |
433 | | - |
434 | | - ;; Move to the next form |
435 | | - :else (recur (z/right loc)))))) |
436 | | - |
437 | | -(defn edit-comment-block |
438 | | - "Edit a comment block in the source code. |
439 | | - |
440 | | - Arguments: |
441 | | - - source: The source string |
442 | | - - comment-substring: Substring to identify the comment block |
443 | | - - new-content: New content to replace the comment block with |
444 | | - |
445 | | - Returns the updated source code string, or the original if no matching block was found." |
446 | | - [source comment-substring new-content] |
447 | | - (let [block (find-comment-block source comment-substring)] |
448 | | - (if (nil? block) |
449 | | - source ;; No matching block found |
450 | | - (let [lines (str/split-lines source)] |
451 | | - (case (:type block) |
452 | | - ;; For comment forms, use zloc to replace |
453 | | - :comment-form |
454 | | - (-> (:zloc block) |
455 | | - (z/replace (p/parse-string new-content)) |
456 | | - z/root-string) |
457 | | - |
458 | | - ;; For line comments, replace the relevant lines |
459 | | - :line-comments |
460 | | - (let [start (:start block) |
461 | | - end (:end block) |
462 | | - new-lines (str/split-lines new-content) |
463 | | - result (concat |
464 | | - (take start lines) |
465 | | - new-lines |
466 | | - (drop (inc end) lines))] |
467 | | - (str/join "\n" result))))))) |
468 | | - |
469 | | -;; Form summary and visualization functions |
470 | | - |
471 | | -;; Forward declaration for extract-form-name to enable better organization |
472 | | -(declare extract-form-name) |
473 | | - |
474 | | -(defn get-form-summary |
475 | | - "Get a summarized representation of a Clojure form showing only up to the argument list. |
476 | | - |
477 | | - Arguments: |
478 | | - - zloc: The zipper location of the form |
479 | | - |
480 | | - Returns a string representation of the form summary, or nil if not a valid form." |
481 | | - [zloc] |
482 | | - (try |
483 | | - (let [sexpr (z/sexpr zloc)] |
484 | | - (when (and (seq? sexpr) (symbol? (first sexpr))) |
485 | | - (let [form-sym (first sexpr) |
486 | | - form-type (name form-sym) |
487 | | - base-type (if (str/ends-with? form-type "-") |
488 | | - (subs form-type 0 (dec (count form-type))) |
489 | | - form-type) |
490 | | - form-name (extract-form-name sexpr)] |
491 | | - |
492 | | - (case base-type |
493 | | - "defn" (let [zloc-down (z/down zloc) ; Move to the symbol (defn/defn-) |
494 | | - name-loc (and zloc-down (z/right zloc-down)) ; Move to name |
495 | | - maybe-docstring (and name-loc (z/right name-loc)) ; Next node after name |
496 | | - args-loc (if (and maybe-docstring |
497 | | - (contains? #{:token :multi-line} (z/tag maybe-docstring)) |
498 | | - (string? (z/sexpr maybe-docstring))) |
499 | | - (z/right maybe-docstring) ; Skip docstring to find args |
500 | | - maybe-docstring)] ; No docstring, args right after name |
501 | | - (if (and args-loc (= (z/tag args-loc) :vector)) |
502 | | - (str "(" form-type " " form-name " " (z/string args-loc) " ...)") |
503 | | - (str "(" form-type " " form-name " [...] ...)"))) |
504 | | - |
505 | | - "defmacro" (let [zloc-down (z/down zloc) ; Move to the symbol (defmacro/defmacro-) |
506 | | - name-loc (and zloc-down (z/right zloc-down)) ; Move to name |
507 | | - maybe-docstring (and name-loc (z/right name-loc)) ; Next node after name |
508 | | - args-loc (if (and maybe-docstring |
509 | | - (contains? #{:token :multi-line} (z/tag maybe-docstring)) |
510 | | - (string? (z/sexpr maybe-docstring))) |
511 | | - (z/right maybe-docstring) ; Skip docstring to find args |
512 | | - maybe-docstring)] ; No docstring, args right after name |
513 | | - (if (and args-loc (= (z/tag args-loc) :vector)) |
514 | | - (str "(" form-type " " form-name " " (z/string args-loc) " ...)") |
515 | | - (str "(" form-type " " form-name " [...] ...)"))) |
516 | | - |
517 | | - "defmethod" (let [zloc-down (z/down zloc) ; Move to the symbol "defmethod" |
518 | | - method-loc (and zloc-down (z/right zloc-down)) ; Move to method name |
519 | | - method-sym (and method-loc (z/sexpr method-loc)) |
520 | | - method-name (if (symbol? method-sym) |
521 | | - (if (namespace method-sym) |
522 | | - (str (namespace method-sym) "/" (name method-sym)) |
523 | | - (name method-sym)) |
524 | | - "unknown") |
525 | | - dispatch-loc (and method-loc (z/right method-loc)) ; Move to dispatch value |
526 | | - dispatch-val (and dispatch-loc (z/sexpr dispatch-loc)) |
527 | | - dispatch-str (and dispatch-val (pr-str dispatch-val)) |
528 | | - ;; Find the argument vector after the dispatch value |
529 | | - args-loc (loop [loc (and dispatch-loc (z/right dispatch-loc))] |
530 | | - (cond |
531 | | - (nil? loc) nil |
532 | | - (= (z/tag loc) :vector) loc |
533 | | - :else (recur (z/right loc))))] |
534 | | - (if (and args-loc (= (z/tag args-loc) :vector)) |
535 | | - (str "(" form-type " " method-name " " dispatch-str " " (z/string args-loc) " ...)") |
536 | | - (str "(" form-type " " method-name " " dispatch-str " [...] ...)"))) |
537 | | - |
538 | | - "def" (str "(" form-type " " form-name " ...)") |
539 | | - "deftest" (str "(" form-type " " form-name " ...)") |
540 | | - "ns" (z/string zloc) ; Always show the full namespace |
541 | | - (str "(" form-type " " (or form-name "") " ...)"))))) |
542 | | - (catch Exception e |
543 | | - ;; Provide a fallback in case of errors |
544 | | - (try |
545 | | - (let [raw-str (z/string zloc)] |
546 | | - (if (< (count raw-str) 60) |
547 | | - raw-str |
548 | | - (str (subs raw-str 0 57) "..."))) |
549 | | - (catch Exception _ |
550 | | - nil))))) |
551 | | - |
552 | | -(defn valid-form-to-include? |
553 | | - "Check if a form should be included in the collapsed view. |
554 | | - Excludes forms like comments, unevals, whitespace, etc. |
555 | | - |
556 | | - Arguments: |
557 | | - - zloc: The zipper location to check |
558 | | - |
559 | | - Returns: |
560 | | - - true if the form should be included, false otherwise" |
561 | | - [zloc] |
562 | | - (try |
563 | | - (when zloc |
564 | | - (let [tag (z/tag zloc)] |
565 | | - ;; Exclude specific node types we don't want to process |
566 | | - (not (or |
567 | | - ;; Skip uneval forms (#_) |
568 | | - (= tag :uneval) |
569 | | - ;; Skip whitespace |
570 | | - (= tag :whitespace) |
571 | | - ;; Skip newlines |
572 | | - (= tag :newline) |
573 | | - ;; Skip comments |
574 | | - (= tag :comment))))) |
575 | | - (catch Exception _ |
576 | | - ;; If we can't determine the type, skip it to be safe |
577 | | - false))) |
578 | | - |
579 | | -;; TODO form name can be a keyword if its a spec |
580 | | -(defn extract-form-name |
581 | | - "Extract the name of a form from its sexpr representation. |
582 | | - For example, from (defn foo [x] ...) it extracts 'foo'. |
583 | | - |
584 | | - Arguments: |
585 | | - - sexpr: The S-expression to extract the name from |
586 | | - |
587 | | - Returns: |
588 | | - - The name as a string, or nil if no name could be extracted" |
589 | | - [sexpr] |
590 | | - (try |
591 | | - (when (and (seq? sexpr) |
592 | | - (> (count sexpr) 1) |
593 | | - (symbol? (second sexpr))) |
594 | | - (name (second sexpr))) |
595 | | - (catch Exception _ |
596 | | - nil))) |
597 | | - |
598 | 275 | ;; Source code formatting |
599 | 276 |
|
600 | 277 | (def default-formatting-options |
|
0 commit comments