- Add GPL-3.0 LICENSE file - Add checks.sh script for quality validation (checkdoc, package-lint, melpazoid) - Fix checkdoc issue: quote symbol in docstring - Remove redundant smerge-mode-hook (already handled by globalized mode) - Move configuration examples from example-config.el to README - Add advanced customization sections to README (lazy loading, terminal, Magit) - Update Contributing section with link to contribution guidelines - Update .gitignore to exclude melpazoid/ directory All MELPA quality checks now pass successfully.
240 lines
8.5 KiB
EmacsLisp
240 lines
8.5 KiB
EmacsLisp
;;; conflict-buttons.el --- Clickable buttons for smerge-mode conflicts -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2026 Andros Fenollosa
|
|
|
|
;; Author: Andros Fenollosa <hi@andros.dev>
|
|
;; Version: 0.1.0
|
|
;; Package-Requires: ((emacs "26.1"))
|
|
;; Keywords: vc, tools, convenience
|
|
;; URL: https://git.andros.dev/andros/conflict-buttons.el
|
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
;;; Commentary:
|
|
|
|
;; This package adds clickable inline buttons to smerge-mode conflict markers,
|
|
;; similar to VS Code's CodeLens feature. When a merge conflict is detected,
|
|
;; buttons appear above the conflict markers allowing one-click resolution.
|
|
;;
|
|
;; Available actions:
|
|
;; - Accept Current: keep the current branch's changes
|
|
;; - Accept Incoming: keep the incoming branch's changes
|
|
;; - Accept Both: keep both changes
|
|
;; - Compare: open ediff to compare versions
|
|
;;
|
|
;; The buttons appear automatically when smerge-mode is active.
|
|
;;
|
|
;; Usage:
|
|
;; (add-hook 'smerge-mode-hook #'conflict-buttons-mode)
|
|
;;
|
|
;; Or enable globally:
|
|
;; (conflict-buttons-global-mode 1)
|
|
|
|
;;; Code:
|
|
|
|
(require 'smerge-mode)
|
|
|
|
(defgroup conflict-buttons nil
|
|
"Clickable buttons for merge conflict resolution."
|
|
:group 'smerge
|
|
:prefix "conflict-buttons-")
|
|
|
|
(defcustom conflict-buttons-separator " | "
|
|
"Separator string between buttons."
|
|
:type 'string
|
|
:group 'conflict-buttons)
|
|
|
|
(defcustom conflict-buttons-style 'unicode
|
|
"Style for button rendering.
|
|
Options are:
|
|
`unicode' - use Unicode box drawing characters
|
|
`ascii' - use simple ASCII brackets"
|
|
:type '(choice (const :tag "Unicode" unicode)
|
|
(const :tag "ASCII" ascii))
|
|
:group 'conflict-buttons)
|
|
|
|
(defface conflict-buttons-button-face
|
|
'((t :inherit button :weight bold))
|
|
"Face for conflict resolution buttons."
|
|
:group 'conflict-buttons)
|
|
|
|
(defface conflict-buttons-button-hover-face
|
|
'((t :inherit highlight :weight bold))
|
|
"Face for conflict resolution buttons when mouse hovers over them."
|
|
:group 'conflict-buttons)
|
|
|
|
(defvar-local conflict-buttons--overlays nil
|
|
"List of overlays created by `conflict-buttons-mode'.")
|
|
|
|
(defvar-local conflict-buttons--timer nil
|
|
"Timer for delayed refresh of conflict buttons.")
|
|
|
|
(defvar conflict-buttons-mode) ;; Defined later by define-minor-mode
|
|
|
|
(defun conflict-buttons--create-button (label action)
|
|
"Create a propertized button string with LABEL to execute ACTION."
|
|
(let ((map (make-sparse-keymap))
|
|
(formatted-label (if (eq conflict-buttons-style 'unicode)
|
|
(format "[ %s ]" label)
|
|
(format "[%s]" label))))
|
|
(define-key map [mouse-1] action)
|
|
(define-key map (kbd "RET") action)
|
|
(propertize formatted-label
|
|
'face 'conflict-buttons-button-face
|
|
'mouse-face 'conflict-buttons-button-hover-face
|
|
'keymap map
|
|
'help-echo (format "Click to %s" label))))
|
|
|
|
(defun conflict-buttons--goto-conflict ()
|
|
"Move point to the nearest conflict marker."
|
|
(let ((start (point)))
|
|
;; Try to find conflict marker backwards first
|
|
(unless (re-search-backward "^<<<<<<< " nil t)
|
|
;; If not found backwards, try forwards
|
|
(goto-char start)
|
|
(unless (re-search-forward "^<<<<<<< " nil t)
|
|
(error "No conflict found")))
|
|
;; Position cursor inside the conflict (after the marker)
|
|
(forward-line 1)))
|
|
|
|
(defun conflict-buttons--keep-current ()
|
|
"Keep the current branch's version and refresh buttons."
|
|
(interactive)
|
|
(condition-case err
|
|
(progn
|
|
(save-excursion
|
|
(conflict-buttons--goto-conflict)
|
|
(smerge-keep-current))
|
|
(conflict-buttons--refresh))
|
|
(error (message "Error resolving conflict: %s" (error-message-string err)))))
|
|
|
|
(defun conflict-buttons--keep-other ()
|
|
"Keep the incoming branch's version and refresh buttons."
|
|
(interactive)
|
|
(condition-case err
|
|
(progn
|
|
(save-excursion
|
|
(conflict-buttons--goto-conflict)
|
|
(smerge-keep-lower))
|
|
(conflict-buttons--refresh))
|
|
(error (message "Error resolving conflict: %s" (error-message-string err)))))
|
|
|
|
(defun conflict-buttons--keep-all ()
|
|
"Keep both versions and refresh buttons."
|
|
(interactive)
|
|
(condition-case err
|
|
(progn
|
|
(save-excursion
|
|
(conflict-buttons--goto-conflict)
|
|
(smerge-keep-all))
|
|
(conflict-buttons--refresh))
|
|
(error (message "Error resolving conflict: %s" (error-message-string err)))))
|
|
|
|
(defun conflict-buttons--compare ()
|
|
"Open ediff to compare conflict versions."
|
|
(interactive)
|
|
(condition-case err
|
|
(progn
|
|
(save-excursion
|
|
(conflict-buttons--goto-conflict)
|
|
(call-interactively #'smerge-ediff))
|
|
;; Refresh after ediff in case the conflict was resolved
|
|
(run-with-idle-timer 0.5 nil
|
|
(let ((buf (current-buffer)))
|
|
(lambda ()
|
|
(when (buffer-live-p buf)
|
|
(with-current-buffer buf
|
|
(conflict-buttons--refresh)))))))
|
|
(error (message "Error comparing conflict: %s" (error-message-string err)))))
|
|
|
|
(defun conflict-buttons--create-buttons-string ()
|
|
"Create the complete button string with all actions."
|
|
(concat
|
|
(conflict-buttons--create-button "Accept Current" #'conflict-buttons--keep-current)
|
|
conflict-buttons-separator
|
|
(conflict-buttons--create-button "Accept Incoming" #'conflict-buttons--keep-other)
|
|
conflict-buttons-separator
|
|
(conflict-buttons--create-button "Accept Both" #'conflict-buttons--keep-all)
|
|
conflict-buttons-separator
|
|
(conflict-buttons--create-button "Compare" #'conflict-buttons--compare)
|
|
"\n"))
|
|
|
|
(defun conflict-buttons--add-overlay (pos)
|
|
"Add button overlay at position POS."
|
|
(let ((ov (make-overlay pos pos)))
|
|
(overlay-put ov 'before-string (conflict-buttons--create-buttons-string))
|
|
(overlay-put ov 'conflict-buttons t)
|
|
(push ov conflict-buttons--overlays)))
|
|
|
|
(defun conflict-buttons--remove-overlays ()
|
|
"Remove all conflict button overlays."
|
|
(mapc #'delete-overlay conflict-buttons--overlays)
|
|
(setq conflict-buttons--overlays nil))
|
|
|
|
(defun conflict-buttons--refresh ()
|
|
"Refresh conflict button overlays."
|
|
(when conflict-buttons-mode
|
|
(conflict-buttons--remove-overlays)
|
|
(conflict-buttons--install)))
|
|
|
|
(defun conflict-buttons--install ()
|
|
"Install button overlays on all conflict markers in the buffer."
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(while (re-search-forward "^<<<<<<< " nil t)
|
|
(conflict-buttons--add-overlay (line-beginning-position)))))
|
|
|
|
(defun conflict-buttons--after-change (_beg _end _len)
|
|
"Hook function to refresh buttons after the buffer is modified."
|
|
(when (and conflict-buttons-mode
|
|
(bound-and-true-p smerge-mode))
|
|
;; Cancel existing timer to avoid accumulation
|
|
(when conflict-buttons--timer
|
|
(cancel-timer conflict-buttons--timer))
|
|
;; Create new timer with buffer context
|
|
(let ((buf (current-buffer)))
|
|
(setq conflict-buttons--timer
|
|
(run-with-idle-timer 0.1 nil
|
|
(lambda ()
|
|
(when (buffer-live-p buf)
|
|
(with-current-buffer buf
|
|
(conflict-buttons--refresh)))))))))
|
|
|
|
;;;###autoload
|
|
(define-minor-mode conflict-buttons-mode
|
|
"Toggle display of clickable buttons for merge conflict resolution.
|
|
|
|
When enabled, clickable buttons appear above merge conflict markers,
|
|
allowing quick resolution with a single click. The buttons provide
|
|
actions to accept the current version, accept the incoming version,
|
|
accept both, or compare the versions using ediff."
|
|
:lighter " CB"
|
|
:group 'conflict-buttons
|
|
(if conflict-buttons-mode
|
|
(progn
|
|
(conflict-buttons--install)
|
|
(add-hook 'after-change-functions #'conflict-buttons--after-change nil t))
|
|
(conflict-buttons--remove-overlays)
|
|
;; Cancel pending timer
|
|
(when conflict-buttons--timer
|
|
(cancel-timer conflict-buttons--timer)
|
|
(setq conflict-buttons--timer nil))
|
|
(remove-hook 'after-change-functions #'conflict-buttons--after-change t)))
|
|
|
|
;;;###autoload
|
|
(define-globalized-minor-mode conflict-buttons-global-mode
|
|
conflict-buttons-mode
|
|
(lambda ()
|
|
(when (bound-and-true-p smerge-mode)
|
|
(conflict-buttons-mode 1)))
|
|
:group 'conflict-buttons)
|
|
|
|
;;;###autoload
|
|
(defun conflict-buttons-setup ()
|
|
"Setup conflict-buttons to activate with `smerge-mode'.
|
|
Add this to your init file:
|
|
(add-hook \\='smerge-mode-hook #\\='conflict-buttons-setup)"
|
|
(conflict-buttons-mode 1))
|
|
|
|
(provide 'conflict-buttons)
|
|
;;; conflict-buttons.el ends here
|