From 751c8e56470d60e12062d02b2d56eeadf5a478de Mon Sep 17 00:00:00 2001 From: Eshel Yaron Date: Sat, 22 Oct 2022 19:10:08 +0300 Subject: [PATCH] ADDED: context-based command for inserting new clauses or terms * sweeprolog.el (sweeprolog--forward-hole, sweeprolog--backward-hole): new functions. (sweeprolog-forward-hole): new command. (sweeprolog-end-of-predicate-definition,sweeprolog-insert-clause, sweeprolog-maybe-insert-next-clause, sweeprolog-maybe-define-predicate): new functions. (sweeprolog-insert-term-functions): new hook. (sweeprolog-insert-term-dwim): new command. (sweeprolog-mode-map): bind new commands. * sweeprolog-tests.el (dwim-define-predicate): new test case. * README.org (Context-Based Term Insertion): new section. --- NEWS.org | 19 ++++++++ README.org | 78 ++++++++++++++++++++++++++++-- sweeprolog-tests.el | 19 ++++++++ sweeprolog.el | 113 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 4 deletions(-) diff --git a/NEWS.org b/NEWS.org index 0284cfd..67adb80 100644 --- a/NEWS.org +++ b/NEWS.org @@ -11,6 +11,25 @@ SWI-Prolog in Emacs. For further details, please consult the manual: . +* Version 0.8.0 on 2022-10-22 + +** New command ~sweeprolog-insert-term-dwim~ in ~sweeprolog-mode~ buffers + +This version introduces a new mechanism for context-based term +insertion which revolves around a new command +~sweeprolog-insert-term-dwim~, bound to ~C-M-m~. When invoked after a +fullstop ending a predicate clause, this command inserts a new clause +for the same predicate. When called with point over a call to an +undefined predicate, this command insert a definition for that +predicate after the current predicate definition. + +** New command ~sweeprolog-forward-hole~ in ~sweeprolog-mode~ buffers + +This command, bound to ~C-c C-i~ in ~sweeprolog-mode-map~, moves the + cursor and marks the next hole (placeholder variable) inserted by + ~sweeprolog-insert-term-dwim~ for the user to fill it. + +** Newly deprecated user option ~sweeprolog-init-on-load * Version 0.7.2 on 2022-10-20 ** ~sweep-module~ is now loaded on-demand diff --git a/README.org b/README.org index d5b6596..567a34a 100644 --- a/README.org +++ b/README.org @@ -895,6 +895,80 @@ the non-exported predicates defined in the current buffer. To force prompting for a predicate, invoke ~sweeprolog-export-predicate~ with a prefix argument (~C-u C-c C-e~). +** Context-Based Term Insertion +:PROPERTIES: +:CUSTOM_ID: insert-term-at-point +:DESCRIPTION: Commands for smart insertion of Prolog terms based on the surrounding context +:END: + +#+CINDEX: context-based term insertion +#+CINDEX: term insertion at-point +#+FINDEX: sweeprolog-insert-term-dwim +#+KINDEX: M-RET +#+KINDEX: C-M-m +As a means of automating common Prolog code editing tasks, such as +adding new clauses to an existing predicate, ~sweeprolog-mode~ provides +the "do what I mean" command ~M-x sweeprolog-insert-term-dwim~, bound by +default to ~C-M-m~ (or equivalently, ~M-RET~). This command inserts a new +term at or after point according to the context in which +~sweeprolog-insert-term-dwim~ is invoked. + +#+VINDEX: sweeprolog-insert-term-functions +To determine which term to insert and exactly where, this command +calls the functions in the list held by the variable +~sweeprolog-insert-term-functions~ one after the other until one of the +functions signal success by returning non-nil. + +By default, ~sweeprolog-insert-term-dwim~ tries the following insertion +functions, in order: + +#+FINDEX: sweeprolog-maybe-insert-next-clause +#+FINDEX: sweeprolog-maybe-define-predicate +- ~sweeprolog-maybe-insert-next-clause~ :: If the last token before + point is a fullstop ending a predicate clause, insert a new clause + below it. +- ~sweeprolog-maybe-define-predicate~ :: If point is over a call to an + undefined predicate, insert a definition for that predicate below + the last clause of the current predicate definition. + +*** Filling Holes +:PROPERTIES: +:CUSTOM_ID: filling-holes +:DESCRIPTION: Commands for finding and filling holes for interactive term insertion +:END: + +#+CINDEX: holes +The default term insertion functions used by +~sweeprolog-insert-term-dwim~ create a new clause in the buffer, with +placeholders for the arguments of the head term (if any) and for the +clause's body. These placeholders are simply anonymous variables (~_~), +but they are annotated by the insertion functions with a special text +property[fn:1] that allows ~sweeprolog-mode~ to recognize them as +"holes" needed to be filled. After a term is inserted with +~sweeprolog-insert-term-dwim~, the region is set to the first hole and +the cursor left at the its end. + +#+FINDEX: sweeprolog-forward-hole +#+KINDEX: C-c C-i +#+KINDEX: C-c TAB +#+KINDEX: C-- C-c C-i +#+KINDEX: C-- C-c TAB +To jump to the next hole in a ~sweeprolog-mode~ buffer, use the command +~C-c C-i~ (~M-x sweeprolog-forward-hole~). This command sets up the +region to cover the next hole after point leaving the cursor at right +after the hole. To jump to the previous hole instead, call +~sweeprolog-forward-hole~ with a negative prefix argument (~C-- C-c C-i~). + +To "fill" a hole marked by one of the aforementioned commands, type +~C-w~ (~M-x kill-region~) to kill the region and remove the placeholder +variable, then insert Prolog code as usual. As an alternative to +manually killing the region with ~C-w~, with ~delete-selection-mode~ +enabled the placeholder is automatically deleted when the user inserts +a character while the region is active (see also [[info:emacs#Using Region][Using Region in the +Emacs manual]]). + +[fn:1] see [[info:elisp#Text Properties][Text Properties in the Elisp manual]] + * Prolog Help :PROPERTIES: :CUSTOM_ID: prolog-help @@ -1344,10 +1418,6 @@ there some further improvements that we want to pursue: appropriate position, i.e. before the first predicate definition in the buffer. -- Add a command for inserting a new clause, similar to ~C-M-m~ in ~prolog-mode~ :: ~sweeprolog-mode~ - should provide a command for inserting a new clause for the - predicate at or above point. - - Add a command for interactively inserting a new predicate :: ~sweeprolog-mode~ should provide a command for interactively inserting a new predicate definition, ideally with optional =PlDoc= comments (see [[#sweeprolog-pldoc][Documenting diff --git a/sweeprolog-tests.el b/sweeprolog-tests.el index a51afcc..6c87262 100644 --- a/sweeprolog-tests.el +++ b/sweeprolog-tests.el @@ -52,6 +52,25 @@ (should (equal (sweeprolog-next-solution) nil)) (should (equal (sweeprolog-cut-query) t))) + +(ert-deftest dwim-define-predicate () + "Tests defining a new predicate with `sweeprolog-insert-term-dwim'." + (with-temp-buffer + (sweeprolog-mode) + (insert " +foo :- bar. +") + (backward-word) + (sweeprolog-insert-term-dwim) + (call-interactively #'kill-region) + (insert "foo") + (should (string= (buffer-string) + " +foo :- bar. + +bar :- foo. +")))) + (ert-deftest end-of-top-term-with-univ () "Tests detecting the fullstop in presence of `=..'." (with-temp-buffer diff --git a/sweeprolog.el b/sweeprolog.el index be81643..5d611af 100644 --- a/sweeprolog.el +++ b/sweeprolog.el @@ -2014,6 +2014,100 @@ Interactively, PROJ is the prefix argument." (flymake-show-buffer-diagnostics)) (user-error "Flymake is not active in the current buffer"))) +(defun sweeprolog--forward-hole () + (if-let ((prop (text-property-search-forward 'sweeprolog-hole))) + (progn + (push-mark (prop-match-beginning prop) t t)) + (user-error "No holes following point"))) + +(defun sweeprolog--backward-hole () + (if-let ((prop (text-property-search-backward 'sweeprolog-hole)) + (end (prop-match-end prop))) + (progn + (goto-char end) + (push-mark (1- end) t t)) + (user-error "No holes before point"))) + +(defun sweeprolog-forward-hole (&optional arg) + "Move point to the next hole in a `sweeprolog-mode' buffer. + +With negative prefix argument ARG, move to the previous hole +instead." + (interactive "p" sweeprolog-mode) + (setq arg (or arg 1) + deactivate-mark nil) + (if (> 0 arg) + (sweeprolog--backward-hole) + (sweeprolog--forward-hole))) + +(defun sweeprolog--hole () + (propertize "_" + 'sweeprolog-hole t + 'rear-sticky '(sweeprolog-hole))) + +(defun sweeprolog-insert-clause (functor arity) + (let ((point nil)) + (combine-after-change-calls + (insert "\n" functor) + (setq point (point)) + (when (< 0 arity) + (insert "(") + (dotimes (_ (1- arity)) + (insert (sweeprolog--hole) ", ")) + (insert (sweeprolog--hole) ")")) + (insert " :- " (sweeprolog--hole) ".\n")) + (goto-char point) + (sweeprolog-forward-hole))) + +(defun sweeprolog-maybe-insert-next-clause (point kind beg end) + (when-let ((current-predicate (and (eq kind 'operator) + (string= "." (buffer-substring-no-properties beg end)) + (cdr (sweeprolog-definition-at-point point)))) + (functor (car current-predicate)) + (arity (cadr current-predicate))) + (goto-char end) + (end-of-line) + (sweeprolog-insert-clause functor arity) + t)) + +(defun sweeprolog-maybe-define-predicate (point _kind _beg _end) + (when-let ((pred (sweeprolog-identifier-at-point point))) + (unless (sweeprolog-predicate-properties pred) + (push-mark) + (sweeprolog-end-of-predicate-definition) + (let ((functor-arity (sweeprolog--mfn-to-functor-arity pred))) + (sweeprolog-insert-clause (car functor-arity) + (cdr functor-arity))) + t))) + +(defvar sweeprolog-insert-term-functions + '(sweeprolog-maybe-insert-next-clause + sweeprolog-maybe-define-predicate) + "Hook of functions that insert a Prolog term in a certain context. + +Each hook function is called with four arguments describing the +current context. The first argument, POINT, is the buffer +position in which insertion should take place. The rest of the +arguments, KIND, BEG and END, describe the previous non-comment +Prolog token as returned from `sweeprolog-last-token-boundaries'.") + +(defun sweeprolog-insert-term-dwim (&optional point) + "Insert an appropriate Prolog term at POINT. + +This command calls the functions in +`sweeprolog-insert-term-functions' one after the other until one +of them signal success by returning non-nil." + (interactive "d" sweeprolog-mode) + (setq point (or point (point))) + (let* ((bounds (sweeprolog-last-token-boundaries)) + (kind (car bounds)) + (beg (cadr bounds)) + (end (caddr bounds))) + (unless (run-hook-with-args-until-success + 'sweeprolog-insert-term-functions + point kind beg end) + (user-error "No term insertion function applies here")))) + (defvar sweeprolog-mode-syntax-table (let ((table (make-syntax-table))) (modify-syntax-entry ?_ "_" table) @@ -2039,11 +2133,13 @@ Interactively, PROJ is the prefix argument." (define-key map (kbd "C-c C-o") #'sweeprolog-find-file-at-point) (define-key map (kbd "C-c C-d") #'sweeprolog-document-predicate-at-point) (define-key map (kbd "C-c C-e") #'sweeprolog-export-predicate) + (define-key map (kbd "C-c C-i") #'sweeprolog-forward-hole) (define-key map (kbd "C-c C-`") (if (fboundp 'flymake-show-buffer-diagnostics) ;; Flymake 1.2.1+ #'sweeprolog-show-diagnostics #'flymake-show-diagnostics-buffer)) (define-key map (kbd "C-M-^") #'kill-backward-up-list) + (define-key map (kbd "C-M-m") #'sweeprolog-insert-term-dwim) map) "Keymap for `sweeprolog-mode'.") @@ -2580,6 +2676,23 @@ Interactively, PROJ is the prefix argument." summary)) (fill-paragraph)) +(defun sweeprolog-end-of-predicate-definition () + "Move to the end of the predicate definition at point." + (when-let* ((def (sweeprolog-definition-at-point))) + (let ((point (point)) + (fun (cadr def)) + (ari (caddr def))) + (while (and point (not (eobp))) + (sweeprolog-end-of-top-term) + (if-let* ((ndef (sweeprolog-definition-at-point (point))) + (nfun (cadr ndef)) + (nari (caddr ndef)) + (same (and (string= fun nfun) + (= ari nari)))) + (setq point (point)) + (goto-char point) + (setq point nil)))))) + (defun sweeprolog-beginning-of-predicate-at-point (&optional point) "Find the beginning of the predicate definition at or above POINT. -- 2.39.2