(in-package :lw-add-ons)
(defun symbol-string-at-point (&key (point (current-point)) previous)
"Returns two values - a string denoting the symbol at POINT and
the package to use at that POINT if the symbol isn't
package-qualified. PREVIOUS controls whether to look at the
previous symbol if POINT is between two symbols."
(ignore-errors*
(let ((string (editor::read-symbol-from-point :point point
:read-package-name t
:previous previous))
(package (editor::buffer-package-to-use point)))
(values string package))))
(defun symbol-at-point (&key (point (current-point)) previous)
"Returns the symbol at POINT. PREVIOUS controls whether to
look at the previous symbol if POINT is between two symbols."
(ignore-errors*
(multiple-value-bind (string package)
(symbol-string-at-point :point point :previous previous)
(let* ((*package* package)
(candidate (read-from-string string)))
(and (symbolp candidate)
candidate)))))
(defun enclosing-operators ()
"Returns a list of potential operators \(symbols behind an
opening parenthesis) starting from point and going up backwards."
(save-excursion
(loop while (ignore-errors*
(backward-up-list-command 1) t)
when (when (looking-at "\(")
(forward-character-command 1)
(prog1
(symbol-at-point)
(forward-character-command -1)))
collect it)))
(defun show-info (info &key full-length-p)
"Shows the string INFO in the echo area. Shows no more than
*MAX-INFO-LENGTH* unless FULL-LENGHT-P is true."
(apply #'message
(if (and (not full-length-p)
(> (length info) *max-info-length*))
(list "~A [...]" (subseq info 0 *max-info-length*))
(list "~A" info))))
(defun show-arglist ()
"Shows the argument list of the nearest enclosing operator that
has a function definition in the echo arrea. Shows the doc
string as well unless *SHOW-DOC-STRING-WHEN-SHOWING-ARGLIST* is
NIL."
(when-let (object
(loop for operator in (enclosing-operators)
when (and (symbolp operator)
(fboundp operator))
do (return operator)))
(show-info (format nil "~A~@[~%~A~]"
(cons object (function-lambda-list object))
(and *show-doc-string-when-showing-arglist*
(documentation object 'function))))))
(defun completions-for-echo-area (completions)
"Returns a string which shows a two-column list of the elements
\(which should be strings) in COMPLETIONS but no more than
*MAX-COMPLETIONS-TO-SHOW* of them."
(let ((max-left-width
(loop for completion in completions
for i from 1 to *max-completions-to-show*
when (oddp i)
maximize (length completion))))
(with-output-to-string (out)
(format out "~&Possible completions:~%~%")
(loop for (completion-1 completion-2 . rest) on completions by #'cddr
for i from 1 to *max-completions-to-show* by 2
do (format out "~&~VA ~A" max-left-width
completion-1
(cond ((and rest
(>= (1+ i) *max-completions-to-show*))
"[...]")
(completion-2)
(t "")))))))
(defun char-before ()
"Returns the character before the current point."
(character-at (current-point) -1))
(defun maybe-insert-right-parenthesis ()
"If the symbol at or before point is in function position and
denotes a function with an empty lambda list inserts a right
parenthesis, otherwise inserts a space and show the argument list
in the echo area."
(when-let (symbol (symbol-at-point :previous t))
(when (and (save-excursion
(backward-form-command 1)
(eql (char-before) #\())
(symbolp symbol)
(fboundp symbol))
(cond ((null (function-lambda-list symbol))
(self-insert-command 1 #\)))
((not (looking-at " "))
(insert-space-and-show-arglist-command nil))))))
#-:editor-has-dont-undo
(defmacro without-undo-with-cleanups (buffer form &body cleanups)
"Editor utility macro. See source code for LW editor."
(lw:rebinding (buffer)
(lw:with-unique-names (was-recording)
`(let ((,was-recording (editor::check-set-buffer-without-undo ,buffer)))
(unwind-protect
,form
(when ,was-recording
(editor::set-buffer-flag-bit ,buffer editor::*buffer-flag-dont-record-undo* nil))
,@cleanups)))))
#-:editor-has-dont-undo
(defmacro recording-for-undo-internal (point1 point2 line-start-p &body body)
"Does the whole work for RECORDING-FOR-UNDO. See source code for LW editor."
(lw:with-unique-names (old-string start end want-undo before-modified buffer-sym)
(lw:rebinding (point1 point2)
`(let* ((,buffer-sym (point-buffer ,point1))
(,want-undo (editor::check-want-to-record-undo-p ,buffer-sym nil))
(,before-modified (editor::buffer-modified-tick ,buffer-sym))
(,start (when ,want-undo
(let ((lsp ,line-start-p)
(sp (copy-i-point ,point1 :before-insert)))
(if lsp
(line-start sp))
sp)))
(,end (when ,want-undo (copy-i-point ,point2 :after-insert)))
(,old-string (when ,want-undo
(editor::points-to-buffer-string ,start ,end))))
(without-undo-with-cleanups ,buffer-sym
(progn ,@body)
(when ,want-undo
(editor::record-replace-region ,start ,end ,old-string ,before-modified)
(editor::delete-it ,start)
(editor::delete-it ,end)))))))
#-:editor-has-dont-undo
(defmacro recording-for-undo (point1 point2 &body body)
"Performs code in BODY and records changes between POINT1 and
POINT2 for undo operation. See source code for LW editor."
`(recording-for-undo-internal ,point1 ,point2 nil ,@body))
#-:editor-has-dont-undo
(defmacro recording-for-undo-locking (point1 point2 &body body)
"Like RECORDING-FOR-UNDO, but with lock. See source code for LW editor."
(lw:rebinding (point1)
#-:editor-does-not-have-with-buffer-locked
`(with-buffer-locked ((point-buffer ,point1))
(recording-for-undo ,point1 ,point2 ,@body))
#+:editor-does-not-have-with-buffer-locked
`(editor::with-locked-buffer (point-buffer ,point1)
(recording-for-undo ,point1 ,point2 ,@body))))
#+:editor-has-dont-undo
(defmacro recording-for-undo (point1 point2 &body body)
"Performs code in BODY and records changes between POINT1 and
POINT2 for undo operation. See source code for LW editor."
(lw:with-unique-names (old-string start end dont changed)
(lw:rebinding (point1 point2)
`(let* ((,dont editor::*dont-undo*)
(,changed (buffer-modified (point-buffer ,point1)))
(,start (unless ,dont (copy-i-point ,point1 :before-insert)))
(,end (unless ,dont (copy-i-point ,point2 :after-insert)))
(,old-string (unless ,dont (editor::points-to-buffer-string ,point1 ,point2))))
(unwind-protect
(let ((editor::*dont-undo* t))
,@body)
(progn
(unless ,dont
(editor::record-replace-region ,start ,end ,old-string ,changed)
(editor::delete-it ,start)
(editor::delete-it ,end))))))))
(defmacro recording-for-undo% (point1 point2 &body body)
"Helper macro which dispatches to RECORDING-FOR-UNDO or
RECORDING-FOR-UNDO-LOCKING depending on the LispWorks release."
#-:editor-has-dont-undo
`(recording-for-undo-locking ,point1 ,point2
,@body)
#+:editor-has-dont-undo
`(recording-for-undo ,point1 ,point2
,@body))
(defun current-line ()
"Returns the line the point is currently on as a string."
(line-string (current-point)))
(defun can-move-upwards-p ()
"Returns true if it is possible to move backward up from the
current point."
(save-excursion
(with-point ((point (current-point)))
(backward-up-list-command 1)
(point< (current-point) point))))
(defadvice (editor:find-alternate-file-command change-prompt :around
:documentation "Makes
sure FIND-ALTERNATE-FILE-COMMAND provides the full pathname of the
current buffer as the default when prompting.")
(p &optional pathname (buffer (current-buffer)))
(let ((*change-default-for-file-prompt* t))
(call-next-advice p pathname buffer)))
(defadvice (editor:find-alternate-file-command refresh :after
:documentation "After
FIND-ALTERNATE-COMMAND has run makes sure the contents of the buffer
are consistent with the file on disk.")
(p &optional pathname (buffer (current-buffer)))
(declare (ignore p pathname))
(let ((pathname (buffer-pathname buffer)))
(unless (check-disk-version-consistent pathname buffer)
(let* ((tn (probe-file pathname))
(pn (or tn (editor::canonical-pathname pathname))))
(editor::read-da-file pn tn buffer)))))
(defadvice (editor:prompt-for-file change-prompt :around
:documentation "When DEFAULT-STRING
is NIL, DEFAULT is a pathname, and *CHANGE-DEFAULT-FOR-FILE-PROMPT* is
true sets the full namestring of DEFAULT to be the default string.")
(&rest rest &key default default-string &allow-other-keys)
(let ((default-string (cond (default-string)
((and *change-default-for-file-prompt*
(pathnamep default))
(namestring default))
((pathnamep default)
(namestring (pathname-location default)))
(t default))))
(apply #'call-next-advice
:default-string default-string
rest)))
(defadvice (editor::find-pattern region-only :around
:documentation "Searches only up
until *SEARCH-END* unless the value of this variable is NIL.")
(point pattern &optional limit)
(cond ((and (null limit)
*search-end*)
(call-next-advice point pattern *search-end*))
(t (call-next-advice point pattern limit))))
#-:editor-does-not-have-i-find-pattern
(defadvice (editor::i-find-pattern region-only :around
:documentation "Searches only up
until *SEARCH-END* unless the value of this variable is NIL.")
(point pattern &optional limit)
(cond ((and (null limit)
*search-end*)
(call-next-advice point pattern *search-end*))
(t (call-next-advice point pattern limit))))
(defadvice (editor::query-replace-string region-only :around
:documentation "Performs
operation only up until *SEARCH-END* unless the value of this variable
is NIL. Also makes sure that all replacements can be undone with one
undo command.")
(&rest rest &key (point (current-point)) &allow-other-keys)
(let* ((current-mark (and (variable-value-if-bound 'editor::active-region-overlay
:buffer (current-buffer))
(current-mark nil t)))
(switch-p (and current-mark
(point< current-mark (current-point))))
(*search-end* (and current-mark
(copy-point (cond (switch-p (current-point))
(t current-mark)))))
(start (cond ((and current-mark switch-p)
current-mark)
(current-mark (current-point))
(t point))))
(unwind-protect
(with-point ((%start start)
(%end (or *search-end*
(current-point))))
(unless *search-end*
(editor:buffer-end %end))
#+:editor-has-dont-undo
(recording-for-undo %start %end
(apply #'call-next-advice :point start rest))
#-:editor-has-dont-undo
(apply #'call-next-advice :point start rest))
(when *search-end*
(delete-point *search-end*)))))
(defadvice (editor::find-next-ordinary-window allow-listener :around
:documentation "Allows
the \"Next Ordinary Window\" command to switch to a listener window.")
(current-window)
(let ((*forbidden-buffers* (remove :listener *forbidden-buffers*)))
(call-next-advice current-window)))
#+:editor-does-not-have-go-back
(defun push-onto-definitions-stack ()
"Pushes current point onto *FIND-DEFINITIONS-STACK* unless the
buffer isn't selectable."
(unless (editor::forbidden-buffer-p (current-buffer))
(push (copy-point (current-point))
*find-definitions-stack*)))
#+:editor-does-not-have-go-back
(defadvice (find-source-command push-onto-definitions-stack :around
:documentation "Pushes current point
onto *FIND-DEFINITIONS-STACK*.")
(&rest args)
(push-onto-definitions-stack)
(apply #'call-next-advice args))
#+:editor-does-not-have-go-back
(defadvice (find-source-for-dspec-command push-onto-definitions-stack :around
:documentation "Pushes
current point onto *FIND-DEFINITIONS-STACK*.")
(&rest args)
(push-onto-definitions-stack)
(apply #'call-next-advice args))
#+:editor-does-not-have-go-back
(defadvice (find-command-definition-command push-onto-definitions-stack :around
:documentation "Pushes
current point onto *FIND-DEFINITIONS-STACK*.")
(&rest args)
(push-onto-definitions-stack)
(apply #'call-next-advice args))
#+:editor-does-not-have-go-back
(defadvice (editor::edit-callers-command push-onto-definitions-stack :around
:documentation "Pushes
current point onto *FIND-DEFINITIONS-STACK*.")
(&rest args)
(push-onto-definitions-stack)
(apply #'call-next-advice args))
#+:editor-does-not-have-go-back
(defadvice (editor::edit-callees-command push-onto-definitions-stack :around
:documentation "Pushes
current point onto *FIND-DEFINITIONS-STACK*.")
(&rest args)
(push-onto-definitions-stack)
(apply #'call-next-advice args))
#+:editor-does-not-have-go-back
(defadvice (find-tag-command push-onto-definitions-stack :around
:documentation "Pushes current point onto
*FIND-DEFINITIONS-STACK*.")
(&rest args)
(push-onto-definitions-stack)
(apply #'call-next-advice args))
#+:editor-does-not-have-go-back
(defadvice (tags-search-command push-onto-definitions-stack :around
:documentation "Pushes current point
onto *FIND-DEFINITIONS-STACK*.")
(&rest args)
(push-onto-definitions-stack)
(apply #'call-next-advice args))
#+:editor-does-not-have-go-back
(defadvice (continue-tags-search-command push-onto-definitions-stack :around
:documentation "Pushes
current point onto *FIND-DEFINITIONS-STACK*.")
(&rest args)
(when editor::*meta-comma-action*
(push-onto-definitions-stack))
(apply #'call-next-advice args))
(defun complete-system (string parse-inf)
"Completion function used by PROMPT-FOR-ASDF-SYSTEM."
(declare (ignore parse-inf))
(editor::complete-string string *all-asdf-systems*
:ignore-case t))
(defun prompt-for-asdf-system (string &optional prompt help no-check)
"Prompts for an ASDF system name with STRING being the default."
(let ((*all-asdf-systems* (list-asdf-systems)))
(editor::parse-for-something
:prompt (or prompt "ASDF system: ")
:must-exist t
:help (or help "Type a name of an ASDF system.")
:default (or string "")
:default-string (or string "")
:verify-func (if no-check
(lambda (string parse-inf)
(declare (ignore parse-inf))
string)
(lambda (string parse-inf)
(declare (ignore parse-inf))
(and (find string *all-asdf-systems* :test #'string-equal)
string)))
:type :string
:default-in-prompt nil
:complete-func 'complete-system)))
(defun prompt-for-asdf-system-with-default (&optional prompt help no-check)
"Prompts for an ASDF system name and tries to find a default in the
default directory of the current buffer."
(let* ((directory (editor::buffer-default-directory (editor:current-buffer)))
(candidate (first (directory (make-pathname :name nil
:type "asd"
:defaults directory))))
(default (and candidate
(pathname-name candidate))))
(prompt-for-asdf-system default prompt help no-check)))
(defun complete-shortcut (string parse-inf)
"Completion function used by PROMPT-FOR-LISTENER-SHORTCUT."
(declare (ignore parse-inf))
(editor::complete-string string (mapcar #'cdr *listener-shortcuts*)
:ignore-case t))
(defun find-full-name (abbrev)
"Given an abbreviation finds the first item in *LISTENER-SHORTCUTS*
that is named by this abbreviation."
(or (loop for (short . long) in *listener-shortcuts*
when (string-equal short abbrev)
do (return long))
(loop for (nil . long) in *listener-shortcuts*
when (starts-with-p long abbrev)
do (return long))))
(defun prompt-for-listener-shortcut ()
"Prompts for a listener shortcut."
(let ((input
(editor::parse-for-something
:prompt (format nil "Shortcut [~{~A~^,~}] or Command: "
(sort (mapcar #'car *listener-shortcuts*) #'string-lessp))
:must-exist t
:help (format nil "Type the name or abbreviation of a listener shortcut:~%~%~{~A: ~A~%~}"
(loop for (short . long) in *listener-shortcuts*
collect short
collect long))
:default ""
:default-string ""
:verify-func (lambda (string parse-inf)
(declare (ignore parse-inf))
(and (find-full-name string)
string))
:type :string
:default-in-prompt nil
:complete-func 'complete-shortcut)))
(find-full-name input)))
(eval-when (:compile-toplevel :load-toplevel :execute)
(let ((*handle-warn-on-redefinition* :quiet))
(defmacro with-input-from-region ((var start end) &body body &environment env)
"During the evaluation of BODY, VAR is bound to a stream which
returns input from the region denoted by START and END."
(multiple-value-bind (forms decls)
(dspec:separate-declarations body env)
`(let ((,var (pop editor::*free-editor-region-streams*)))
,@decls
(setq ,var
(if ,var
(editor::modify-editor-region-stream ,var ,start ,end)
(editor::make-editor-region-stream ,start ,end)))
(unwind-protect
(progn ,@forms)
(editor::free-region-stream ,var)))))))
(defmacro with-compilation-environment-at-point ((point &key (compilep nil)
start-message
end-message)
&body body)
(with-unique-names (buffer)
`(editor::with-compilation-environment-at-point-fn
,point ,start-message ,end-message
#'(lambda (,buffer)
(let* ((,(if compilep '*compile-file-pathname* '*load-pathname*)
(buffer-pathname ,buffer))
(,(if compilep '*compile-file-truename* '*load-truename*)
(buffer-pathname ,buffer)) )
,@body)))))
(defun returning-lisp-eval (buffer start end print)
"Evaluates the region in the buffer BUFFER which is denoted by
START and END and returns the result."
(with-compilation-environment-at-point (start :start-message "Evaluating..."
:end-message (and (not (editor::windowp print))
"Finished evaluating"))
(with-input-from-region (stream start end)
(let ((out-stream (if (streamp print)
print
(editor::buffer-stream buffer)))
return-value)
(handler-case
(progn
(common-utilities:load-text-stream
stream
:exit-load-p t
:eval-function #'(lambda (form)
(multiple-value-list
(editor::editor-eval buffer form)))
:print-function #'(lambda (result)
(setq return-value result)
(and print
(if (editor::windowp print)
(process-character
`(message ,editor::*values-format-string* ,result)
print)
(editor::in-output-eval-results out-stream result)))))
return-value)
(end-of-file (x)
(editor::report-region-lisp-eval-error "Incomplete S-expression in region " x)
(return-from returning-lisp-eval nil))
(reader-error (x)
(editor::report-region-lisp-eval-error "Error while reading: ~a " x)
(return-from returning-lisp-eval nil)))))))
(defmacro with-output-to-help-window ((stream &rest options) &body body)
"Executes BODY with output that goes to STREAM redirected to an
IDE help window."
`(editor::with-output-to-help-window-1
#'(lambda (,stream) ,@body)
,@options))
(defun complete-package-name (string parse-inf)
"Like the function of the same name in the EDITOR package, but
case-insensitive."
(declare (ignore parse-inf))
(editor::complete-string
string
(sort (loop for pkg in (list-all-packages)
append (cons (package-name pkg) (package-nicknames pkg)))
'string<)
:ignore-case t))
(defun verify-package-func (string parse-inf)
"Like the function of the same name in the EDITOR package, but
case-insensitive."
(declare (type editor::parse-inf parse-inf))
(or (find-package (ignore-errors* (read-from-string (string-upcase string))))
(if (and (parse-inf-must-exist parse-inf)
(not (editor::recursive-parse 'prompt-for-y-or-n
:prompt
"No such package. Create it?")))
(values nil :no-value)
(make-package string))))
(defun prompt-for-package* (&key (must-exist t)
(default *package*)
(prompt "package: ")
(help "Type a package name.")
&allow-other-keys)
"Like EDITOR:PROMPT-FOR-PACKAGE, but case-insensitive."
(editor::parse-for-something :prompt prompt
:must-exist must-exist
:help help
:default default
:verify-func 'verify-package-func
:type :keyword
:complete-func 'complete-package-name
:default default))
(defun clean-namestring (namestring)
"Replaces characters in NAMESTRING which are illegal for a filename
with underlines. This function is aimed at Microsoft Windows but
shouldn't do any harm on OS X or Linux."
(regex-replace-all "[\\\\/?*:<>|\"\\000-\\037]" namestring "_"))
(defun normalize-pathname-for-backup (pathname)
"Converts the full form of the pathname designator PATHNAME to a
string that is suitable \(modulo illegal characters) as the NAME
component of a filename. This is a simplified form of what GNU Emacs
does."
(regex-replace-all "[/\\\\]"
(regex-replace "^([a-zA-Z]):[/\\\\]"
(namestring pathname)
"!drive_\\1!")
"!"))
(defun make-backup-filename-using-backup-directory (pathname)
"Creates and returns a backup pathname for PATHNAME. Assumes that
*BACKUP-DIRECTORY* denotes a directory. Note that due to the way the
backup pathname is constructed it is possible that two different files
end up with the same backup filename!"
(ensure-directories-exist
(make-pathname :name (clean-namestring
(normalize-pathname-for-backup pathname))
:type nil
:version nil
:defaults *backup-directory*)))
(defadvice (editor::make-backup-filename alternative-location :around
:documentation "Circumvents
the original function if the variable *MAKE-BACKUP-FILENAME-FUNCTION*
specifies another function to be used instead.")
(pathname)
(cond (*make-backup-filename-function*
(funcall *make-backup-filename-function* pathname))
(t (call-next-advice pathname))))