Skip to content

Commit d2f44b2

Browse files
committed
feat(file): add blob buffer support
- Add majutsu-file.el for viewing files at revisions - Support blob buffer navigation (prev/next change) - Smart diff visit behavior (worktree vs blob) - Line mapping between revisions via diff hunks - Add change-at-point detection for edit/new commands - Add evil keybindings for blob mode
1 parent 93a4317 commit d2f44b2

8 files changed

Lines changed: 569 additions & 27 deletions

File tree

docs/majutsu.org

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ Press ~d~ to open the Diff transient.
134134

135135
*** Diff Buffer
136136
The diff buffer is highly interactive:
137-
- ~RET~ :: Visit the file in your workspace at the corresponding line.
137+
- ~RET~ :: Visit the appropriate version of the file at point. For working copy diffs, visits the actual file. For committed changes, visits the blob.
138+
- ~C-j~ / ~C-<return>~ :: Always visit the workspace file, regardless of diff type.
138139
- ~+~ / ~-~ :: Increase or decrease the amount of context shown.
139140
- ~t~ :: Toggle word-level refinement.
140141

majutsu-base.el

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,38 @@ of the selected frame."
263263
(unless (derived-mode-p mode)
264264
(user-error "Command is only valid in %s buffers" mode)))
265265

266+
;;; Change at Point
267+
268+
(defvar majutsu-buffer-blob-revision)
269+
270+
(defun majutsu-change-at-point ()
271+
"Return the change-id at point.
272+
This checks multiple sources in order:
273+
1. Section value (jj-commit section)
274+
2. `jj-revision' thing-at-point
275+
3. Blob buffer revision
276+
4. Diff/revision buffer revision"
277+
(or (magit-section-value-if 'jj-commit)
278+
(magit-thing-at-point 'jj-revision t)
279+
(and (bound-and-true-p majutsu-buffer-blob-revision)
280+
majutsu-buffer-blob-revision)
281+
(and (derived-mode-p 'majutsu-diff-mode)
282+
(bound-and-true-p majutsu-buffer-diff-range)
283+
(let ((range majutsu-buffer-diff-range))
284+
(or (and (equal (car range) "-r") (cadr range))
285+
(cdr (assoc "--revisions=" range)))))))
286+
287+
;; Register jj-revision as a thing-at-point type
288+
(put 'jj-revision 'bounds-of-thing-at-point
289+
(lambda ()
290+
(when (looking-at "[a-z]\\{1,12\\}")
291+
(cons (match-beginning 0) (match-end 0)))))
292+
293+
(put 'jj-revision 'thing-at-point
294+
(lambda ()
295+
(when-let* ((bounds (bounds-of-thing-at-point 'jj-revision)))
296+
(buffer-substring-no-properties (car bounds) (cdr bounds)))))
297+
266298
;;; _
267299
(provide 'majutsu-base)
268300
;;; majutsu-base.el ends here

majutsu-diff.el

Lines changed: 131 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
(require 'majutsu-config)
2222
(require 'majutsu-selection)
2323
(require 'majutsu-section)
24+
(require 'majutsu-file)
2425
(require 'magit-diff) ; for faces/font-lock keywords
2526
(require 'diff-mode)
2627
(require 'smerge-mode)
@@ -747,31 +748,146 @@ works with the simplified jj diff we render here."
747748
(when-let* ((file (majutsu-diff--file-at-point)))
748749
(find-file (expand-file-name file default-directory))))
749750

751+
(defun majutsu-diff--range-value (range prefix)
752+
"Return the value in RANGE for argument starting with PREFIX."
753+
(when range
754+
(when-let* ((arg (seq-find (lambda (item) (string-prefix-p prefix item)) range)))
755+
(substring arg (length prefix)))))
756+
757+
(defun majutsu-diff--on-removed-line-p ()
758+
"Return non-nil if point is on a removed diff line."
759+
(eq (char-after (line-beginning-position)) ?-))
760+
761+
(defun majutsu-diff--default-revset ()
762+
"Return the revset implied by the current diff buffer."
763+
(let* ((range majutsu-buffer-diff-range)
764+
(removed (majutsu-diff--on-removed-line-p))
765+
(from (majutsu-diff--range-value range "--from="))
766+
(to (majutsu-diff--range-value range "--to="))
767+
(revisions (majutsu-diff--range-value range "--revisions=")))
768+
(cond
769+
((and range (equal (car range) "-r") (cadr range)) (cadr range))
770+
(revisions revisions)
771+
(from (if (and removed from) from (or to from)))
772+
(t "@"))))
773+
774+
(defun majutsu-diff--hunk-line (section goto-from)
775+
"Return the line number in SECTION for GOTO-FROM side."
776+
(with-slots (content from-range to-range) section
777+
(let ((start (car (if goto-from from-range to-range))))
778+
(when start
779+
(let ((line start)
780+
(target (point)))
781+
(save-excursion
782+
(goto-char content)
783+
(while (< (point) target)
784+
(let ((ch (char-after (line-beginning-position))))
785+
(cond
786+
((eq ch ?+) (unless goto-from (setq line (1+ line))))
787+
((eq ch ?-) (when goto-from (setq line (1+ line))))
788+
(t (setq line (1+ line)))))
789+
(forward-line 1)))
790+
line)))))
791+
792+
(defun majutsu-diff--hunk-column (section goto-from)
793+
"Return the column for SECTION based on GOTO-FROM side."
794+
(let ((bol (line-beginning-position)))
795+
(if (or (< (point) (oref section content))
796+
(and (not goto-from) (eq (char-after bol) ?-)))
797+
0
798+
(let ((col (current-column)))
799+
(if (memq (char-after bol) '(?+ ?-))
800+
(max 0 (1- col))
801+
col)))))
802+
803+
(defun majutsu-diff--goto-line-col (buffer line col)
804+
"Move point in BUFFER to LINE and COL."
805+
(with-current-buffer buffer
806+
(widen)
807+
(goto-char (point-min))
808+
(forward-line (max 0 (1- line)))
809+
(move-to-column col)))
810+
811+
(defun majutsu-diff--visit-workspace-p ()
812+
"Return non-nil if the current diff should visit the workspace file.
813+
This is true when diffing the working copy (@) on the new/right side."
814+
(let* ((range majutsu-buffer-diff-range)
815+
(to (majutsu-diff--range-value range "--to="))
816+
(revisions (majutsu-diff--range-value range "--revisions=")))
817+
(cond
818+
;; Explicit --to=@ means we're looking at working copy changes
819+
((equal to "@") t)
820+
;; No range specified defaults to -r @ (working copy)
821+
((null range) t)
822+
;; Single revision diff (-r @) shows working copy
823+
((and revisions (equal revisions "@")) t)
824+
;; Otherwise we're looking at committed changes
825+
(t nil))))
826+
750827
;;;###autoload
751-
(defun majutsu-diff-visit-file ()
752-
"Visit the file at point.
828+
(defun majutsu-diff-visit-file (&optional force-workspace)
829+
"From a diff, visit the appropriate version of the file at point.
753830
754-
When point is on a hunk section, jump to the corresponding line in the
755-
file."
756-
(interactive)
757-
(let ((section (magit-current-section)))
758-
(cond
759-
((and section (magit-section-match 'jj-hunk section))
760-
(majutsu-goto-diff-line))
761-
((majutsu-diff--file-at-point)
762-
(majutsu-visit-file))
763-
(t
764-
(user-error "No file at point")))))
831+
If point is on an added or context line, visit the new/right side.
832+
If point is on a removed line, visit the old/left side.
833+
834+
For diffs of the working copy (@), this visits the actual file in
835+
the workspace. For diffs of committed changes, this visits the
836+
blob from the appropriate revision.
837+
838+
With prefix argument FORCE-WORKSPACE, always visit the workspace file
839+
regardless of what the diff is about."
840+
(interactive "P")
841+
(let* ((section (magit-current-section))
842+
(file (majutsu-diff--file-at-point)))
843+
(unless file
844+
(user-error "No file at point"))
845+
(let* ((goto-from (and section (magit-section-match 'jj-hunk section)
846+
(majutsu-diff--on-removed-line-p)))
847+
(goto-workspace (or force-workspace
848+
(and (majutsu-diff--visit-workspace-p)
849+
(not goto-from))))
850+
(line (and section (magit-section-match 'jj-hunk section)
851+
(majutsu-diff--hunk-line section goto-from)))
852+
(col (and section (magit-section-match 'jj-hunk section)
853+
(majutsu-diff--hunk-column section goto-from))))
854+
(if goto-workspace
855+
;; Visit workspace file
856+
(let ((full-path (expand-file-name file default-directory)))
857+
(if (file-exists-p full-path)
858+
(progn
859+
(find-file full-path)
860+
(when (and line col)
861+
(goto-char (point-min))
862+
(forward-line (1- line))
863+
(move-to-column col)))
864+
(user-error "File does not exist in workspace: %s" file)))
865+
;; Visit blob
866+
(let* ((revset (majutsu-diff--default-revset))
867+
(buf (majutsu-find-file revset file)))
868+
(when (and buf line col)
869+
(majutsu-diff--goto-line-col buf line col)))))))
765870

766871
;;; Section Keymaps
767872

768873
(defvar-keymap majutsu-diff-section-map
769874
:doc "Keymap for diff sections."
770-
"<remap> <majutsu-visit-thing>" #'majutsu-diff-visit-file)
875+
"<remap> <majutsu-visit-thing>" #'majutsu-diff-visit-file
876+
"C-j" #'majutsu-diff-visit-workspace-file
877+
"C-<return>" #'majutsu-diff-visit-workspace-file)
878+
879+
;;;###autoload
880+
(defun majutsu-diff-visit-workspace-file ()
881+
"From a diff, visit the workspace version of the file at point.
882+
Always visits the actual file in the working tree, regardless of
883+
what the diff is about."
884+
(interactive)
885+
(majutsu-diff-visit-file t))
771886

772887
(defvar-keymap majutsu-file-section-map
773888
:doc "Keymap for `jj-file' sections."
774-
:parent majutsu-diff-section-map)
889+
:parent majutsu-diff-section-map
890+
"v" #'majutsu-find-file-at-point)
775891

776892
(defvar-keymap majutsu-hunk-section-map
777893
:doc "Keymap for `jj-hunk' sections."

majutsu-edit.el

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@
3838
3939
With prefix ARG, pass --ignore-immutable."
4040
(interactive "P")
41-
(when-let* ((revset (magit-section-value-if 'jj-commit))
42-
(args (append (list "edit" revset)
43-
(when arg (list "--ignore-immutable")))))
44-
(when (zerop (apply #'majutsu-run-jj args))
45-
(message "Now editing commit %s" revset))))
41+
(if-let* ((revset (majutsu-change-at-point))
42+
(args (append (list "edit" revset)
43+
(when arg (list "--ignore-immutable")))))
44+
(when (zerop (apply #'majutsu-run-jj args))
45+
(message "Now editing commit %s" revset))
46+
(user-error "No revision at point")))
4647

4748
;;; _
4849
(provide 'majutsu-edit)

majutsu-evil.el

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,28 @@ If KEYMAP is not yet bound, defer binding until it becomes available."
8282
majutsu-diff-mode))
8383
(evil-set-initial-state mode majutsu-evil-initial-state))))
8484

85+
(defun majutsu-evil--adjust-section-bindings ()
86+
"Unbind C-j from section maps so Evil navigation takes precedence.
87+
This mirrors `evil-collection-magit-adjust-section-bindings'."
88+
(when (boundp 'majutsu-diff-section-map)
89+
(define-key majutsu-diff-section-map "\C-j" nil))
90+
(when (boundp 'majutsu-file-section-map)
91+
(define-key majutsu-file-section-map "\C-j" nil))
92+
(when (boundp 'majutsu-hunk-section-map)
93+
(define-key majutsu-hunk-section-map "\C-j" nil)))
94+
8595
(defun majutsu-evil--define-mode-keys ()
8696
"Install Evil keybindings for Majutsu maps."
97+
;; Unbind C-j from section maps first.
98+
(majutsu-evil--adjust-section-bindings)
8799
;; Normal/visual/motion share the same bindings for navigation commands.
88100
(majutsu-evil--define-keys '(normal visual motion) 'majutsu-mode-map
101+
(kbd "C-j") #'magit-section-forward
102+
(kbd "C-k") #'magit-section-backward
103+
(kbd "g j") #'magit-section-forward-sibling
104+
(kbd "g k") #'magit-section-backward-sibling
105+
(kbd "]") #'magit-section-forward-sibling
106+
(kbd "[") #'magit-section-backward-sibling
89107
(kbd "R") #'majutsu-restore
90108
(kbd "g r") #'majutsu-refresh
91109
(kbd "`") #'majutsu-process-buffer
@@ -114,7 +132,17 @@ If KEYMAP is not yet bound, defer binding until it becomes available."
114132
(kbd "Y") #'majutsu-duplicate-dwim)
115133

116134
(majutsu-evil--define-keys '(normal visual) 'majutsu-diff-mode-map
117-
(kbd "g d") #'majutsu-jump-to-diffstat-or-diff)
135+
(kbd "g d") #'majutsu-jump-to-diffstat-or-diff
136+
(kbd "C-<return>") #'majutsu-diff-visit-workspace-file)
137+
138+
;; majutsu-blob-mode is a minor mode, need hook + define-keys
139+
(add-hook 'majutsu-blob-mode-hook #'evil-normalize-keymaps)
140+
(majutsu-evil--define-keys '(normal visual motion) 'majutsu-blob-mode-map
141+
(kbd "p") #'majutsu-blob-previous
142+
(kbd "n") #'majutsu-blob-next
143+
(kbd "q") #'majutsu-blob-quit
144+
;; RET visits the revision (edit)
145+
(kbd "RET") #'majutsu-edit-changeset)
118146

119147
(majutsu-evil--define-keys '(normal visual motion) 'majutsu-log-mode-map
120148
(kbd ".") #'majutsu-log-goto-@

0 commit comments

Comments
 (0)