From 256859e7c4ed8bd223d34122f9add37f292c6c1f Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Sun, 29 Jun 2025 18:27:20 +0600 Subject: [PATCH 01/13] feat: emacs stuff (wip) --- .gitignore | 3 +- Eask | 18 ++++++++++ emacs/cc-autoloads.el | 34 ++++++++++++++++++ emacs/eask-test.el | 51 ++++++++++++++++++++++++++ rustowl.el => emacs/rustowl.el | 66 +++++++++++++++++++--------------- 5 files changed, 142 insertions(+), 30 deletions(-) create mode 100644 Eask create mode 100644 emacs/cc-autoloads.el create mode 100644 emacs/eask-test.el rename rustowl.el => emacs/rustowl.el (70%) diff --git a/.gitignore b/.gitignore index 2fcb0f9c..14d8583b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ completions/ man/ memory-bank/ security-logs/ -benchmark-summary.* \ No newline at end of file +benchmark-summary.* +.eask/ diff --git a/Eask b/Eask new file mode 100644 index 00000000..a732c89d --- /dev/null +++ b/Eask @@ -0,0 +1,18 @@ +;; -*- mode: eask; lexical-binding: t -*- + +(package "rustowl" + "0.3.4" + "Visualize Ownership and Lifetimes in Rust") + +(website-url "https://github.com/cordx56/rustowl") +(keywords "lifetime" "ownership" "rust" "visualization" "tools") + +(package-file "emacs/rustowl.el") + +(script "test" "eask test ert emacs/eask-test.el") + +(source "gnu") +(source "melpa") + +(depends-on "emacs" "26.4") +(depends-on "lsp-mode" "9.0.0") diff --git a/emacs/cc-autoloads.el b/emacs/cc-autoloads.el new file mode 100644 index 00000000..39815ab9 --- /dev/null +++ b/emacs/cc-autoloads.el @@ -0,0 +1,34 @@ +;;; cc-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- +;; Generated by the `loaddefs-generate' function. + +;; This file is part of GNU Emacs. + +;;; Code: + +(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) + + + +;;; Generated autoloads from rustowl.el + +(eval-after-load 'lsp-mode '(lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection '("rustowl")) :major-modes '(rust-mode) :server-id 'rustowl :priority -1 :add-on? t))) +(defvar rustowl-cursor-timer nil) +(defvar rustowl-cursor-timeout 2) +(autoload 'rustowl-reset-cursor-timer "rustowl") +(autoload 'rustowl-enable-cursor "rustowl") +(autoload 'rustowl-disable-cursor "rustowl") +(register-definition-prefixes "rustowl" '("rustowl-")) + +;;; End of scraped data + +(provide 'cc-autoloads) + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; no-native-compile: t +;; coding: utf-8-emacs-unix +;; End: + +;;; cc-autoloads.el ends here diff --git a/emacs/eask-test.el b/emacs/eask-test.el new file mode 100644 index 00000000..faddfc4e --- /dev/null +++ b/emacs/eask-test.el @@ -0,0 +1,51 @@ +;;; eask-test.el --- Tests for rustowl.el using Eask and ert -*- lexical-binding: t; -*- + +(require 'ert) +(require 'rustowl) + +(ert-deftest rustowl-line-number-at-pos-test () + "Test `rustowl-line-number-at-pos' returns correct line number." + (with-temp-buffer + (insert "line1\nline2\nline3") + (goto-char (point-min)) + (should (= (rustowl-line-number-at-pos) 0)) + (forward-line 1) + (should (= (rustowl-line-number-at-pos) 1)) + (goto-char (point-max)) + (should (= (rustowl-line-number-at-pos) 2)))) + +(ert-deftest rustowl-current-column-test () + "Test `rustowl-current-column' returns correct column." + (with-temp-buffer + (insert "abc\ndef") + (goto-char (point-min)) + (should (= (rustowl-current-column) 0)) + (forward-char 3) + (should (= (rustowl-current-column) 3)) + (goto-char (point-max)) + (should (= (rustowl-current-column) 3)))) + +(ert-deftest rustowl-line-col-to-pos-test () + "Test `rustowl-line-col-to-pos' returns correct buffer position." + (with-temp-buffer + (insert "abc\ndef\nghi") + (should (= (rustowl-line-col-to-pos 0 0) (point-min))) + (should (= (rustowl-line-col-to-pos 1 0) + (save-excursion (goto-char (point-min)) (forward-line 1) (point)))) + (should (= (rustowl-line-col-to-pos 2 2) + (save-excursion (goto-char (point-min)) (forward-line 2) (move-to-column 2) (point)))))) + +(ert-deftest rustowl-underline-and-clear-overlays-test () + "Test `rustowl-underline' and `rustowl-clear-overlays'." + (with-temp-buffer + (insert "abcde") + (let ((start (point-min)) + (end (1+ (point-min)))) + (should (= (length rustowl-overlays) 0)) + (let ((ov (rustowl-underline start end "#ff0000"))) + (should (overlayp ov)) + (should (= (length rustowl-overlays) 1)) + (rustowl-clear-overlays) + (should (= (length rustowl-overlays) 0)))))) + +(provide 'eask-test) diff --git a/rustowl.el b/emacs/rustowl.el similarity index 70% rename from rustowl.el rename to emacs/rustowl.el index 96da6b78..9c76edcf 100644 --- a/rustowl.el +++ b/emacs/rustowl.el @@ -3,37 +3,39 @@ ;; Copyright (C) cordx56 ;; Author: cordx56 -;; Keywords: tools +;; Keywords: tools lifetime ownership visualization rust -;; Version: 0.2.0 -;; Package-Requires: ((emacs "24.1") (lsp-mode "9.0.0")) +;; Version: 0.3.4 +;; Package-Requires: ((emacs "24.4") (lsp-mode "9.0.0")) ;; URL: https://github.com/cordx56/rustowl ;; SPDX-License-Identifier: MPL-2.0 ;;; Commentary: +;; Visualize Ownership and Lifetimes in Rust. ;;; Code: (require 'lsp-mode) (defgroup rustowl () - "Visualize Ownership and Lifetimes in Rust" + "Visualize Ownership and Lifetimes in Rust." :group 'tools :prefix "rustowl-" :link '(url-link "https://github.com/cordx56/rustowl")) ;;;###autoload -(with-eval-after-load 'lsp-mode - (lsp-register-client - (make-lsp-client - :new-connection (lsp-stdio-connection '("rustowl")) - :major-modes '(rust-mode) - :server-id 'rustowl - :priority -1 - :add-on? t))) +(eval-after-load 'lsp-mode + '(lsp-register-client + (make-lsp-client + :new-connection (lsp-stdio-connection '("rustowl")) + :major-modes '(rust-mode) + :server-id 'rustowl + :priority -1 + :add-on? t))) (defun rustowl-cursor (params) + "Request and visualize Rust ownership/lifetime overlays for PARAMS." (lsp-request-async "rustowl/cursor" params @@ -53,7 +55,7 @@ (gethash "line" end) (gethash "character" end))) (overlapped (gethash "overlapped" deco))) - (if (not overlapped) + (unless overlapped (cond ((equal type "lifetime") (rustowl-underline start-pos end-pos "#00cc00")) @@ -68,38 +70,39 @@ decorations))) :mode 'current)) - (defun rustowl-line-number-at-pos () + "Return the line number at point." (save-excursion (goto-char (point)) (count-lines (point-min) (line-beginning-position)))) + (defun rustowl-current-column () + "Return the current column at point." (save-excursion (let ((start (point))) (move-beginning-of-line 1) (- start (point))))) (defun rustowl-cursor-call () + "Call RustOwl for current cursor position." (let ((line (rustowl-line-number-at-pos)) (column (rustowl-current-column)) (uri (lsp--buffer-uri))) - (rustowl-cursor `( - :position ,`( - :line ,line - :character ,column - ) - :document ,`( - :uri ,uri - ) - )))) + (rustowl-cursor + `(:position (:line ,line :character ,column) + :document (:uri ,uri))))) ;;;###autoload -(defvar rustowl-cursor-timer nil) +(defvar rustowl-cursor-timer nil + "Timer object for rustowl cursor overlays.") + ;;;###autoload -(defvar rustowl-cursor-timeout 2) +(defvar rustowl-cursor-timeout 2 + "Idle seconds before showing cursor overlays.") ;;;###autoload (defun rustowl-reset-cursor-timer () + "Reset RustOwl's idle timer for overlays." (when rustowl-cursor-timer (cancel-timer rustowl-cursor-timer)) (rustowl-clear-overlays) @@ -107,33 +110,38 @@ (run-with-idle-timer rustowl-cursor-timeout nil #'rustowl-cursor-call))) ;;;###autoload -(defun enable-rustowl-cursor () +(defun rustowl-enable-cursor () + "Enable RustOwl overlay updates on cursor move." (add-hook 'post-command-hook #'rustowl-reset-cursor-timer)) ;;;###autoload -(defun disable-rustowl-cursor () +(defun rustowl-disable-cursor () + "Disable RustOwl overlay updates." (remove-hook 'post-command-hook #'rustowl-reset-cursor-timer) (when rustowl-cursor-timer (cancel-timer rustowl-cursor-timer) (setq rustowl-cursor-timer nil))) -;; RustOwl visualization (defun rustowl-line-col-to-pos (line col) + "Convert LINE and COL to buffer position." (save-excursion (goto-char (point-min)) (forward-line line) (move-to-column col) (point))) -(defvar rustowl-overlays nil) +(defvar rustowl-overlays nil + "List of currently active RustOwl overlays.") (defun rustowl-underline (start end color) + "Underline region from START to END with COLOR." (let ((overlay (make-overlay start end))) (overlay-put overlay 'face `(:underline (:color ,color :style wave))) (push overlay rustowl-overlays) overlay)) (defun rustowl-clear-overlays () + "Remove all RustOwl overlays." (interactive) (mapc #'delete-overlay rustowl-overlays) (setq rustowl-overlays nil)) From 35da863805d13a5cbdaf70eb6130c5d4b7f9bf96 Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Sun, 29 Jun 2025 18:28:32 +0600 Subject: [PATCH 02/13] chore: format --- emacs/rustowl.el | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/emacs/rustowl.el b/emacs/rustowl.el index 9c76edcf..96316439 100644 --- a/emacs/rustowl.el +++ b/emacs/rustowl.el @@ -28,7 +28,8 @@ (eval-after-load 'lsp-mode '(lsp-register-client (make-lsp-client - :new-connection (lsp-stdio-connection '("rustowl")) + :new-connection + (lsp-stdio-connection '("rustowl")) :major-modes '(rust-mode) :server-id 'rustowl :priority -1 @@ -37,8 +38,7 @@ (defun rustowl-cursor (params) "Request and visualize Rust ownership/lifetime overlays for PARAMS." (lsp-request-async - "rustowl/cursor" - params + "rustowl/cursor" params (lambda (response) (let ((decorations (gethash "decorations" response))) (mapc @@ -52,8 +52,7 @@ (gethash "character" start))) (end-pos (rustowl-line-col-to-pos - (gethash "line" end) - (gethash "character" end))) + (gethash "line" end) (gethash "character" end))) (overlapped (gethash "overlapped" deco))) (unless overlapped (cond @@ -89,7 +88,8 @@ (column (rustowl-current-column)) (uri (lsp--buffer-uri))) (rustowl-cursor - `(:position (:line ,line :character ,column) + `(:position + (:line ,line :character ,column) :document (:uri ,uri))))) ;;;###autoload @@ -107,7 +107,8 @@ (cancel-timer rustowl-cursor-timer)) (rustowl-clear-overlays) (setq rustowl-cursor-timer - (run-with-idle-timer rustowl-cursor-timeout nil #'rustowl-cursor-call))) + (run-with-idle-timer + rustowl-cursor-timeout nil #'rustowl-cursor-call))) ;;;###autoload (defun rustowl-enable-cursor () @@ -136,7 +137,8 @@ (defun rustowl-underline (start end color) "Underline region from START to END with COLOR." (let ((overlay (make-overlay start end))) - (overlay-put overlay 'face `(:underline (:color ,color :style wave))) + (overlay-put + overlay 'face `(:underline (:color ,color :style wave))) (push overlay rustowl-overlays) overlay)) From ff51086e4217f9be6c529e86c192e3bb1ef43568 Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Sun, 29 Jun 2025 18:29:12 +0600 Subject: [PATCH 03/13] chore: format (use elfmt only) --- emacs/rustowl.el | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/emacs/rustowl.el b/emacs/rustowl.el index 96316439..e67eb864 100644 --- a/emacs/rustowl.el +++ b/emacs/rustowl.el @@ -18,7 +18,8 @@ (require 'lsp-mode) -(defgroup rustowl () +(defgroup rustowl + () "Visualize Ownership and Lifetimes in Rust." :group 'tools :prefix "rustowl-" @@ -28,8 +29,7 @@ (eval-after-load 'lsp-mode '(lsp-register-client (make-lsp-client - :new-connection - (lsp-stdio-connection '("rustowl")) + :new-connection (lsp-stdio-connection '("rustowl")) :major-modes '(rust-mode) :server-id 'rustowl :priority -1 @@ -44,16 +44,17 @@ (mapc (lambda (deco) (let* ((type (gethash "type" deco)) - (start (gethash "start" (gethash "range" deco))) - (end (gethash "end" (gethash "range" deco))) - (start-pos + (start (gethash "start" (gethash "range" deco))) + (end (gethash "end" (gethash "range" deco))) + (start-pos (rustowl-line-col-to-pos (gethash "line" start) (gethash "character" start))) - (end-pos + (end-pos (rustowl-line-col-to-pos - (gethash "line" end) (gethash "character" end))) - (overlapped (gethash "overlapped" deco))) + (gethash "line" end) + (gethash "character" end))) + (overlapped (gethash "overlapped" deco))) (unless overlapped (cond ((equal type "lifetime") @@ -85,8 +86,8 @@ (defun rustowl-cursor-call () "Call RustOwl for current cursor position." (let ((line (rustowl-line-number-at-pos)) - (column (rustowl-current-column)) - (uri (lsp--buffer-uri))) + (column (rustowl-current-column)) + (uri (lsp--buffer-uri))) (rustowl-cursor `(:position (:line ,line :character ,column) @@ -103,8 +104,7 @@ ;;;###autoload (defun rustowl-reset-cursor-timer () "Reset RustOwl's idle timer for overlays." - (when rustowl-cursor-timer - (cancel-timer rustowl-cursor-timer)) + (when rustowl-cursor-timer (cancel-timer rustowl-cursor-timer)) (rustowl-clear-overlays) (setq rustowl-cursor-timer (run-with-idle-timer @@ -138,7 +138,8 @@ "Underline region from START to END with COLOR." (let ((overlay (make-overlay start end))) (overlay-put - overlay 'face `(:underline (:color ,color :style wave))) + overlay 'face + `(:underline (:color ,color :style wave))) (push overlay rustowl-overlays) overlay)) From d1177f0fba0a09c9b2bd08c0559212ff5f7a1b11 Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Sun, 29 Jun 2025 18:35:17 +0600 Subject: [PATCH 04/13] some test stuff --- Eask | 2 +- emacs/{eask-test.el => test.el} | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) rename emacs/{eask-test.el => test.el} (93%) diff --git a/Eask b/Eask index a732c89d..3eb4087c 100644 --- a/Eask +++ b/Eask @@ -9,7 +9,7 @@ (package-file "emacs/rustowl.el") -(script "test" "eask test ert emacs/eask-test.el") +(script "test" "eask test ert emacs/test.el") (source "gnu") (source "melpa") diff --git a/emacs/eask-test.el b/emacs/test.el similarity index 93% rename from emacs/eask-test.el rename to emacs/test.el index faddfc4e..844bb85d 100644 --- a/emacs/eask-test.el +++ b/emacs/test.el @@ -1,4 +1,4 @@ -;;; eask-test.el --- Tests for rustowl.el using Eask and ert -*- lexical-binding: t; -*- +;;; test.el --- Tests for rustowl.el using Eask and ert -*- lexical-binding: t; -*- (require 'ert) (require 'rustowl) @@ -48,4 +48,5 @@ (rustowl-clear-overlays) (should (= (length rustowl-overlays) 0)))))) -(provide 'eask-test) +(provide 'test) +;;; test.el ends here From f6770dcee2f8d5b6e016acea30a317d9496ea189 Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Thu, 17 Jul 2025 13:18:16 +0600 Subject: [PATCH 05/13] feat: add tests! --- .github/workflows/emacs-checks.yml | 117 ++++++++++++ Eask | 8 +- emacs-tests/rustowl-test.el | 282 +++++++++++++++++++++++++++++ emacs/cc-autoloads.el | 34 ---- emacs/test.el | 52 ------ emacs/rustowl.el => rustowl.el | 63 ++++--- scripts/bump.sh | 13 +- 7 files changed, 452 insertions(+), 117 deletions(-) create mode 100644 .github/workflows/emacs-checks.yml create mode 100644 emacs-tests/rustowl-test.el delete mode 100644 emacs/cc-autoloads.el delete mode 100644 emacs/test.el rename emacs/rustowl.el => rustowl.el (72%) diff --git a/.github/workflows/emacs-checks.yml b/.github/workflows/emacs-checks.yml new file mode 100644 index 00000000..25db97fa --- /dev/null +++ b/.github/workflows/emacs-checks.yml @@ -0,0 +1,117 @@ +name: Eask CI + +on: + push: + paths: + - Eask + - rustowl.el + - emacs-tests/ + - .github/workflows/emacs-checks.yml + branches: + - main + pull_request: + paths: + - Eask + - rustowl.el + - emacs-tests/ + - .github/workflows/emacs-checks.yml + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Emacs + uses: jcs090218/setup-emacs@master + with: + version: 28.1 + + - name: Set up Eask (snapshot) + uses: emacs-eask/setup-eask@master + with: + version: 'snapshot' + + - name: Setup Rustowl + run: | + ./scripts/build/toolchain cargo build --release + ./scripts/build/toolchain cargo install --path . --locked + + - name: Lint Regexps + run: eask lint regexps + + - name: Lint Package + run: eask lint package + + - name: Lint License + run: eask lint license + + - name: Lint Keywords + run: eask lint keywords + + - name: Lint Indent + run: eask lint indent + + - name: Lint Checkdocs + run: eask lint checkdocs + + - name: Lint Declare + run: eask lint declare + + - name: Lint elisp-lint + run: eask lint elisp-lint + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Emacs + uses: jcs090218/setup-emacs@master + with: + version: 28.1 + + - name: Set up Eask (snapshot) + uses: emacs-eask/setup-eask@master + with: + version: 'snapshot' + + - name: Setup Rustowl + run: | + ./scripts/build/toolchain cargo build --release + ./scripts/build/toolchain cargo install --path . --locked + + - name: Run Tests + run: eask run script test + + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Emacs + uses: jcs090218/setup-emacs@master + with: + version: 28.1 + + - name: Setup Rustowl + run: | + ./scripts/build/toolchain cargo build --release + ./scripts/build/toolchain cargo install --path . --locked + + - name: Setup Eask + uses: emacs-eask/setup-eask@master + with: + version: 'snapshot' + + - name: Build And Compile + run: | + eask package + eask install + eask compile diff --git a/Eask b/Eask index 3eb4087c..e7b1dd4f 100644 --- a/Eask +++ b/Eask @@ -7,12 +7,14 @@ (website-url "https://github.com/cordx56/rustowl") (keywords "lifetime" "ownership" "rust" "visualization" "tools") -(package-file "emacs/rustowl.el") +(package-file "rustowl.el") -(script "test" "eask test ert emacs/test.el") +(script "test" "eask test ert emacs-tests/rustowl-test.el") (source "gnu") (source "melpa") -(depends-on "emacs" "26.4") +(depends-on "emacs" "28.1") (depends-on "lsp-mode" "9.0.0") + +(setq network-security-level 'low) ; see https://github.com/jcs090218/setup-emacs-windows/issues/156#issuecomment-932956432 diff --git a/emacs-tests/rustowl-test.el b/emacs-tests/rustowl-test.el new file mode 100644 index 00000000..9befa4f3 --- /dev/null +++ b/emacs-tests/rustowl-test.el @@ -0,0 +1,282 @@ +;;; rustowl-test.el --- ERT tests for rustowl.el -*- lexical-binding: t; -*- + +;;; Code: + +(require 'ert) +(require 'rustowl) + +;; Test rustowl-line-number-at-pos +(ert-deftest rustowl-test-line-number-at-pos () + (with-temp-buffer + (insert "line1\nline2\nline3") + (goto-char (point-min)) + (should (= (rustowl-line-number-at-pos) 0)) + (forward-line 1) + (should (= (rustowl-line-number-at-pos) 1)) + (forward-line 1) + (should (= (rustowl-line-number-at-pos) 2)))) + +;; Test rustowl-current-column +(ert-deftest rustowl-test-current-column () + (with-temp-buffer + (insert "abc\ndef\nghi") + (goto-char (point-min)) + (forward-char 2) + (should (= (rustowl-current-column) 2)) + (forward-line 1) + (should (= (rustowl-current-column) 0)))) + +;; Test rustowl-line-col-to-pos +(ert-deftest rustowl-test-line-col-to-pos () + (with-temp-buffer + (insert "abc\ndef\nghi") + (let ((pos (rustowl-line-col-to-pos 1 2))) + (goto-char pos) + (should (= (rustowl-line-number-at-pos) 1)) + (should (= (rustowl-current-column) 2))))) + +;; Test rustowl-underline and rustowl-clear-overlays +(ert-deftest rustowl-test-underline-and-clear () + (with-temp-buffer + (insert "abcdef") + (let ((ov (rustowl-underline 1 4 "#ff0000"))) + (should (overlayp ov)) + (should (memq ov rustowl-overlays)) + (should + (equal + (overlay-get ov 'face) + '(:underline (:color "#ff0000" :style wave)))) + (rustowl-clear-overlays) + (should (null rustowl-overlays))))) + +;; Test rustowl-underline edge cases +(ert-deftest rustowl-test-underline-edge-cases () + (with-temp-buffer + (insert "abcdef") + ;; start == end: should still create a zero-length overlay + (let ((ov (rustowl-underline 2 2 "#00ff00"))) + (should (overlayp ov)) + (should (= (overlay-start ov) 2)) + (should (= (overlay-end ov) 2))) + ;; start > end: should still create an overlay, but Emacs overlays swap start/end + (let ((ov (rustowl-underline 4 2 "#00ff00"))) + (should (overlayp ov)) + (should (= (overlay-start ov) 2)) + (should (= (overlay-end ov) 4))) + ;; out-of-bounds: should not error, overlay will be clamped + (let ((ov (rustowl-underline -5 100 "#00ff00"))) + (should (overlayp ov)) + (should (<= (overlay-start ov) (point-max))) + (should (>= (overlay-start ov) (point-min))) + (should (<= (overlay-end ov) (point-max))) + (should (>= (overlay-end ov) (point-min)))))) + +;; Test rustowl-clear-overlays when overlays list is empty +(ert-deftest rustowl-test-clear-overlays-empty () + (let ((rustowl-overlays nil)) + (should-not (rustowl-clear-overlays)) + (should (null rustowl-overlays)))) + +;; Test timer logic: rustowl-reset-cursor-timer, rustowl-enable-cursor, rustowl-disable-cursor +(ert-deftest rustowl-test-timer-logic () + (let ((rustowl-cursor-timer nil) + (rustowl-cursor-timeout 0.01) + (called nil)) + (cl-letf (((symbol-function 'run-with-idle-timer) + (lambda (_secs _repeat function &rest _args) + (setq called t) + 'fake-timer)) + ((symbol-function 'cancel-timer) + (lambda (_timer) (setq called 'cancelled))) + ((symbol-function 'rustowl-clear-overlays) + (lambda () (setq called 'cleared)))) + (rustowl-reset-cursor-timer) + (should (or (eq called t) (eq called 'cleared))) + (setq rustowl-cursor-timer 'fake-timer) + (rustowl-disable-cursor) + (should (or (eq called 'cancelled) (eq called 'cleared))) + (let ((added nil)) + (cl-letf (((symbol-function 'add-hook) + (lambda (hook fn) (setq added (list hook fn))))) + (rustowl-enable-cursor) + (should + (equal + added + '(post-command-hook rustowl-reset-cursor-timer)))))))) + +;; Test idempotency of rustowl-enable-cursor and rustowl-disable-cursor +(ert-deftest rustowl-test-enable-disable-idempotent () + (let ((add-count 0) + (remove-count 0)) + (cl-letf (((symbol-function 'add-hook) + (lambda (hook fn) (cl-incf add-count))) + ((symbol-function 'remove-hook) + (lambda (hook fn) (cl-incf remove-count)))) + (rustowl-enable-cursor) + (rustowl-enable-cursor) + (should (>= add-count 2)) + (rustowl-disable-cursor) + (rustowl-disable-cursor) + (should (>= remove-count 2))))) + +;; Test rustowl-reset-cursor-timer when timer is nil +(ert-deftest rustowl-test-reset-cursor-timer-nil () + (let ((rustowl-cursor-timer nil) + (called nil)) + (cl-letf (((symbol-function 'cancel-timer) + (lambda (_timer) (setq called t))) + ((symbol-function 'rustowl-clear-overlays) + (lambda () (setq called 'cleared))) + ((symbol-function 'run-with-idle-timer) + (lambda (_timeout _repeat fn &rest _args) + 'fake-timer))) + (should (not called)) + (rustowl-reset-cursor-timer) + (should (not (eq called t))) + (should (or (eq called 'cleared) (null called)))))) + +;; Test rustowl-line-col-to-pos with out-of-bounds line/col +(ert-deftest rustowl-test-line-col-to-pos-out-of-bounds () + (with-temp-buffer + (insert "abc\ndef\nghi") + ;; Negative line/col should signal error + (should-error (rustowl-line-col-to-pos -1 -1)) + ;; Line past end + (should (= (rustowl-line-col-to-pos 100 0) (point-max))) + ;; Col past end of line + (goto-char (rustowl-line-col-to-pos 0 100)) + (should (>= (point) (point-min))) + (should (<= (point) (point-max))))) + +;; Test rustowl-cursor overlays (mocking lsp-request-async) +(ert-deftest rustowl-test-cursor-overlays () + (let ((called nil) + (response + (let ((ht (make-hash-table :test 'equal))) + (let* ((deco1 (make-hash-table :test 'equal)) + (range1 (make-hash-table :test 'equal)) + (start1 (make-hash-table :test 'equal)) + (end1 (make-hash-table :test 'equal)) + (deco2 (make-hash-table :test 'equal)) + (range2 (make-hash-table :test 'equal)) + (start2 (make-hash-table :test 'equal)) + (end2 (make-hash-table :test 'equal))) + (puthash "line" 0 start1) + (puthash "character" 0 start1) + (puthash "line" 0 end1) + (puthash "character" 3 end1) + (puthash "start" start1 range1) + (puthash "end" end1 range1) + (puthash "type" "lifetime" deco1) + (puthash "range" range1 deco1) + (puthash "overlapped" nil deco1) + (puthash "line" 0 start2) + (puthash "character" 4 start2) + (puthash "line" 0 end2) + (puthash "character" 6 end2) + (puthash "start" start2 range2) + (puthash "end" end2 range2) + (puthash "type" "imm_borrow" deco2) + (puthash "range" range2 deco2) + (puthash "overlapped" nil deco2) + (puthash "decorations" (vector deco1 deco2) ht) + ht)))) + (with-temp-buffer + (insert "abcdef") + (cl-letf (((symbol-function 'lsp-request-async) + (lambda (_method _params cb &rest _args) + (funcall cb response) + (setq called t))) + ((symbol-function 'rustowl-underline) + (lambda (start end color) + (setq called (list start end color)) + (make-overlay start end)))) + (rustowl-cursor + '(:position + (:line 0 :character 0) + :document (:uri "file:///fake"))) + (should called))))) + +;; Test rustowl-cursor overlays for all type branches and overlapped +(ert-deftest rustowl-test-cursor-overlays-all-types () + (let ((called-types '())) + (let* ((make-deco + (lambda (type &optional overlapped) + (let ((deco (make-hash-table :test 'equal)) + (range (make-hash-table :test 'equal)) + (start (make-hash-table :test 'equal)) + (end (make-hash-table :test 'equal))) + (puthash "line" 0 start) + (puthash "character" 0 start) + (puthash "line" 0 end) + (puthash "character" 2 end) + (puthash "start" start range) + (puthash "end" end range) + (puthash "type" type deco) + (puthash "range" range deco) + (puthash "overlapped" overlapped deco) + deco))) + (response + (let ((ht (make-hash-table :test 'equal))) + (puthash + "decorations" + (vector + (funcall make-deco "lifetime") + (funcall make-deco "imm_borrow") + (funcall make-deco "mut_borrow") + (funcall make-deco "move") + (funcall make-deco "call") + (funcall make-deco "outlive") + (funcall make-deco "lifetime" t)) ; overlapped + ht) + ht))) + (with-temp-buffer + (insert "abcdef") + (cl-letf (((symbol-function 'lsp-request-async) + (lambda (_method _params cb &rest _args) + (funcall cb response))) + ((symbol-function 'rustowl-underline) + (lambda (_start _end color) + (push color called-types) + (make-overlay 1 2)))) + (rustowl-cursor + '(:position + (:line 0 :character 0) + :document (:uri "file:///fake"))) + ;; Should get all colors except for the overlapped one + (should (member "#00cc00" called-types)) ; lifetime + (should (member "#0000cc" called-types)) ; imm_borrow + (should (member "#cc00cc" called-types)) ; mut_borrow + (should (member "#cccc00" called-types)) ; move/call + (should (member "#cc0000" called-types)) ; outlive + ;; Should not call underline for overlapped + (should + (= (length + (cl-remove-if-not + (lambda (c) (equal c "#00cc00")) called-types)) + 1))))))) + +;; Test rustowl-cursor-call (mocking buffer and lsp) +(ert-deftest rustowl-test-cursor-call () + (let ((called nil)) + (with-temp-buffer + (insert "abc\ndef") + (goto-char (point-min)) + (cl-letf (((symbol-function 'rustowl-line-number-at-pos) + (lambda () 0)) + ((symbol-function 'rustowl-current-column) + (lambda () 1)) + ((symbol-function 'lsp--buffer-uri) + (lambda () "file:///fake")) + ((symbol-function 'rustowl-cursor) + (lambda (params) (setq called params)))) + (rustowl-cursor-call) + (should + (equal + called + '(:position + (:line 0 :character 1) + :document (:uri "file:///fake")))))))) + +(provide 'rustowl-test) +;;; rustowl-test.el ends here diff --git a/emacs/cc-autoloads.el b/emacs/cc-autoloads.el deleted file mode 100644 index 39815ab9..00000000 --- a/emacs/cc-autoloads.el +++ /dev/null @@ -1,34 +0,0 @@ -;;; cc-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- -;; Generated by the `loaddefs-generate' function. - -;; This file is part of GNU Emacs. - -;;; Code: - -(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) - - - -;;; Generated autoloads from rustowl.el - -(eval-after-load 'lsp-mode '(lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection '("rustowl")) :major-modes '(rust-mode) :server-id 'rustowl :priority -1 :add-on? t))) -(defvar rustowl-cursor-timer nil) -(defvar rustowl-cursor-timeout 2) -(autoload 'rustowl-reset-cursor-timer "rustowl") -(autoload 'rustowl-enable-cursor "rustowl") -(autoload 'rustowl-disable-cursor "rustowl") -(register-definition-prefixes "rustowl" '("rustowl-")) - -;;; End of scraped data - -(provide 'cc-autoloads) - -;; Local Variables: -;; version-control: never -;; no-byte-compile: t -;; no-update-autoloads: t -;; no-native-compile: t -;; coding: utf-8-emacs-unix -;; End: - -;;; cc-autoloads.el ends here diff --git a/emacs/test.el b/emacs/test.el deleted file mode 100644 index 844bb85d..00000000 --- a/emacs/test.el +++ /dev/null @@ -1,52 +0,0 @@ -;;; test.el --- Tests for rustowl.el using Eask and ert -*- lexical-binding: t; -*- - -(require 'ert) -(require 'rustowl) - -(ert-deftest rustowl-line-number-at-pos-test () - "Test `rustowl-line-number-at-pos' returns correct line number." - (with-temp-buffer - (insert "line1\nline2\nline3") - (goto-char (point-min)) - (should (= (rustowl-line-number-at-pos) 0)) - (forward-line 1) - (should (= (rustowl-line-number-at-pos) 1)) - (goto-char (point-max)) - (should (= (rustowl-line-number-at-pos) 2)))) - -(ert-deftest rustowl-current-column-test () - "Test `rustowl-current-column' returns correct column." - (with-temp-buffer - (insert "abc\ndef") - (goto-char (point-min)) - (should (= (rustowl-current-column) 0)) - (forward-char 3) - (should (= (rustowl-current-column) 3)) - (goto-char (point-max)) - (should (= (rustowl-current-column) 3)))) - -(ert-deftest rustowl-line-col-to-pos-test () - "Test `rustowl-line-col-to-pos' returns correct buffer position." - (with-temp-buffer - (insert "abc\ndef\nghi") - (should (= (rustowl-line-col-to-pos 0 0) (point-min))) - (should (= (rustowl-line-col-to-pos 1 0) - (save-excursion (goto-char (point-min)) (forward-line 1) (point)))) - (should (= (rustowl-line-col-to-pos 2 2) - (save-excursion (goto-char (point-min)) (forward-line 2) (move-to-column 2) (point)))))) - -(ert-deftest rustowl-underline-and-clear-overlays-test () - "Test `rustowl-underline' and `rustowl-clear-overlays'." - (with-temp-buffer - (insert "abcde") - (let ((start (point-min)) - (end (1+ (point-min)))) - (should (= (length rustowl-overlays) 0)) - (let ((ov (rustowl-underline start end "#ff0000"))) - (should (overlayp ov)) - (should (= (length rustowl-overlays) 1)) - (rustowl-clear-overlays) - (should (= (length rustowl-overlays) 0)))))) - -(provide 'test) -;;; test.el ends here diff --git a/emacs/rustowl.el b/rustowl.el similarity index 72% rename from emacs/rustowl.el rename to rustowl.el index e67eb864..4cd8e1ce 100644 --- a/emacs/rustowl.el +++ b/rustowl.el @@ -6,7 +6,7 @@ ;; Keywords: tools lifetime ownership visualization rust ;; Version: 0.3.4 -;; Package-Requires: ((emacs "24.4") (lsp-mode "9.0.0")) +;; Package-Requires: ((emacs "28.1") (lsp-mode "9.0.0")) ;; URL: https://github.com/cordx56/rustowl ;; SPDX-License-Identifier: MPL-2.0 @@ -18,22 +18,21 @@ (require 'lsp-mode) -(defgroup rustowl - () +(defgroup rustowl () "Visualize Ownership and Lifetimes in Rust." :group 'tools :prefix "rustowl-" :link '(url-link "https://github.com/cordx56/rustowl")) ;;;###autoload -(eval-after-load 'lsp-mode - '(lsp-register-client - (make-lsp-client - :new-connection (lsp-stdio-connection '("rustowl")) - :major-modes '(rust-mode) - :server-id 'rustowl - :priority -1 - :add-on? t))) +(with-eval-after-load 'lsp-mode + (lsp-register-client + (make-lsp-client + :new-connection (lsp-stdio-connection '("rustowl")) + :major-modes '(rust-mode) + :server-id 'rustowl + :priority -1 + :add-on? t))) (defun rustowl-cursor (params) "Request and visualize Rust ownership/lifetime overlays for PARAMS." @@ -44,17 +43,16 @@ (mapc (lambda (deco) (let* ((type (gethash "type" deco)) - (start (gethash "start" (gethash "range" deco))) - (end (gethash "end" (gethash "range" deco))) - (start-pos + (start (gethash "start" (gethash "range" deco))) + (end (gethash "end" (gethash "range" deco))) + (start-pos (rustowl-line-col-to-pos (gethash "line" start) (gethash "character" start))) - (end-pos + (end-pos (rustowl-line-col-to-pos - (gethash "line" end) - (gethash "character" end))) - (overlapped (gethash "overlapped" deco))) + (gethash "line" end) (gethash "character" end))) + (overlapped (gethash "overlapped" deco))) (unless overlapped (cond ((equal type "lifetime") @@ -86,8 +84,8 @@ (defun rustowl-cursor-call () "Call RustOwl for current cursor position." (let ((line (rustowl-line-number-at-pos)) - (column (rustowl-current-column)) - (uri (lsp--buffer-uri))) + (column (rustowl-current-column)) + (uri (lsp--buffer-uri))) (rustowl-cursor `(:position (:line ,line :character ,column) @@ -104,7 +102,8 @@ ;;;###autoload (defun rustowl-reset-cursor-timer () "Reset RustOwl's idle timer for overlays." - (when rustowl-cursor-timer (cancel-timer rustowl-cursor-timer)) + (when rustowl-cursor-timer + (cancel-timer rustowl-cursor-timer)) (rustowl-clear-overlays) (setq rustowl-cursor-timer (run-with-idle-timer @@ -124,12 +123,23 @@ (setq rustowl-cursor-timer nil))) (defun rustowl-line-col-to-pos (line col) - "Convert LINE and COL to buffer position." + "Convert LINE and COL to buffer position. +If LINE or COL is negative, signal an error. +If LINE is past the last line, return (point-max). +If COL is past end of line, clamp to end of line." + (when (or (< line 0) (< col 0)) + (error "rustowl-line-col-to-pos: negative line or column")) (save-excursion (goto-char (point-min)) - (forward-line line) - (move-to-column col) - (point))) + (let ((max-line (count-lines (point-min) (point-max)))) + (if (>= line max-line) + (point-max) + (forward-line line) + (let ((bol (point)) + (eol (line-end-position))) + (goto-char bol) + (forward-char (min col (- eol bol))) + (point)))))) (defvar rustowl-overlays nil "List of currently active RustOwl overlays.") @@ -138,8 +148,7 @@ "Underline region from START to END with COLOR." (let ((overlay (make-overlay start end))) (overlay-put - overlay 'face - `(:underline (:color ,color :style wave))) + overlay 'face `(:underline (:color ,color :style wave))) (push overlay rustowl-overlays) overlay)) diff --git a/scripts/bump.sh b/scripts/bump.sh index d07c9613..481ec07c 100755 --- a/scripts/bump.sh +++ b/scripts/bump.sh @@ -67,7 +67,18 @@ else echo "Warning: aur/PKGBUILD-BIN not found" fi -# 5. Create a git tag +# 5. Update Emacs only for stable +if [ "$IS_PRERELEASE" = false ] && [ -f Eask ] && [ -f rustowl.el ]; then + echo "Updating Eask And rustowl..." + $sed -i "4s/.*/$VERSION_WITHOUT_V/" Eask + $sed -i "8s/.*/;; Version: $VERSION_WITHOUT_V/" rustowl.el +elif [ -f Eask ] && [ -f rustowl.el ]; then + echo "Skipping Eask and rustowl.el update for pre-release version" +else + echo "Warning: Eask or rustowl.el not found" +fi + +# 6. Create a git tag echo "Creating git tag: $VERSION" git tag $VERSION From 13d1ace6bb11fc61705b353239456e03a6878b68 Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Thu, 17 Jul 2025 15:43:18 +0600 Subject: [PATCH 06/13] lockfile --- .github/workflows/emacs-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/emacs-checks.yml b/.github/workflows/emacs-checks.yml index 25db97fa..ebfae1a0 100644 --- a/.github/workflows/emacs-checks.yml +++ b/.github/workflows/emacs-checks.yml @@ -55,7 +55,7 @@ jobs: run: eask lint indent - name: Lint Checkdocs - run: eask lint checkdocs + run: eask lint checkdoc - name: Lint Declare run: eask lint declare From 605b2b0edfec42bf06c2d5d8ee0aad4c09e7013a Mon Sep 17 00:00:00 2001 From: Muntasir Mahmud Date: Thu, 17 Jul 2025 15:52:16 +0600 Subject: [PATCH 07/13] fix --- .github/workflows/emacs-checks.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/emacs-checks.yml b/.github/workflows/emacs-checks.yml index ebfae1a0..339ef761 100644 --- a/.github/workflows/emacs-checks.yml +++ b/.github/workflows/emacs-checks.yml @@ -86,7 +86,9 @@ jobs: ./scripts/build/toolchain cargo install --path . --locked - name: Run Tests - run: eask run script test + run: | + eask install-deps + eask run script test build: name: Build From 6731452cf13dd1899825dfe3c5a47d192b1cf299 Mon Sep 17 00:00:00 2001 From: Muntasir Mahmud Date: Thu, 17 Jul 2025 17:15:03 +0600 Subject: [PATCH 08/13] Update emacs-checks.yml --- .github/workflows/emacs-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/emacs-checks.yml b/.github/workflows/emacs-checks.yml index 339ef761..54729b62 100644 --- a/.github/workflows/emacs-checks.yml +++ b/.github/workflows/emacs-checks.yml @@ -1,4 +1,4 @@ -name: Eask CI +name: Emacs Plugin CI on: push: From a6b311b7cf02933ff7ff26a669824897fb7528b8 Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Tue, 5 Aug 2025 20:47:37 +0600 Subject: [PATCH 09/13] fix --- emacs-tests/rustowl-test.el | 2 ++ rustowl.el | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/emacs-tests/rustowl-test.el b/emacs-tests/rustowl-test.el index 9befa4f3..25278308 100644 --- a/emacs-tests/rustowl-test.el +++ b/emacs-tests/rustowl-test.el @@ -141,6 +141,8 @@ (insert "abc\ndef\nghi") ;; Negative line/col should signal error (should-error (rustowl-line-col-to-pos -1 -1)) + ;; Test boundary case: (0, 0) should be valid per LSP protocol + (should-not (should-error (rustowl-line-col-to-pos 0 0))) ;; Line past end (should (= (rustowl-line-col-to-pos 100 0) (point-max))) ;; Col past end of line diff --git a/rustowl.el b/rustowl.el index 4cd8e1ce..cff84c83 100644 --- a/rustowl.el +++ b/rustowl.el @@ -28,7 +28,8 @@ (with-eval-after-load 'lsp-mode (lsp-register-client (make-lsp-client - :new-connection (lsp-stdio-connection '("rustowl")) + :new-connection + (lsp-stdio-connection '("rustowl")) :major-modes '(rust-mode) :server-id 'rustowl :priority -1 @@ -122,9 +123,17 @@ (cancel-timer rustowl-cursor-timer) (setq rustowl-cursor-timer nil))) +;;;###autoload +(defalias 'enable-rustowl-cursor 'rustowl-enable-cursor + "Backward compatibility alias for 'rustowl-enable-cursor'.") + +;;;###autoload +(defalias 'disable-rustowl-cursor 'rustowl-disable-cursor + "Backward compatibility alias for 'rustowl-disable-cursor'.") + (defun rustowl-line-col-to-pos (line col) "Convert LINE and COL to buffer position. -If LINE or COL is negative, signal an error. +LINE and COL are 0-based (LSP compatible); if either is negative (< 0), signal an error. If LINE is past the last line, return (point-max). If COL is past end of line, clamp to end of line." (when (or (< line 0) (< col 0)) From a580faedc2ef722514aa47da5951479b3fd6407c Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Tue, 5 Aug 2025 20:56:39 +0600 Subject: [PATCH 10/13] breaking change --- emacs-tests/rustowl-test.el | 2 +- rustowl.el | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/emacs-tests/rustowl-test.el b/emacs-tests/rustowl-test.el index 25278308..4ea6c9aa 100644 --- a/emacs-tests/rustowl-test.el +++ b/emacs-tests/rustowl-test.el @@ -142,7 +142,7 @@ ;; Negative line/col should signal error (should-error (rustowl-line-col-to-pos -1 -1)) ;; Test boundary case: (0, 0) should be valid per LSP protocol - (should-not (should-error (rustowl-line-col-to-pos 0 0))) + (should (rustowl-line-col-to-pos 0 0)) ;; Line past end (should (= (rustowl-line-col-to-pos 100 0) (point-max))) ;; Col past end of line diff --git a/rustowl.el b/rustowl.el index cff84c83..85611cc7 100644 --- a/rustowl.el +++ b/rustowl.el @@ -123,14 +123,6 @@ (cancel-timer rustowl-cursor-timer) (setq rustowl-cursor-timer nil))) -;;;###autoload -(defalias 'enable-rustowl-cursor 'rustowl-enable-cursor - "Backward compatibility alias for 'rustowl-enable-cursor'.") - -;;;###autoload -(defalias 'disable-rustowl-cursor 'rustowl-disable-cursor - "Backward compatibility alias for 'rustowl-disable-cursor'.") - (defun rustowl-line-col-to-pos (line col) "Convert LINE and COL to buffer position. LINE and COL are 0-based (LSP compatible); if either is negative (< 0), signal an error. From de8876c5f43355e8e3eac709cdf5d9f26cd17415 Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Sat, 9 Aug 2025 16:30:14 +0600 Subject: [PATCH 11/13] refactor(emacs)!: rename cursor api, update hooks, and harden overlays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Simplify rustowl-cursor to always issue async requests for better responsiveness; remove explicit lsp-mode/workspace guard • Rework rustowl-cursor-call to build params via plist for clarity and reduce boilerplate • Rename public autoloads to consistent rustowl-* names and update mode hooks accordingly • Make rustowl-underline robust by clamping to buffer bounds and handling reversed ranges to avoid overlay errors • Change add-hook/remove-hook usage to non-buffer-local, aligning with typical mode-level activation BREAKING CHANGE: • Function renames: • enable-rustowl-cursor -> rustowl-enable-cursor • disable-rustowl-cursor -> rustowl-disable-cursor • Hook behavior changed: post-command-hook is no longer added/removed buffer-locally from these helpers. If you relied on buffer-local hooks, pass a local arg in your own configuration or adapt your setup accordingly. --- rustowl.el | 112 +++++++++++++++++++++++++---------------------------- 1 file changed, 52 insertions(+), 60 deletions(-) diff --git a/rustowl.el b/rustowl.el index 250beabc..df9c6905 100644 --- a/rustowl.el +++ b/rustowl.el @@ -60,39 +60,38 @@ (add-hook 'rustic-mode-hook #'rustowl-enable-analyze-on-save) (defun rustowl-cursor (params) - "Send rustowl/cursor request if LSP is active in this buffer." - (when (and (bound-and-true-p lsp-mode) (lsp-workspaces)) - (lsp-request-async - "rustowl/cursor" params - (lambda (response) - (let ((decorations (gethash "decorations" response))) - (mapc - (lambda (deco) - (let* ((type (gethash "type" deco)) - (start (gethash "start" (gethash "range" deco))) - (end (gethash "end" (gethash "range" deco))) - (start-pos - (rustowl-line-col-to-pos - (gethash "line" start) - (gethash "character" start))) - (end-pos - (rustowl-line-col-to-pos - (gethash "line" end) (gethash "character" end))) - (overlapped (gethash "overlapped" deco))) - (when (not overlapped) - (cond - ((equal type "lifetime") - (rustowl-underline start-pos end-pos "#00cc00")) - ((equal type "imm_borrow") - (rustowl-underline start-pos end-pos "#0000cc")) - ((equal type "mut_borrow") - (rustowl-underline start-pos end-pos "#cc00cc")) - ((or (equal type "move") (equal type "call")) - (rustowl-underline start-pos end-pos "#cccc00")) - ((equal type "outlive") - (rustowl-underline start-pos end-pos "#cc0000")))))) - decorations))) - :mode 'current))) + "Request and visualize Rust ownership/lifetime overlays for PARAMS." + (lsp-request-async + "rustowl/cursor" params + (lambda (response) + (let ((decorations (gethash "decorations" response))) + (mapc + (lambda (deco) + (let* ((type (gethash "type" deco)) + (start (gethash "start" (gethash "range" deco))) + (end (gethash "end" (gethash "range" deco))) + (start-pos + (rustowl-line-col-to-pos + (gethash "line" start) + (gethash "character" start))) + (end-pos + (rustowl-line-col-to-pos + (gethash "line" end) (gethash "character" end))) + (overlapped (gethash "overlapped" deco))) + (unless overlapped + (cond + ((equal type "lifetime") + (rustowl-underline start-pos end-pos "#00cc00")) + ((equal type "imm_borrow") + (rustowl-underline start-pos end-pos "#0000cc")) + ((equal type "mut_borrow") + (rustowl-underline start-pos end-pos "#cc00cc")) + ((or (equal type "move") (equal type "call")) + (rustowl-underline start-pos end-pos "#cccc00")) + ((equal type "outlive") + (rustowl-underline start-pos end-pos "#cc0000")))))) + decorations))) + :mode 'current)) (defun rustowl-line-number-at-pos () "Return the line number at point." @@ -108,25 +107,14 @@ (- start (point))))) (defun rustowl-cursor-call () - (when (and (bound-and-true-p lsp-mode) (lsp-workspaces)) - (let* ((line (rustowl-line-number-at-pos)) - (column (rustowl-current-column)) - (uri (lsp--buffer-uri)) - (pos - (let ((ht (make-hash-table :test 'equal))) - (puthash "line" line ht) - (puthash "character" column ht) - ht)) - (doc - (let ((ht (make-hash-table :test 'equal))) - (puthash "uri" uri ht) - ht)) - (params - (let ((ht (make-hash-table :test 'equal))) - (puthash "position" pos ht) - (puthash "document" doc ht) - ht))) - (rustowl-cursor params)))) + "Call RustOwl for current cursor position." + (let ((line (rustowl-line-number-at-pos)) + (column (rustowl-current-column)) + (uri (lsp--buffer-uri))) + (rustowl-cursor + `(:position + (:line ,line :character ,column) + :document (:uri ,uri))))) ;;;###autoload (defvar rustowl-cursor-timer nil @@ -146,20 +134,22 @@ rustowl-cursor-timeout nil #'rustowl-cursor-call))) ;;;###autoload -(defun enable-rustowl-cursor () - (add-hook 'post-command-hook #'rustowl-reset-cursor-timer nil t)) +(defun rustowl-enable-cursor () + "Enable RustOwl overlay updates on cursor move." + (add-hook 'post-command-hook #'rustowl-reset-cursor-timer)) ;;;###autoload -(defun disable-rustowl-cursor () - (remove-hook 'post-command-hook #'rustowl-reset-cursor-timer t) +(defun rustowl-disable-cursor () + "Disable RustOwl overlay updates." + (remove-hook 'post-command-hook #'rustowl-reset-cursor-timer) (when rustowl-cursor-timer (cancel-timer rustowl-cursor-timer) (setq rustowl-cursor-timer nil))) ;; Automatically enable cursor-based highlighting for Rust buffers -(add-hook 'rust-mode-hook #'enable-rustowl-cursor) -(add-hook 'rust-ts-mode-hook #'enable-rustowl-cursor) -(add-hook 'rustic-mode-hook #'enable-rustowl-cursor) +(add-hook 'rust-mode-hook #'rustowl-enable-cursor) +(add-hook 'rust-ts-mode-hook #'rustowl-enable-cursor) +(add-hook 'rustic-mode-hook #'rustowl-enable-cursor) ;; RustOwl visualization (defun rustowl-line-col-to-pos (line col) @@ -186,7 +176,9 @@ If COL is past end of line, clamp to end of line." (defun rustowl-underline (start end color) "Underline region from START to END with COLOR." - (let ((overlay (make-overlay start end))) + (let* ((s (max (point-min) (min start end))) + (e (min (point-max) (max start end))) + (overlay (make-overlay s e))) (overlay-put overlay 'face `(:underline (:color ,color :style wave))) (push overlay rustowl-overlays) From f8f5304b55b0b6b7d79f94e72979d19c0267595d Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Sun, 10 Aug 2025 21:01:09 +0600 Subject: [PATCH 12/13] =?UTF-8?q?fix(emacs):=20add=20aliases=20and=20buffe?= =?UTF-8?q?r=E2=80=91local=20hooks;=20restore=20LSP=20guard;=20optimize=20?= =?UTF-8?q?line=20calc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make post-command hooks buffer-local to avoid global side effects - Restore LSP active/workspace check in `rustowl-cursor` - Optimize line count using `(line-number-at-pos (point-max))` --- rustowl.el | 69 +++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/rustowl.el b/rustowl.el index df9c6905..b38278b2 100644 --- a/rustowl.el +++ b/rustowl.el @@ -61,37 +61,38 @@ (defun rustowl-cursor (params) "Request and visualize Rust ownership/lifetime overlays for PARAMS." - (lsp-request-async - "rustowl/cursor" params - (lambda (response) - (let ((decorations (gethash "decorations" response))) - (mapc - (lambda (deco) - (let* ((type (gethash "type" deco)) - (start (gethash "start" (gethash "range" deco))) - (end (gethash "end" (gethash "range" deco))) - (start-pos - (rustowl-line-col-to-pos - (gethash "line" start) - (gethash "character" start))) - (end-pos - (rustowl-line-col-to-pos - (gethash "line" end) (gethash "character" end))) - (overlapped (gethash "overlapped" deco))) - (unless overlapped - (cond - ((equal type "lifetime") - (rustowl-underline start-pos end-pos "#00cc00")) - ((equal type "imm_borrow") - (rustowl-underline start-pos end-pos "#0000cc")) - ((equal type "mut_borrow") - (rustowl-underline start-pos end-pos "#cc00cc")) - ((or (equal type "move") (equal type "call")) - (rustowl-underline start-pos end-pos "#cccc00")) - ((equal type "outlive") - (rustowl-underline start-pos end-pos "#cc0000")))))) - decorations))) - :mode 'current)) + (when (and (bound-and-true-p lsp-mode) (lsp-workspaces)) + (lsp-request-async + "rustowl/cursor" params + (lambda (response) + (let ((decorations (gethash "decorations" response))) + (mapc + (lambda (deco) + (let* ((type (gethash "type" deco)) + (start (gethash "start" (gethash "range" deco))) + (end (gethash "end" (gethash "range" deco))) + (start-pos + (rustowl-line-col-to-pos + (gethash "line" start) + (gethash "character" start))) + (end-pos + (rustowl-line-col-to-pos + (gethash "line" end) (gethash "character" end))) + (overlapped (gethash "overlapped" deco))) + (unless overlapped + (cond + ((equal type "lifetime") + (rustowl-underline start-pos end-pos "#00cc00")) + ((equal type "imm_borrow") + (rustowl-underline start-pos end-pos "#0000cc")) + ((equal type "mut_borrow") + (rustowl-underline start-pos end-pos "#cc00cc")) + ((or (equal type "move") (equal type "call")) + (rustowl-underline start-pos end-pos "#cccc00")) + ((equal type "outlive") + (rustowl-underline start-pos end-pos "#cc0000")))))) + decorations))) + :mode 'current))) (defun rustowl-line-number-at-pos () "Return the line number at point." @@ -136,12 +137,12 @@ ;;;###autoload (defun rustowl-enable-cursor () "Enable RustOwl overlay updates on cursor move." - (add-hook 'post-command-hook #'rustowl-reset-cursor-timer)) + (add-hook 'post-command-hook #'rustowl-reset-cursor-timer nil t)) ;;;###autoload (defun rustowl-disable-cursor () "Disable RustOwl overlay updates." - (remove-hook 'post-command-hook #'rustowl-reset-cursor-timer) + (remove-hook 'post-command-hook #'rustowl-reset-cursor-timer t) (when rustowl-cursor-timer (cancel-timer rustowl-cursor-timer) (setq rustowl-cursor-timer nil))) @@ -161,7 +162,7 @@ If COL is past end of line, clamp to end of line." (error "rustowl-line-col-to-pos: negative line or column")) (save-excursion (goto-char (point-min)) - (let ((max-line (count-lines (point-min) (point-max)))) + (let ((max-line (line-number-at-pos (point-max)))) (if (>= line max-line) (point-max) (forward-line line) From 57191db3299b212546a165538278a651c8202d14 Mon Sep 17 00:00:00 2001 From: MuntasirSZN Date: Sun, 10 Aug 2025 21:40:04 +0600 Subject: [PATCH 13/13] fix: tests --- emacs-tests/rustowl-test.el | 132 +++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/emacs-tests/rustowl-test.el b/emacs-tests/rustowl-test.el index 4ea6c9aa..047376f5 100644 --- a/emacs-tests/rustowl-test.el +++ b/emacs-tests/rustowl-test.el @@ -97,21 +97,24 @@ (should (or (eq called 'cancelled) (eq called 'cleared))) (let ((added nil)) (cl-letf (((symbol-function 'add-hook) - (lambda (hook fn) (setq added (list hook fn))))) + (lambda (hook fn &optional _depth local) + (setq added (list hook fn local))))) (rustowl-enable-cursor) (should (equal added - '(post-command-hook rustowl-reset-cursor-timer)))))))) + '(post-command-hook rustowl-reset-cursor-timer t)))))))) ;; Test idempotency of rustowl-enable-cursor and rustowl-disable-cursor (ert-deftest rustowl-test-enable-disable-idempotent () (let ((add-count 0) (remove-count 0)) (cl-letf (((symbol-function 'add-hook) - (lambda (hook fn) (cl-incf add-count))) + (lambda (hook fn &optional _depth _local) + (cl-incf add-count))) ((symbol-function 'remove-hook) - (lambda (hook fn) (cl-incf remove-count)))) + (lambda (hook fn &optional _local) + (cl-incf remove-count)))) (rustowl-enable-cursor) (rustowl-enable-cursor) (should (>= add-count 2)) @@ -185,19 +188,22 @@ ht)))) (with-temp-buffer (insert "abcdef") - (cl-letf (((symbol-function 'lsp-request-async) - (lambda (_method _params cb &rest _args) - (funcall cb response) - (setq called t))) - ((symbol-function 'rustowl-underline) - (lambda (start end color) - (setq called (list start end color)) - (make-overlay start end)))) - (rustowl-cursor - '(:position - (:line 0 :character 0) - :document (:uri "file:///fake"))) - (should called))))) + (let ((lsp-mode t)) + (cl-letf (((symbol-function 'lsp-workspaces) + (lambda () '(fake-workspace))) + ((symbol-function 'lsp-request-async) + (lambda (_method _params cb &rest _args) + (funcall cb response) + (setq called t))) + ((symbol-function 'rustowl-underline) + (lambda (start end color) + (setq called (list start end color)) + (make-overlay start end)))) + (rustowl-cursor + '(:position + (:line 0 :character 0) + :document (:uri "file:///fake"))) + (should called)))))) ;; Test rustowl-cursor overlays for all type branches and overlapped (ert-deftest rustowl-test-cursor-overlays-all-types () @@ -234,51 +240,53 @@ ht))) (with-temp-buffer (insert "abcdef") - (cl-letf (((symbol-function 'lsp-request-async) - (lambda (_method _params cb &rest _args) - (funcall cb response))) - ((symbol-function 'rustowl-underline) - (lambda (_start _end color) - (push color called-types) - (make-overlay 1 2)))) - (rustowl-cursor - '(:position - (:line 0 :character 0) - :document (:uri "file:///fake"))) - ;; Should get all colors except for the overlapped one - (should (member "#00cc00" called-types)) ; lifetime - (should (member "#0000cc" called-types)) ; imm_borrow - (should (member "#cc00cc" called-types)) ; mut_borrow - (should (member "#cccc00" called-types)) ; move/call - (should (member "#cc0000" called-types)) ; outlive - ;; Should not call underline for overlapped - (should - (= (length - (cl-remove-if-not - (lambda (c) (equal c "#00cc00")) called-types)) - 1))))))) - -;; Test rustowl-cursor-call (mocking buffer and lsp) -(ert-deftest rustowl-test-cursor-call () - (let ((called nil)) - (with-temp-buffer - (insert "abc\ndef") - (goto-char (point-min)) - (cl-letf (((symbol-function 'rustowl-line-number-at-pos) - (lambda () 0)) - ((symbol-function 'rustowl-current-column) - (lambda () 1)) - ((symbol-function 'lsp--buffer-uri) - (lambda () "file:///fake")) - ((symbol-function 'rustowl-cursor) - (lambda (params) (setq called params)))) - (rustowl-cursor-call) - (should - (equal - called - '(:position - (:line 0 :character 1) - :document (:uri "file:///fake")))))))) + (let ((lsp-mode t)) + (cl-letf (((symbol-function 'lsp-workspaces) + (lambda () '(fake-workspace))) + ((symbol-function 'lsp-request-async) + (lambda (_method _params cb &rest _args) + (funcall cb response))) + ((symbol-function 'rustowl-underline) + (lambda (_start _end color) + (push color called-types) + (make-overlay 1 2)))) + (rustowl-cursor + '(:position + (:line 0 :character 0) + :document (:uri "file:///fake"))) + ;; Should get all colors except for the overlapped one + (should (member "#00cc00" called-types)) ; lifetime + (should (member "#0000cc" called-types)) ; imm_borrow + (should (member "#cc00cc" called-types)) ; mut_borrow + (should (member "#cccc00" called-types)) ; move/call + (should (member "#cc0000" called-types)) ; outlive + ;; Should not call underline for overlapped + (should + (= (length + (cl-remove-if-not + (lambda (c) (equal c "#00cc00")) called-types)) + 1))))))) + ;; Test rustowl-cursor-call (mocking buffer and lsp) + (ert-deftest rustowl-test-cursor-call () + (let ((called nil)) + (with-temp-buffer + (insert "abc\ndef") + (goto-char (point-min)) + (cl-letf (((symbol-function 'rustowl-line-number-at-pos) + (lambda () 0)) + ((symbol-function 'rustowl-current-column) + (lambda () 1)) + ((symbol-function 'lsp--buffer-uri) + (lambda () "file:///fake")) + ((symbol-function 'rustowl-cursor) + (lambda (params) (setq called params)))) + (rustowl-cursor-call) + (should + (equal + called + '(:position + (:line 0 :character 1) + :document (:uri "file:///fake"))))))))) (provide 'rustowl-test) ;;; rustowl-test.el ends here