]> git.eshelyaron.com Git - dict.git/commitdiff
ADDED: Support setting breakpoints in sweeprolog-mode
authorEshel Yaron <me@eshelyaron.com>
Thu, 16 Feb 2023 12:54:44 +0000 (14:54 +0200)
committerEshel Yaron <me@eshelyaron.com>
Thu, 16 Feb 2023 12:54:44 +0000 (14:54 +0200)
* sweep.pl (sweep_set_breakpoint/2)
(sweep_delete_breakpoint/2)
(sweep_set_breakpoint_condition/2): new predicates.

* sweeprolog.el (sweeprolog-dependency-directive): update
package-version field.
(sweeprolog-set-breakpoint)
(sweeprolog-set-breakpoint-condition)
(sweeprolog-delete-breakpoint)
(sweeprolog-list-breakpoints): new commands.
(sweeprolog-highlight-breakpoints): new user option.
(sweeprolog-mode-map): bind sweeprolog-set-breakpoint.

* README.org (Setting Breakpoints): new manual section.

README.org
sweep.pl
sweeprolog.el

index 876534285a69dfc7c974862529b63859a383271d..31b8fe1750b7d9288b9c908b077dd3a6a52e0e7c 100644 (file)
@@ -1316,6 +1316,71 @@ information about the mode line.
 More relevant information about loading code in SWI-Prolog can be
 found in [[https://www.swi-prolog.org/pldoc/man?section=consulting][Loading Prolog source files]] in the SWI-Prolog manual.
 
+** Setting Breakpoints
+:PROPERTIES:
+:CUSTOM_ID: breakpoints
+:DESCRIPTION: Commands for setting breakpoints in Prolog buffers
+:ALT_TITLE: Setting Breakpoints
+:END:
+
+#+CINDEX: breakpoints
+You can set /breakpoints/ in ~sweeprolog-mode~ buffers to have SWI-Prolog
+break before specific goals in the code (see [[https://www.swi-prolog.org/pldoc/man?section=trace-breakpoints][Breakpoints]] in the
+SWI-Prolog manual).
+
+#+FINDEX: sweeprolog-set-breakpoint
+- Key: C-c C-b (sweeprolog-set-breakpoint) :: Set a breakpoint.
+#+VINDEX: sweeprolog-highlight-breakpoints
+- User Option: sweeprolog-highlight-breakpoints :: If non-nil,
+  highlight breakpoints in ~sweeprolog-mode~ buffers.  Defaults to ~t~.
+
+The command ~sweeprolog-set-breakpoint~, bound to ~C-c C-b~, sets a
+breakpoint at the position of the cursor.  If you call it with a
+positive prefix argument (e.g. ~C-u C-c C-b~), it creates a conditional
+breakpoint with a condition goal that you insert in the minibuffer.
+If you call it with a non-positive prefix argument (e.g. ~C-0 C-c C-b~),
+it deletes the breakpoint at point instead.
+
+When Context Menu mode is enabled, you can also create and delete
+breakpoints in ~sweeprolog-mode~ buffers through right-click context
+menus (see [[#context-menu][Context Menu]]).
+
+By default, Sweep highlights terms with active breakpoints in
+~sweeprolog-mode~ buffers.  To inhibit breakpoint highlighting,
+customize the user option ~sweeprolog-highlight-breakpoints~ to ~nil~.
+
+*** Breakpoint Menu
+:PROPERTIES:
+:CUSTOM_ID: breakpoint-menu
+:DESCRIPTION: Special mode for managing breakpoints
+:ALT_TITLE: Breakpoint Menu
+:END:
+
+Sweep provides a /breakpoint menu/ that lets you manage breakpoints
+across your codebase.
+
+#+FINDEX: sweeprolog-list-breakpoints
+- Command: sweeprolog-list-breakpoints :: Display a list of active
+  breakpoints.
+
+To open the breakpoint menu, type ~M-x sweeprolog-list-breakpoints~.
+This command opens the breakpoint menu in the =*Sweep Breakpoints*=
+buffer.  The major mode of this buffer is Sweep Breakpoint Menu,
+which is a special mode that includes useful commands for managing
+Prolog breakpoints:
+
+#+FINDEX: sweeprolog-breakpoint-menu-find
+- Key: RET (sweeprolog-breakpoint-menu-find) :: Go to the position of
+  the breakpoint corresponding to the breakpoint menu entry at point.
+#+FINDEX: sweeprolog-breakpoint-menu-find-other-window
+- Key: o (sweeprolog-breakpoint-menu-find-other-window) :: Show the
+  position of the breakpoint corresponding to the breakpoint menu
+  entry at point, in another window.
+#+FINDEX: sweeprolog-breakpoint-menu-set-condition
+- Key: c (sweeprolog-breakpoint-menu-set-condition) :: Set the
+  condition goal for the breakpoint corresponding to the breakpoint
+  menu entry at point.
+
 ** Creating New Modules
 :PROPERTIES:
 :CUSTOM_ID: creating-new-modules
@@ -2336,6 +2401,7 @@ The full list of keybindings in ~sweeprolog-prefix-map~ is given below:
 
 | Key | Command                              | Documentation                     |
 |-----+--------------------------------------+-----------------------------------|
+| ~B~   | ~sweeprolog-list-breakpoints~          | [[#breakpoint-menu][Breakpoint Menu]]                   |
 | ~F~   | ~sweeprolog-set-prolog-flag~           | [[*Setting Prolog flags][Setting Prolog Flags]]              |
 | ~P~   | ~sweeprolog-pack-install~              | [[*Installing Prolog packages][Installing Prolog packages]]        |
 | ~R~   | ~sweeprolog-restart~                   | [[#prolog-init][Prolog Initialization and Cleanup]] |
index 046e7eb45718a7c60fb2375d8265facb08c2b8ee..71c63feefad855ac01e00450030f2d3b395f58c0 100644 (file)
--- a/sweep.pl
+++ b/sweep.pl
             sweep_predicate_dependencies/2,
             sweep_async_goal/2,
             sweep_interrupt_async_goal/2,
-            sweep_source_file_load_time/2
+            sweep_source_file_load_time/2,
+            sweep_set_breakpoint/2,
+            sweep_set_breakpoint_condition/2,
+            sweep_delete_breakpoint/2,
+            sweep_current_breakpoints/2,
+            sweep_current_breakpoints_in_region/2,
+            sweep_breakpoint_range/2,
+            sweep_breakpoint_file/2
           ]).
 
 :- use_module(library(pldoc)).
@@ -1234,3 +1241,55 @@ sweep_start_async_goal(Caller, Cookie, Goal, FD) :-
 
 sweep_interrupt_async_goal(TId, TId) :-
     thread_signal(TId, throw(interrupted)).
+
+sweep_set_breakpoint([File0,Line,Char], Id) :-
+    atom_string(File, File0),
+    set_breakpoint(File, Line, Char, Id).
+
+sweep_set_breakpoint_condition([Id|Cond], _) :-
+    set_breakpoint_condition(Id, Cond).
+
+sweep_delete_breakpoint(Id, _) :-
+    delete_breakpoint(Id).
+
+sweep_current_breakpoints(_, BPs) :-
+    findall(BP-Claue,
+            breakpoint_property(BP, clause(Claue)),
+            BPs0),
+    maplist(format_breakpoint, BPs0, BPs).
+
+format_breakpoint(Id-Clause, [["id"|Id],["predicate"|Pred],["clause"|ClauseNum]|BP]) :-
+    clause_property(Clause, predicate(Pred0)),
+    term_string(Pred0, Pred),
+    pi_head(Pred0, Head),
+    nth_clause(Head, ClauseNum, Clause),
+    findall(Prop, breakpoint_property(Id, Prop), Props),
+    convlist(format_breakpoint_property, Props, BP).
+
+format_breakpoint_property(file(File0), ["file"|File]) :-
+    atom_string(File0, File).
+format_breakpoint_property(line_count(Line), ["line"|Line]).
+format_breakpoint_property(character_range(Start0, Len), ["range",Start,End]) :-
+    Start is Start0 + 1, End is Start + Len.
+format_breakpoint_property(condition(Cond), ["condition"|Cond]).
+
+sweep_current_breakpoints_in_region([Path0, Beg, End], BPs) :-
+    atom_string(Path, Path0),
+    findall([BPBeg|BPEnd],
+            (   breakpoint_property(BPId, file(Path)),
+                breakpoint_property(BPId, character_range(BPBeg0, Len)),
+                BPBeg is BPBeg0 + 1,
+                Beg =< BPBeg,
+                BPBeg =< End,
+                BPEnd is BPBeg + Len
+            ),
+            BPs).
+
+sweep_breakpoint_range(Id, [Beg|End]) :-
+    breakpoint_property(Id, character_range(Beg0, Len)),
+    Beg is Beg0 + 1,
+    End is Beg + Len.
+
+sweep_breakpoint_file(Id, File) :-
+    breakpoint_property(Id, file(File0)),
+    atom_string(File0, File).
index fd5dc50a3785ae78bcdab0bf1086042adac45dd7..6bf7e5afb09b03a0de6702a51d85f7855d6eaee6 100644 (file)
@@ -411,17 +411,23 @@ directives.  If set to the symbol `infer', then
 unless the buffer already contains dependency directives and all
 of them are `use_module/2' directives.  Any other values means to
 use `autoload/2' for all added directives."
-  :package-version '((sweeprolog "0.16.1"))
+  :package-version '((sweeprolog "0.17.0"))
   :type '(choice (const :tag "Prefer use_module/2" use-module)
                  (const :tag "Prefer autoload/2"  autoload)
                  (const :tag "Infer" infer))
   :group 'sweeprolog)
 
+(defcustom sweeprolog-highlight-breakpoints t
+  "If non-nil, highlight breakpoints with a dedicated face."
+  :package-version '((sweeprolog "0.17.0"))
+  :type 'boolean
+  :group 'sweeprolog)
 
 ;;;; Keymaps
 
 (defvar sweeprolog-mode-map
   (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "C-c C-b") #'sweeprolog-set-breakpoint)
     (define-key map (kbd "C-c C-c") #'sweeprolog-analyze-buffer)
     (define-key map (kbd "C-c C-d") #'sweeprolog-document-predicate-at-point)
     (define-key map (kbd "C-c C-e") #'sweeprolog-export-predicate)
@@ -489,6 +495,7 @@ use `autoload/2' for all added directives."
 ;;;###autoload
 (defvar sweeprolog-prefix-map
   (let ((map (make-sparse-keymap)))
+    (define-key map "B" #'sweeprolog-list-breakpoints)
     (define-key map "F" #'sweeprolog-set-prolog-flag)
     (define-key map "P" #'sweeprolog-pack-install)
     (define-key map "R" #'sweeprolog-restart)
@@ -546,6 +553,12 @@ use `autoload/2' for all added directives."
     [ "Set Prolog Flag" sweeprolog-set-prolog-flag t ]
     [ "Install Prolog Package" sweeprolog-pack-install t ]
     "--"
+    [ "Set Breakpoint" sweeprolog-set-breakpoint
+      (derived-mode-p 'sweeprolog-mode) ]
+    [ "Delete Breakpoint" sweeprolog-delete-breakpoint
+      (sweeprolog-current-breakpoints) ]
+    [ "List Breakpoints" sweeprolog-list-breakpoints t ]
+    "--"
     [ "Open Top-level" sweeprolog-top-level t ]
     [ "Signal Top-level"
       sweeprolog-top-level-signal
@@ -2327,7 +2340,12 @@ resulting list even when found in the current clause."
                                               (current-buffer)
                                               nil)
                 (goto-char hend)
-                (setq hole (sweeprolog--next-hole))))))))))
+                (setq hole (sweeprolog--next-hole)))))))))
+  (when (and sweeprolog-highlight-breakpoints
+             (sweeprolog-buffer-loaded-since-last-modification-p))
+    (with-silent-modifications
+      (dolist (bp (sweeprolog-current-breakpoints-in-region beg end))
+        (sweeprolog--highlight-breakpoint (car bp) (cdr bp))))))
 
 (defun sweeprolog--help-echo-for-comment (kind)
   (pcase kind
@@ -5438,6 +5456,9 @@ GOAL."
 (defvar sweeprolog-context-menu-variable-at-click nil
   "Prolog variable at mouse click.")
 
+(defvar sweeprolog-context-menu-breakpoints-at-click nil
+  "Prolog breakpoints at mouse click.")
+
 (defun sweeprolog-context-menu-find-module ()
   "Find Prolog module at mouse click."
   (interactive)
@@ -5492,6 +5513,27 @@ GOAL."
    sweeprolog-context-menu-point-at-click
    sweeprolog-context-menu-variable-at-click))
 
+(defun sweeprolog-breakpoint-context-menu-set ()
+  "Set breakpoint at click."
+  (interactive)
+  (sweeprolog-set-breakpoint sweeprolog-context-menu-point-at-click))
+
+(defun sweeprolog-breakpoint-context-menu-delete ()
+  "Delete breakpoints at click."
+  (interactive)
+  (dolist (id sweeprolog-context-menu-breakpoints-at-click)
+    (sweeprolog-delete-breakpoint id))
+  (let ((n (length sweeprolog-context-menu-breakpoints-at-click)))
+   (message "Deleted %d %s" n
+            (ngettext "breakpoint" "breakpoints" n))))
+
+(defun sweeprolog-breakpoint-context-menu-set-condition ()
+  "Set condition goal for the breakpoint at click."
+  (interactive)
+  (let ((id (car sweeprolog-context-menu-breakpoints-at-click))
+        (cond (sweeprolog-read-breakpoint-condition)))
+    (sweeprolog-set-breakpoint-condition id cond)))
+
 (defun sweeprolog-context-menu-for-predicate (menu tok _beg _end _point)
   "Extend MENU with predicate-related commands if TOK describes one."
   (pcase tok
@@ -5581,8 +5623,43 @@ POINT is the buffer position of the mouse click."
                                              sweeprolog-context-menu-variable-at-click))
                              :keys "\\[sweeprolog-rename-variable]")))))
 
+(defun sweeprolog-context-menu-for-clause (menu tok _beg _end point)
+  "Extend MENU with clause-related commands if TOK specifies one.
+POINT is the buffer position of the mouse click."
+  (pcase tok
+    ((or "clause"
+         "grammar_rule")
+     (when-let ((file (buffer-file-name))
+                (submenu (make-sparse-keymap (propertize "Breakpoint"))))
+       (if-let ((bps-at-point
+                 (sweeprolog-breakpoints-at-point file point))
+                (ids (mapcar (lambda (bp)
+                               (alist-get "id" bp nil nil #'string=))
+                             bps-at-point)))
+           (progn
+             (setq sweeprolog-context-menu-breakpoints-at-click ids)
+             (define-key-after submenu [sweeprolog-delete-breakpoint]
+               `(menu-item "Delete"
+                           sweeprolog-breakpoint-context-menu-delete
+                           :help "Delete this breakpoint"
+                           :keys "\\[negative-argument] \\[sweeprolog-set-breakpoint]"))
+             (define-key-after submenu [sweeprolog-set-breakpoint-condition]
+               `(menu-item "Set condition"
+                           sweeprolog-breakpoint-context-menu-set-condition
+                           :help "Set condition goal for this breakpoint"
+                           :keys "\\[universal-argument] \\[sweeprolog-set-breakpoint]")))
+         (setq sweeprolog-context-menu-point-at-click point)
+         (define-key-after submenu [sweeprolog-set-breakpoint]
+           `(menu-item "Set"
+                       sweeprolog-breakpoint-context-menu-set
+                       :help "Set breakpoint"
+                       :keys "\\[sweeprolog-set-breakpoint]")))
+       (define-key-after menu [sweeprolog-breakpoint]
+         `(menu-item "Breakpoint" ,submenu))))))
+
 (defvar sweeprolog-context-menu-functions
-  '(sweeprolog-context-menu-for-file
+  '(sweeprolog-context-menu-for-clause
+    sweeprolog-context-menu-for-file
     sweeprolog-context-menu-for-module
     sweeprolog-context-menu-for-predicate
     sweeprolog-context-menu-for-variable)
@@ -6022,6 +6099,287 @@ numeric prefix argument (1 without prefix argument)."
    1 (point) sweeprolog-increment-numbered-variables-last-result))
 
 
+;;;; Breakpoints
+
+(defun sweeprolog-current-breakpoints ()
+  "Return the list for current Prolog breakpoints.
+Each breakpoint is represented as an alist with string keys."
+  (sweeprolog--query-once "sweep" "sweep_current_breakpoints" nil))
+
+(defun sweeprolog-current-breakpoints-in-region (beg end &optional buf)
+  "Return breakpoints that start between BEG an END in BUF.
+If BUF is nil, it defaults to the current buffer.
+
+The return value is an alist of elements (BPBEG . BPEND) where
+BPBEG is the start position of the breakpoint and BPEND is its
+end.  list for current Prolog breakpoints."
+  (setq buf (or buf (current-buffer)))
+  (with-current-buffer buf
+    (sweeprolog--query-once "sweep" "sweep_current_breakpoints_in_region"
+                            (list (or (buffer-file-name)
+                                      (expand-file-name (buffer-name)))
+                                  beg end))))
+
+(defun sweeprolog-read-breakpoint (&optional prompt)
+  "Read a Prolog breakpoint id in the minibuffer, with completion.
+If PROMPT is non-nil, use it as the minibuffer prompt, otherwise
+prompt with \"Breakpoint: \"."
+  (let* ((bps (sweeprolog-current-breakpoints))
+         (col (mapcar (lambda (bp)
+                        (let ((id (alist-get "id" bp nil nil #'string=))
+                              (file (alist-get "file" bp nil nil #'string=))
+                              (line (alist-get "line" bp nil nil #'string=)))
+                          (cons (number-to-string id)
+                                (format "%s%s:%d"
+                                        (make-string (- 4 (floor (log id 10))) ? )
+                                        file line))))
+                      bps))
+         (current-file (buffer-file-name))
+         (current-line (line-number-at-pos (point)))
+         (def
+          (mapcar (lambda (bp)
+                    (number-to-string (alist-get "id" bp nil nil #'string=)))
+                  (seq-filter (lambda (bp)
+                                (and (string= (expand-file-name (alist-get "file" bp nil nil #'string=))
+                                              current-file)
+                                     (= (alist-get "line" bp nil nil #'string=)
+                                        current-line)))
+                              bps)))
+         (completion-extra-properties
+          (list :annotation-function
+                (lambda (key)
+                  (alist-get key col nil nil #'string=)))))
+    (string-to-number
+     (completing-read (concat (or prompt "Breakpoint")
+                              (when def (concat " (default " (car def) ")"))
+                              ": ")
+                      col nil t nil nil def))))
+
+(defun sweeprolog-read-breakpoint-condition ()
+  "Read a Prolog breakpoint condition in the minibuffer."
+  (sweeprolog-read-goal "[breakpoint condition] ?- "))
+
+(defun sweeprolog-set-breakpoint-condition (id cond)
+  "Attach condition goal COND to the breakpoint with id ID."
+  (interactive (list (sweeprolog-read-breakpoint "Set condition for breakpoint")
+                     (sweeprolog-read-breakpoint-condition)))
+  (sweeprolog--query-once "sweep" "sweep_set_breakpoint_condition"
+                          (cons id cond)))
+
+(defun sweeprolog-delete-breakpoint (id)
+  "Delete the breakpoint with id ID."
+  (interactive (list (sweeprolog-read-breakpoint "Delete breakpoint")))
+  (let* ((file (sweeprolog--query-once "sweep" "sweep_breakpoint_file" id))
+         (buf (find-buffer-visiting file (lambda (b)
+                                           (with-current-buffer b
+                                             (and (derived-mode-p 'sweeprolog-mode)
+                                                  sweeprolog-highlight-breakpoints)))))
+         (range (sweeprolog--query-once "sweep" "sweep_breakpoint_range" id)))
+    (sweeprolog--query-once "sweep" "sweep_delete_breakpoint" id)
+    (message "Deleted breakpoint (id %d)" id)
+    (when (and buf range)
+      (with-current-buffer buf
+        (sweeprolog-analyze-term (car range))))))
+
+(defface sweeprolog-breakpoint-face
+  '((((background light)) :background "lightgreen")
+    (t                    :background "darkgreen"))
+  "Face used to highlight Prolog breakpoints."
+  :group 'sweeprolog-faces)
+
+(defvar sweeprolog-breakpoint-face 'sweeprolog-breakpoint-face
+  "Face to use for highlighting Prolog breakpoints.")
+
+(defun sweeprolog--highlight-breakpoint (beg end)
+  (font-lock--add-text-property beg end
+                                'font-lock-face
+                                sweeprolog-breakpoint-face
+                                (current-buffer)
+                                nil))
+
+(defun sweeprolog-highlight-breakpoint (id)
+  (when sweeprolog-highlight-breakpoints
+    (when-let
+        ((range
+          (sweeprolog--query-once "sweep" "sweep_breakpoint_range"
+                                  id)))
+      (with-silent-modifications
+        (sweeprolog--highlight-breakpoint (car range)
+                                          (cdr range))))))
+
+(defun sweeprolog-breakpoints-at-point (file point)
+  (seq-filter (lambda (bp)
+                (and (string= (expand-file-name (alist-get "file" bp nil nil #'string=))
+                              file)
+                     (when-let ((range (alist-get "range" bp nil nil #'string=)))
+                       (<= (car range) point (cadr range)))))
+              (sweeprolog-current-breakpoints)))
+
+(defun sweeprolog-read-breakpoint-at-point (point &optional prompt)
+  "Prompt with PROMPT for a breakpoint at POINT, with completion.
+If there only one breakpoint at POINT, return it without prompting."
+  (let* ((bps (sweeprolog-breakpoints-at-point (buffer-file-name)
+                                               point))
+         (col (mapcar (lambda (bp)
+                        (let ((id (alist-get "id" bp nil nil #'string=))
+                              (file (alist-get "file" bp nil nil #'string=))
+                              (line (alist-get "line" bp nil nil #'string=)))
+                          (cons (number-to-string id)
+                                (format "%s%s:%d"
+                                        (make-string (- 4 (floor (log id 10))) ? )
+                                        file line))))
+                      bps)))
+    (when col
+      (string-to-number
+       (if (= (length col) 1)
+           (caar col)
+         (let ((completion-extra-properties
+                (list :annotation-function
+                      (lambda (key)
+                        (alist-get key col nil nil #'string=)))))
+           (completing-read (or prompt "Breakpoint at point: ") col nil t)))))))
+
+(defun sweeprolog-delete-breakpoint-at-point (point)
+  "Delete breakpoint at POINT."
+  (interactive "d" sweeprolog-mode)
+  (if-let ((id (sweeprolog-read-breakpoint-at-point
+                point "Delete breakpoint at point: ")))
+      (sweeprolog-delete-breakpoint id)
+    (user-error "No breakpoints here!")))
+
+(defun sweeprolog-set-breakpoint (point &optional cond delete)
+  "Set breakpoint at POINT with condition COND.
+If DELETE is non-nil, delete the breakpoint at POINT instead.
+
+Interactively, POINT is point.  If called without a prefix
+argument, COND and DELETE are nil.  If called with a positive
+prefix argument, prompt for COND.  Otherwise, if called with a
+zero or negative prefix argument, delete the breakpoint at POINT
+instead."
+  (interactive
+   (cons (point)
+         (cond ((< (prefix-numeric-value current-prefix-arg) 1)
+                (list nil t))
+               (current-prefix-arg
+                (list (sweeprolog-read-breakpoint-condition)))))
+   sweeprolog-mode)
+  (if delete
+      (sweeprolog-delete-breakpoint-at-point point)
+    (if (or (sweeprolog-buffer-loaded-since-last-modification-p)
+            (and (y-or-n-p (concat (if (sweeprolog-buffer-load-time)
+                                       "Buffer modified since it was last loaded, re"
+                                     "Buffer isn't loaded, ")
+                                   "load before setting breakpoint?"))
+                 (sweeprolog-load-buffer (current-buffer))))
+        (if-let ((bp (sweeprolog--query-once "sweep" "sweep_set_breakpoint"
+                                             (list (buffer-file-name)
+                                                   (line-number-at-pos point)
+                                                   (1- point)))))
+            (progn
+              (if cond
+                  (if (sweeprolog-set-breakpoint-condition bp cond)
+                      (message "Created conditional breakpoint (id %d)." bp)
+                    (sweeprolog-delete-breakpoint bp)
+                    (user-error "Failed to set breakpoint condition"))
+                (message "Created breakpoint (id %d)." bp))
+              (sweeprolog-highlight-breakpoint bp))
+          (user-error "Failed to create breakpoint"))
+      (user-error "Cannot set breakpoint in buffer without loading it"))))
+
+(defun sweeprolog-breakpoint-menu-mode--entries ()
+  (mapcar (lambda (bp)
+            (let ((id (alist-get "id" bp nil nil #'string=))
+                  (file (alist-get "file" bp nil nil #'string=))
+                  (line (alist-get "line" bp nil nil #'string=))
+                  (pred (alist-get "predicate" bp nil nil #'string=))
+                  (clause (alist-get "clause" bp nil nil #'string=))
+                  (cond (alist-get "condition" bp nil nil #'string=)))
+              (list id (vector (number-to-string id)
+                               (if file file "")
+                               (if line (number-to-string line) "")
+                               (propertize pred
+                                           'face
+                                           (sweeprolog-predicate-indicator-face))
+                               (number-to-string clause)
+                               (or cond "")))))
+          (sweeprolog-current-breakpoints)))
+
+(defun sweeprolog-breakpoint-menu-mode--refresh ()
+  (tabulated-list-init-header)
+  (setq tabulated-list-entries (sweeprolog-breakpoint-menu-mode--entries)))
+
+(defun sweeprolog-breakpoint-menu-find (&optional other-window)
+  "Go to the source position of the breakpoint at point.
+If OTHER-WINDOW is non-nil, find it in another window."
+  (interactive "" sweeprolog-breakpoint-menu-mode)
+  (if-let ((vec (tabulated-list-get-entry)))
+      (let* ((file (seq-elt vec 1))
+             (line (seq-elt vec 2)))
+        (if other-window
+            (find-file-other-window file)
+          (find-file file))
+        (goto-char (point-min))
+        (forward-line (1- (string-to-number line))))
+    (user-error "No breakpoint menu entry here")))
+
+(defun sweeprolog-breakpoint-menu-find-other-window ()
+  "Find the position of the breakpoint at point in another window."
+  (interactive "" sweeprolog-breakpoint-menu-mode)
+  (sweeprolog-breakpoint-menu-find t))
+
+(defun sweeprolog-breakpoint-menu-delete ()
+  "Delete the breakpoint at point."
+  (interactive "" sweeprolog-breakpoint-menu-mode)
+  (if-let ((id (car (tabulated-list-delete-entry))))
+      (sweeprolog-delete-breakpoint id)
+    (user-error "No breakpoint menu entry here")))
+
+(defun sweeprolog-breakpoint-menu-set-condition (bp cond)
+  "Attach condition goal COND to the breakpoint BP at point."
+  (interactive (list (tabulated-list-get-id)
+                     (sweeprolog-read-breakpoint-condition))
+               sweeprolog-breakpoint-menu-mode)
+  (if bp
+      (if (sweeprolog-set-breakpoint-condition bp cond)
+          (tabulated-list-revert)
+        (user-error "Failed to set breakpoint condition"))
+    (user-error "No breakpoint menu entry here")))
+
+(defvar sweeprolog-breakpoint-menu-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "RET") #'sweeprolog-breakpoint-menu-find)
+    (define-key map (kbd "o")   #'sweeprolog-breakpoint-menu-find-other-window)
+    (define-key map (kbd "C")   #'sweeprolog-breakpoint-menu-set-condition)
+    (define-key map (kbd "D")   #'sweeprolog-breakpoint-menu-delete)
+    map)
+  "Local keymap for `sweeprolog-breakpoint-menu-mode' buffers.")
+
+(define-derived-mode sweeprolog-breakpoint-menu-mode
+  tabulated-list-mode "Sweep Breakpoint Menu"
+  "Major mode for browsing the list of current Prolog breakpoints."
+  (setq tabulated-list-format [("ID"   8  t)
+                               ("File" 40 t)
+                               ("Line" 8  t)
+                               ("Predicate" 32  t)
+                               ("Clause" 8  t)
+                               ("Condition" 20 t)])
+  (setq tabulated-list-padding 2)
+  (setq tabulated-list-sort-key (cons "ID" nil))
+  (add-hook 'tabulated-list-revert-hook
+            #'sweeprolog-breakpoint-menu-mode--refresh nil t)
+  (tabulated-list-init-header))
+
+(defun sweeprolog-list-breakpoints ()
+  "Display a list of Prolog breakpoints."
+  (interactive)
+  (let ((buf (get-buffer-create "*Sweep Breakpoints*")))
+    (with-current-buffer buf
+      (sweeprolog-breakpoint-menu-mode)
+      (sweeprolog-breakpoint-menu-mode--refresh)
+      (tabulated-list-print))
+    (pop-to-buffer buf)))
+
+
 ;;;; Footer
 
 (provide 'sweeprolog)