| [6] | 1 | ;;; -*- Package: Hemlock; Log: hemlock.log -*-
|
|---|
| 2 | ;;;
|
|---|
| 3 | ;;; **********************************************************************
|
|---|
| 4 | ;;; This code was written as part of the CMU Common Lisp project at
|
|---|
| 5 | ;;; Carnegie Mellon University, and has been placed in the public domain.
|
|---|
| 6 | ;;;
|
|---|
| 7 | #+CMU (ext:file-comment
|
|---|
| 8 | "$Header$")
|
|---|
| 9 | ;;;
|
|---|
| 10 | ;;; **********************************************************************
|
|---|
| 11 | ;;;
|
|---|
| 12 | ;;; This file contains file/buffer manipulating commands.
|
|---|
| 13 | ;;;
|
|---|
| 14 |
|
|---|
| 15 | (in-package :hemlock)
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 | |
|---|
| 19 |
|
|---|
| 20 | ;;;; PROCESS-FILE-OPTIONS.
|
|---|
| 21 |
|
|---|
| 22 | (defvar *mode-option-handlers* ()
|
|---|
| 23 | "Do not modify this; use Define-File-Option instead.")
|
|---|
| 24 |
|
|---|
| 25 | (defvar *file-type-hooks* ()
|
|---|
| 26 | "Do not modify this; use Define-File-Type-Hook instead.")
|
|---|
| 27 |
|
|---|
| 28 | (defun trim-subseq (string start end)
|
|---|
| 29 | (declare (simple-string string))
|
|---|
| 30 | (string-trim '(#\Space #\Tab) (subseq string start end)))
|
|---|
| 31 |
|
|---|
| 32 | ;;; PROCESS-FILE-OPTIONS checks the first line of buffer for the file options
|
|---|
| 33 | ;;; indicator "-*-". IF it finds this, then it enters a do-file-options block.
|
|---|
| 34 | ;;; If any parsing errors occur while picking out options, we return from this
|
|---|
| 35 | ;;; block. Staying inside this function at this point, allows us to still set
|
|---|
| 36 | ;;; a major mode if no file option specified one.
|
|---|
| 37 | ;;;
|
|---|
| 38 | ;;; We also cater to old style mode comments:
|
|---|
| 39 | ;;; -*- Lisp -*-
|
|---|
| 40 | ;;; -*- Text -*-
|
|---|
| 41 | ;;; This kicks in if we find no colon on the file options line.
|
|---|
| [8428] | 42 | ;;;
|
|---|
| 43 | (defun process-file-options (&optional (buffer (current-buffer))
|
|---|
| [6] | 44 | (pathname (buffer-pathname buffer)))
|
|---|
| 45 | "Checks for file options and invokes handlers if there are any. If no
|
|---|
| 46 | \"Mode\" mode option is specified, then this tries to invoke the appropriate
|
|---|
| 47 | file type hook."
|
|---|
| 48 | (let* ((string
|
|---|
| 49 | (line-string (mark-line (buffer-start-mark buffer))))
|
|---|
| 50 | (found (search "-*-" string))
|
|---|
| 51 | (no-major-mode t)
|
|---|
| 52 | (type (if pathname (pathname-type pathname))))
|
|---|
| 53 | (declare (simple-string string))
|
|---|
| 54 | (when found
|
|---|
| 55 | (block do-file-options
|
|---|
| 56 | (let* ((start (+ found 3))
|
|---|
| 57 | (end (search "-*-" string :start2 start)))
|
|---|
| 58 | (unless end
|
|---|
| 59 | (loud-message "No closing \"-*-\". Aborting file options.")
|
|---|
| 60 | (return-from do-file-options))
|
|---|
| 61 | (cond
|
|---|
| [15536] | 62 | ((find #\: string :start start :end end)
|
|---|
| [6] | 63 | (do ((opt-start start (1+ semi)) colon semi real-semi)
|
|---|
| 64 | (nil)
|
|---|
| 65 | (setq colon (position #\: string :start opt-start :end end))
|
|---|
| [15536] | 66 | (unless colon
|
|---|
| 67 | (unless real-semi
|
|---|
| [6] | 68 | (loud-message "Missing \":\". Aborting file options."))
|
|---|
| [15536] | 69 | (return-from do-file-options))
|
|---|
| [6] | 70 | (setq semi (or (setq real-semi (position #\; string :start colon :end end)) end))
|
|---|
| 71 | (let* ((option (nstring-downcase
|
|---|
| 72 | (trim-subseq string opt-start colon)))
|
|---|
| 73 | (handler (assoc option *mode-option-handlers*
|
|---|
| 74 | :test #'string=)))
|
|---|
| 75 | (declare (simple-string option))
|
|---|
| 76 | (cond
|
|---|
| 77 | (handler
|
|---|
| 78 | (let ((result (funcall (cdr handler) buffer
|
|---|
| 79 | (trim-subseq string (1+ colon) semi))))
|
|---|
| 80 | (when (string= option "mode")
|
|---|
| 81 | (setq no-major-mode (not result)))))
|
|---|
| 82 | (t (message "Unknown file option: ~S" option)))
|
|---|
| 83 | (when (= semi end) (return nil)))))
|
|---|
| 84 | (t
|
|---|
| 85 | ;; Old style mode comment.
|
|---|
| 86 | (setq no-major-mode nil)
|
|---|
| 87 | (funcall (cdr (assoc "mode" *mode-option-handlers* :test #'string=))
|
|---|
| 88 | buffer (trim-subseq string start end)))))))
|
|---|
| 89 | (when (and no-major-mode type)
|
|---|
| 90 | (let ((hook (assoc (string-downcase type) *file-type-hooks*
|
|---|
| 91 | :test #'string=)))
|
|---|
| 92 | (when hook (funcall (cdr hook) buffer type))))))
|
|---|
| 93 |
|
|---|
| 94 |
|
|---|
| 95 | |
|---|
| 96 |
|
|---|
| 97 | ;;;; File options and file type hooks.
|
|---|
| 98 |
|
|---|
| 99 | (defmacro define-file-option (name lambda-list &body body)
|
|---|
| 100 | "Define-File-Option Name (Buffer Value) {Form}*
|
|---|
| 101 | Defines a new file option to be user in the -*- line at the top of a file.
|
|---|
| 102 | The body is evaluated with Buffer bound to the buffer the file has been read
|
|---|
| 103 | into and Value to the string argument to the option."
|
|---|
| 104 | (let ((name (string-downcase name)))
|
|---|
| 105 | `(setf (cdr (or (assoc ,name *mode-option-handlers* :test #'string=)
|
|---|
| 106 | (car (push (cons ,name nil) *mode-option-handlers*))))
|
|---|
| 107 | #'(lambda ,lambda-list ,@body))))
|
|---|
| 108 |
|
|---|
| 109 | (define-file-option "Mode" (buffer str)
|
|---|
| 110 | (let ((seen-major-mode-p nil)
|
|---|
| 111 | (lastpos 0))
|
|---|
| 112 | (loop
|
|---|
| 113 | (let* ((pos (position #\, str :start lastpos))
|
|---|
| 114 | (substr (trim-subseq str lastpos pos)))
|
|---|
| 115 | (cond ((getstring substr *mode-names*)
|
|---|
| 116 | (cond ((mode-major-p substr)
|
|---|
| 117 | (when seen-major-mode-p
|
|---|
| 118 | (loud-message
|
|---|
| 119 | "Major mode already processed. Using ~S now."
|
|---|
| 120 | substr))
|
|---|
| 121 | (setf seen-major-mode-p t)
|
|---|
| 122 | (setf (buffer-major-mode buffer) substr))
|
|---|
| 123 | (t
|
|---|
| 124 | (setf (buffer-minor-mode buffer substr) t))))
|
|---|
| 125 | (t
|
|---|
| 126 | (loud-message "~S is not a defined mode -- ignored." substr)))
|
|---|
| 127 | (unless pos
|
|---|
| 128 | (return seen-major-mode-p))
|
|---|
| [12146] | 129 | (setf lastpos (1+ pos))))))
|
|---|
| 130 |
|
|---|
| [6] | 131 | (define-file-option "log" (buffer string)
|
|---|
| [15536] | 132 | (declare (ignore buffer string)))
|
|---|
| 133 |
|
|---|
| [12146] | 134 | (define-file-option "base" (buffer string)
|
|---|
| [15536] | 135 | (declare (ignore buffer string)))
|
|---|
| 136 |
|
|---|
| [12146] | 137 | (define-file-option "syntax" (buffer string)
|
|---|
| [15536] | 138 | (declare (ignore buffer string)))
|
|---|
| 139 |
|
|---|
| 140 | (define-file-option "coding" (buffer string)
|
|---|
| 141 | (hemlock-ext:set-buffer-external-format buffer string))
|
|---|
| 142 |
|
|---|
| 143 |
|
|---|
| [6] | 144 |
|
|---|
| 145 |
|
|---|
| 146 | (defmacro define-file-type-hook (type-list (buffer type) &body body)
|
|---|
| 147 | "Define-File-Type-Hook ({Type}*) (Buffer Type) {Form}*
|
|---|
| 148 | Define some code to be evaluated when a file having one of the specified
|
|---|
| 149 | Types is read by a file command. Buffer is bound to the buffer the
|
|---|
| 150 | file is in, and Type is the actual type read."
|
|---|
| 151 | (let ((fun (gensym)) (str (gensym)))
|
|---|
| 152 | `(flet ((,fun (,buffer ,type) ,@body))
|
|---|
| 153 | (dolist (,str ',(mapcar #'string-downcase type-list))
|
|---|
| 154 | (setf (cdr (or (assoc ,str *file-type-hooks* :test #'string=)
|
|---|
| 155 | (car (push (cons ,str nil) *file-type-hooks*))))
|
|---|
| 156 | #',fun)))))
|
|---|
| 157 |
|
|---|
| 158 | (define-file-type-hook ("pas" "pasmac" "macro" "defs" "spc" "bdy")
|
|---|
| 159 | (buffer type)
|
|---|
| 160 | (declare (ignore type))
|
|---|
| [6770] | 161 | (setf (buffer-major-mode buffer) "Pascal"))
|
|---|
| [6] | 162 |
|
|---|
| 163 | (define-file-type-hook ("lisp" "slisp" "l" "lsp" "mcl" "cl") (buffer type)
|
|---|
| 164 | (declare (ignore type))
|
|---|
| 165 | (setf (buffer-major-mode buffer) "Lisp"))
|
|---|
| 166 |
|
|---|
| 167 | (define-file-type-hook ("txt" "text" "tx") (buffer type)
|
|---|
| 168 | (declare (ignore type))
|
|---|
| 169 | (setf (buffer-major-mode buffer) "Text"))
|
|---|
| 170 |
|
|---|
| 171 |
|
|---|
| 172 | |
|---|
| 173 |
|
|---|
| 174 | ;;;; Support for file hacking commands:
|
|---|
| 175 |
|
|---|
| 176 | (defhvar "Pathname Defaults"
|
|---|
| 177 | "This variable contains a pathname which is used to supply defaults
|
|---|
| 178 | when we don't have anything better."
|
|---|
| 179 | :value (pathname "gazonk.del"))
|
|---|
| 180 |
|
|---|
| 181 | (defhvar "Last Resort Pathname Defaults"
|
|---|
| 182 | "This variable contains a pathname which is used to supply defaults when
|
|---|
| 183 | we don't have anything better, but unlike \"Pathname Defaults\", this is
|
|---|
| 184 | never set to some buffer's pathname."
|
|---|
| 185 | :value (pathname "gazonk"))
|
|---|
| 186 |
|
|---|
| 187 | (defhvar "Last Resort Pathname Defaults Function"
|
|---|
| 188 | "This variable contains a function that is called when a default pathname is
|
|---|
| 189 | needed, the buffer has no pathname, and the buffer's name is not entirely
|
|---|
| 190 | composed of alphanumerics. The default value is a function that simply
|
|---|
| 191 | returns \"Last Resort Pathname Defaults\". The function must take a buffer
|
|---|
| 192 | as an argument, and it must return some pathname."
|
|---|
| 193 | :value #'(lambda (buffer)
|
|---|
| 194 | (declare (ignore buffer))
|
|---|
| 195 | (merge-pathnames (value last-resort-pathname-defaults)
|
|---|
| 196 | (value pathname-defaults))))
|
|---|
| 197 |
|
|---|
| 198 | (defun buffer-default-pathname (buffer)
|
|---|
| 199 | "Returns \"Buffer Pathname\" if it is bound. If it is not, and buffer's name
|
|---|
| 200 | is composed solely of alphnumeric characters, then return a pathname formed
|
|---|
| 201 | from the buffer's name. If the buffer's name has other characters in it,
|
|---|
| 202 | then return the value of \"Last Resort Pathname Defaults Function\" called
|
|---|
| 203 | on buffer."
|
|---|
| 204 | (or (buffer-pathname buffer)
|
|---|
| 205 | (if (every #'alphanumericp (the simple-string (buffer-name buffer)))
|
|---|
| 206 | (merge-pathnames (make-pathname :name (buffer-name buffer))
|
|---|
| 207 | (value pathname-defaults))
|
|---|
| 208 | (funcall (value last-resort-pathname-defaults-function) buffer))))
|
|---|
| 209 |
|
|---|
| 210 |
|
|---|
| 211 | (defun pathname-to-buffer-name (pathname)
|
|---|
| 212 | "Returns a simple-string using components from pathname."
|
|---|
| 213 | (let ((pathname (pathname pathname)))
|
|---|
| 214 | (concatenate 'simple-string
|
|---|
| 215 | (file-namestring pathname)
|
|---|
| 216 | " "
|
|---|
| 217 | (directory-namestring pathname))))
|
|---|
| 218 |
|
|---|
| 219 |
|
|---|
| 220 | |
|---|
| 221 |
|
|---|
| 222 | ;;;; File hacking commands.
|
|---|
| 223 |
|
|---|
| [8428] | 224 | (defcommand "Process File Options" (p)
|
|---|
| [6] | 225 | "Reprocess this buffer's file options."
|
|---|
| [569] | 226 | "Reprocess this buffer's file options."
|
|---|
| 227 | (declare (ignore p))
|
|---|
| 228 | (process-file-options))
|
|---|
| 229 |
|
|---|
| 230 | (defcommand "Ensure File Options Line" (p)
|
|---|
| 231 | "Insert a default file options line at the beginning of the buffer, unless such a line already exists."
|
|---|
| 232 | "Insert a default file options line at the beginning of the buffer, unless such a line already exists."
|
|---|
| 233 | (declare (ignore p))
|
|---|
| 234 | (let* ((buffer (current-buffer))
|
|---|
| 235 | (string
|
|---|
| 236 | (line-string (mark-line (buffer-start-mark buffer))))
|
|---|
| 237 | (found (search "-*-" string))
|
|---|
| 238 | (end (if found (search "-*-" string :start2 (+ found 3)))))
|
|---|
| 239 | (unless end
|
|---|
| 240 | (let* ((mode (buffer-major-mode buffer)))
|
|---|
| 241 | (unless mode
|
|---|
| 242 | ;; Try to derive the buffer's major mode from its pathname's
|
|---|
| 243 | ;; type.
|
|---|
| 244 | (let* ((pathname (buffer-pathname buffer))
|
|---|
| 245 | (type (if pathname (pathname-type pathname)))
|
|---|
| 246 | (hook (if type
|
|---|
| 247 | (assoc (string-downcase type) *file-type-hooks*
|
|---|
| [587] | 248 | :test #'string=))))
|
|---|
| [569] | 249 | (when hook
|
|---|
| 250 | (funcall (cdr hook) buffer type)
|
|---|
| 251 | (setq mode (buffer-major-mode buffer)))))
|
|---|
| 252 | (with-mark ((mark (buffer-start-mark buffer) :left-inserting))
|
|---|
| 253 | (if (string-equal mode "Lisp")
|
|---|
| [15536] | 254 | (let* ((package-name
|
|---|
| 255 | (if (hemlock-bound-p 'current-package :buffer buffer)
|
|---|
| 256 | (variable-value 'hemlock::current-package
|
|---|
| 257 | :buffer buffer)
|
|---|
| 258 | "CL-USER"))
|
|---|
| 259 | (encoding-string (let* ((string (hemlock-ext:buffer-encoding-name buffer))
|
|---|
| 260 | (suffix (case (hi::buffer-line-termination buffer)
|
|---|
| 261 | (:cr "mac")
|
|---|
| [569] | 262 | (:crlf "dos"))))
|
|---|
| 263 | (if suffix
|
|---|
| [15536] | 264 | (concatenate 'string string "-" suffix)
|
|---|
| [569] | 265 | string))))
|
|---|
| 266 | (insert-string
|
|---|
| 267 | mark
|
|---|
| 268 | (format nil ";;; -*- Mode: Lisp; Package: ~a; Coding: ~a; -*-" package-name encoding-string)))
|
|---|
| 269 | (insert-string
|
|---|
| 270 | mark
|
|---|
| 271 | (format nil ";;; -*- Mode: ~a -*-" (or mode "Fundamental"))))
|
|---|
| 272 | (insert-character mark #\NewLine))))
|
|---|
| 273 | (buffer-start (buffer-point buffer))))
|
|---|
| 274 |
|
|---|
| 275 |
|
|---|
| 276 |
|
|---|
| 277 |
|
|---|
| 278 |
|
|---|
| 279 |
|
|---|
| 280 |
|
|---|
| 281 |
|
|---|
| 282 |
|
|---|
| [6] | 283 |
|
|---|
| 284 |
|
|---|
| 285 |
|
|---|
| 286 |
|
|---|
| 287 | (defcommand "Insert File" (p &optional pathname (buffer (current-buffer)))
|
|---|
| 288 | "Inserts a file which is prompted for into the current buffer at the point.
|
|---|
| 289 | The prefix argument is ignored."
|
|---|
| 290 | "Inserts the file named by Pathname into Buffer at the point."
|
|---|
| 291 | (declare (ignore p))
|
|---|
| 292 | (let* ((pn (or pathname
|
|---|
| 293 | (prompt-for-file :default (buffer-default-pathname buffer)
|
|---|
| 294 | :prompt "Insert File: "
|
|---|
| 295 | :help "Name of file to insert")))
|
|---|
| 296 | (point (buffer-point buffer))
|
|---|
| 297 | ;; start and end will be deleted by undo stuff
|
|---|
| [8428] | 298 | (start (copy-mark point :right-inserting))
|
|---|
| [6] | 299 | (end (copy-mark point :left-inserting))
|
|---|
| 300 | (region (region start end)))
|
|---|
| 301 | (setv pathname-defaults pn)
|
|---|
| 302 | (push-new-buffer-mark end)
|
|---|
| 303 | (read-file pn end)
|
|---|
| 304 | (make-region-undo :delete "Insert File" region)))
|
|---|
| 305 |
|
|---|
| 306 | (defcommand "Write Region" (p &optional pathname)
|
|---|
| 307 | "Writes the current region to a file. "
|
|---|
| 308 | "Writes the current region to a file. "
|
|---|
| 309 | (declare (ignore p))
|
|---|
| 310 | (let ((region (current-region))
|
|---|
| 311 | (pn (or pathname
|
|---|
| 312 | (prompt-for-file :prompt "File to Write: "
|
|---|
| 313 | :help "The name of the file to write the region to. "
|
|---|
| 314 | :default (buffer-default-pathname
|
|---|
| 315 | (current-buffer))
|
|---|
| 316 | :must-exist nil))))
|
|---|
| 317 | (write-file region pn)
|
|---|
| 318 | (message "~A written." (namestring (truename pn)))))
|
|---|
| 319 |
|
|---|
| [12860] | 320 |
|
|---|
| [6] | 321 | |
|---|
| 322 |
|
|---|
| 323 | ;;;; Visiting and reverting files.
|
|---|
| 324 |
|
|---|
| 325 | #+No ;; Dubious semantics in a document-centered model. Also, doesn't work, see bug #476.
|
|---|
| 326 | (defcommand "Visit File" (p &optional pathname (buffer (current-buffer)))
|
|---|
| 327 | "Replaces the contents of Buffer with the file Pathname. The prefix
|
|---|
| 328 | argument is ignored. The buffer is set to be writable, so its region
|
|---|
| 329 | can be deleted."
|
|---|
| 330 | "Replaces the contents of the current buffer with the text in the file
|
|---|
| 331 | which is prompted for. The prefix argument is, of course, ignored p times."
|
|---|
| 332 | (declare (ignore p))
|
|---|
| 333 | (when (and (buffer-modified buffer)
|
|---|
| 334 | (prompt-for-y-or-n :prompt "Buffer is modified, save it? "))
|
|---|
| 335 | (save-file-command () buffer))
|
|---|
| 336 | (let ((pn (or pathname
|
|---|
| 337 | (prompt-for-file :prompt "Visit File: "
|
|---|
| 338 | :must-exist nil
|
|---|
| 339 | :help "Name of file to visit."
|
|---|
| 340 | :default (buffer-default-pathname buffer)))))
|
|---|
| 341 | (setf (buffer-writable buffer) t)
|
|---|
| 342 | (read-buffer-file pn buffer)
|
|---|
| 343 | (let ((n (pathname-to-buffer-name (buffer-pathname buffer))))
|
|---|
| 344 | (unless (getstring n *buffer-names*)
|
|---|
| 345 | (setf (buffer-name buffer) n))
|
|---|
| 346 | (warn-about-visit-file-buffers buffer))))
|
|---|
| 347 |
|
|---|
| 348 | (defun warn-about-visit-file-buffers (buffer)
|
|---|
| 349 | (let ((buffer-pn (buffer-pathname buffer)))
|
|---|
| 350 | (dolist (b *buffer-list*)
|
|---|
| 351 | (unless (eq b buffer)
|
|---|
| 352 | (let ((bpn (buffer-pathname b)))
|
|---|
| 353 | (when (equal bpn buffer-pn)
|
|---|
| 354 | (loud-message "Buffer ~A also contains ~A."
|
|---|
| 355 | (buffer-name b) (namestring buffer-pn))
|
|---|
| 356 | (return)))))))
|
|---|
| 357 |
|
|---|
| 358 |
|
|---|
| 359 | (defhvar "Revert File Confirm"
|
|---|
| 360 | "If this is true, Revert File will prompt before reverting."
|
|---|
| 361 | :value t)
|
|---|
| 362 |
|
|---|
| 363 | (defcommand "Revert File" (p)
|
|---|
| 364 | "Unless in Save Mode, reads in the last saved version of the file in
|
|---|
| 365 | the current buffer. When in Save Mode, reads in the last checkpoint or
|
|---|
| 366 | the last saved version, whichever is more recent. An argument will always
|
|---|
| 367 | force Revert File to use the last saved version. In either case, if the
|
|---|
| 368 | buffer has been modified and \"Revert File Confirm\" is true, then Revert
|
|---|
| [7077] | 369 | File will ask for confirmation beforehand. An attempt is made to maintain
|
|---|
| [12859] | 370 | the point's relative position."
|
|---|
| [7077] | 371 | "With an argument reverts to the last saved version of the file in the
|
|---|
| [6] | 372 | current buffer. Without, reverts to the last checkpoint or last saved
|
|---|
| 373 | version, whichever is more recent."
|
|---|
| 374 | (declare (ignore p))
|
|---|
| 375 | (hemlock-ext:revert-hemlock-buffer (current-buffer))
|
|---|
| [810] | 376 | (clear-echo-area))
|
|---|
| [8428] | 377 |
|
|---|
| [6] | 378 |
|
|---|
| 379 | ;;;; Find file.
|
|---|
| 380 |
|
|---|
| 381 |
|
|---|
| [8437] | 382 | (defcommand "Find File" (p)
|
|---|
| [12234] | 383 | "Visit a file in its own buffer.
|
|---|
| [12859] | 384 | If the file is already in some buffer, select that buffer,
|
|---|
| [810] | 385 | otherwise make a new buffer with the same name as the file and
|
|---|
| 386 | read the file into it."
|
|---|
| [8428] | 387 | (declare (ignore p))
|
|---|
| [6] | 388 | (hi::allowing-buffer-display ((current-buffer))
|
|---|
| [8428] | 389 | (hemlock-ext:open-hemlock-buffer :pathname :prompt)))
|
|---|
| [6] | 390 |
|
|---|
| 391 |
|
|---|
| 392 | #|
|
|---|
| 393 | (defun find-file-buffer (pathname)
|
|---|
| 394 | "Return a buffer associated with the file Pathname, reading the file into a
|
|---|
| 395 | new buffer if necessary. The second value is T if we created a buffer, NIL
|
|---|
| [8428] | 396 | otherwise. If the file has already been read, we check to see if the file
|
|---|
| [6] | 397 | has been modified on disk since it was read, giving the user various
|
|---|
| 398 | recovery options."
|
|---|
| 399 | (let* ((pathname (pathname pathname))
|
|---|
| 400 | (trial-pathname (or (probe-file pathname)
|
|---|
| 401 | (merge-pathnames pathname (default-directory))))
|
|---|
| 402 | (found (find trial-pathname (the list *buffer-list*)
|
|---|
| 403 | :key #'buffer-pathname :test #'equal)))
|
|---|
| 404 | (cond ((not found)
|
|---|
| 405 | (if (and (null (pathname-name trial-pathname))
|
|---|
| 406 | (null (pathname-type trial-pathname))
|
|---|
| 407 | (pathname-directory trial-pathname))
|
|---|
| 408 | ;; This looks like a directory -- make dired buffer
|
|---|
| 409 | (dired-guts nil nil trial-pathname)
|
|---|
| 410 |
|
|---|
| 411 | (let* ((name (pathname-to-buffer-name trial-pathname))
|
|---|
| 412 | (found (getstring name *buffer-names*))
|
|---|
| 413 | (use (if found
|
|---|
| 414 | (prompt-for-buffer
|
|---|
| 415 | :prompt "Buffer to use: "
|
|---|
| 416 | :help
|
|---|
| 417 | "Buffer name in use; give another buffer name, or confirm to reuse."
|
|---|
| 418 | :default found
|
|---|
| 419 | :must-exist nil)
|
|---|
| 420 | (make-buffer name)))
|
|---|
| 421 | (buffer (if (stringp use) (make-buffer use) use)))
|
|---|
| 422 | (when (and (buffer-modified buffer)
|
|---|
| 423 | (prompt-for-y-or-n :prompt
|
|---|
| 424 | "Buffer is modified, save it? "))
|
|---|
| 425 | (save-file-command () buffer))
|
|---|
| 426 | (read-buffer-file pathname buffer)
|
|---|
| 427 | (values buffer (stringp use)))))
|
|---|
| [8428] | 428 | ((check-disk-version-consistent pathname found)
|
|---|
| [6] | 429 | (values found nil))
|
|---|
| 430 | (t
|
|---|
| 431 | (read-buffer-file pathname found)
|
|---|
| 432 | (values found nil)))))
|
|---|
| 433 | |#
|
|---|
| 434 |
|
|---|
| 435 | ;;; Check-Disk-Version-Consistent -- Internal
|
|---|
| 436 | ;;;
|
|---|
| 437 | ;;; Check that Buffer contains a valid version of the file Pathname,
|
|---|
| 438 | ;;; harrassing the user if not. We return true if the buffer is O.K., and
|
|---|
| 439 | ;;; false if the file should be read.
|
|---|
| 440 | ;;;
|
|---|
| 441 | (defun check-disk-version-consistent (pathname buffer)
|
|---|
| 442 | (let ((ndate (file-write-date pathname))
|
|---|
| 443 | (odate (buffer-write-date buffer)))
|
|---|
| 444 | (cond ((not (and ndate odate (/= ndate odate)))
|
|---|
| 445 | t)
|
|---|
| 446 | ((buffer-modified buffer)
|
|---|
| 447 | (beep)
|
|---|
| 448 | (clear-input)
|
|---|
| 449 | (command-case (:prompt (list
|
|---|
| 450 | "File has been changed on disk since it was read and you have made changes too!~
|
|---|
| 451 | ~%Read in the disk version of ~A? [Y] " (namestring pathname))
|
|---|
| 452 | :help
|
|---|
| 453 | "The file in disk has been changed since Hemlock last saved it, meaning that
|
|---|
| 454 | someone else has probably overwritten it. Since the version read into Hemlock
|
|---|
| 455 | has been changed as well, the two versions may have inconsistent changes. If
|
|---|
| 456 | this is the case, it would be a good idea to save your changes in another file
|
|---|
| 457 | and compare the two versions.
|
|---|
| 458 |
|
|---|
| 459 | Type one of the following commands:")
|
|---|
| 460 | ((:confirm :yes)
|
|---|
| 461 | "Prompt for a file to write the buffer out to, then read in the disk version."
|
|---|
| 462 | (write-buffer-file
|
|---|
| 463 | buffer
|
|---|
| 464 | (prompt-for-file
|
|---|
| 465 | :prompt "File to save changes in: "
|
|---|
| 466 | :help (list "Save buffer ~S to this file before reading ~A."
|
|---|
| 467 | (buffer-name buffer) (namestring pathname))
|
|---|
| 468 | :must-exist nil
|
|---|
| 469 | :default (buffer-default-pathname buffer)))
|
|---|
| 470 | nil)
|
|---|
| 471 | (:no
|
|---|
| 472 | "Change to the buffer without reading the new version."
|
|---|
| 473 | t)
|
|---|
| 474 | (#\r
|
|---|
| 475 | "Read in the new version, clobbering the changes in the buffer."
|
|---|
| 476 | nil)))
|
|---|
| 477 | (t
|
|---|
| 478 | (not (prompt-for-yes-or-no :prompt
|
|---|
| 479 | (list
|
|---|
| 480 | "File has been changed on disk since it was read.~
|
|---|
| 481 | ~%Read in the disk version of ~A? "
|
|---|
| 482 | (namestring pathname))
|
|---|
| 483 | :help
|
|---|
| 484 | "Type Y to read in the new version or N to just switch to the buffer."
|
|---|
| 485 | :default t))))))
|
|---|
| 486 |
|
|---|
| 487 |
|
|---|
| 488 | (defhvar "Read File Hook"
|
|---|
| 489 | "These functions are called when a file is read into a buffer. Each function
|
|---|
| 490 | must take two arguments -- the buffer the file was read into and whether the
|
|---|
| 491 | file existed (non-nil) or not (nil).")
|
|---|
| 492 |
|
|---|
| 493 | (defun read-buffer-file (pathname buffer)
|
|---|
| 494 | "Delete the buffer's region, and uses READ-FILE to read pathname into it.
|
|---|
| 495 | If the file exists, set the buffer's write date to the file's; otherwise,
|
|---|
| 496 | MESSAGE that this is a new file and set the buffer's write date to nil.
|
|---|
| 497 | Move buffer's point to the beginning, set the buffer unmodified. If the
|
|---|
| 498 | file exists, set the buffer's pathname to the probed pathname; else, set it
|
|---|
| [798] | 499 | to pathname merged with DEFAULT-DIRECTORY. Set \"Pathname Defaults\" to the
|
|---|
| [7595] | 500 | same thing. Process the file options, and then invoke \"Read File Hook\"."
|
|---|
| [6] | 501 | (setf (buffer-writable buffer) t)
|
|---|
| 502 | (delete-region (buffer-region buffer))
|
|---|
| 503 | (let* ((pathname (pathname pathname))
|
|---|
| 504 | (probed-pathname (probe-file pathname))
|
|---|
| 505 | (hi::*current-buffer* buffer))
|
|---|
| 506 | (cond (probed-pathname
|
|---|
| 507 | (read-file probed-pathname (buffer-point buffer))
|
|---|
| 508 | (setf (buffer-write-date buffer) (file-write-date probed-pathname)))
|
|---|
| 509 | (t
|
|---|
| [8428] | 510 | (message "(New File)")
|
|---|
| [6] | 511 | (setf (buffer-write-date buffer) nil)))
|
|---|
| 512 | (buffer-start (buffer-point buffer))
|
|---|
| 513 | (setf (buffer-modified buffer) nil)
|
|---|
| 514 | (let ((stored-pathname (or probed-pathname
|
|---|
| 515 | (merge-pathnames pathname (default-directory)))))
|
|---|
| 516 | (setf (buffer-pathname buffer) stored-pathname)
|
|---|
| 517 | (setf (value pathname-defaults) stored-pathname)
|
|---|
| 518 | (process-file-options buffer stored-pathname)
|
|---|
| 519 | (invoke-hook read-file-hook buffer probed-pathname))))
|
|---|
| 520 |
|
|---|
| 521 |
|
|---|
| 522 | |
|---|
| 523 |
|
|---|
| 524 | ;;;; File writing.
|
|---|
| 525 |
|
|---|
| 526 | (defhvar "Add Newline at EOF on Writing File"
|
|---|
| 527 | "This controls whether WRITE-BUFFER-FILE adds a newline at the end of the
|
|---|
| 528 | file when it ends at the end of a non-empty line. When set, this may be
|
|---|
| 529 | :ask-user and WRITE-BUFFER-FILE will prompt; otherwise, just add one and
|
|---|
| 530 | inform the user. When nil, never add one and don't ask."
|
|---|
| 531 | :value :ask-user)
|
|---|
| 532 |
|
|---|
| 533 | (defhvar "Keep Backup Files"
|
|---|
| 534 | "When set, .BAK files will be saved upon file writing. This defaults to nil."
|
|---|
| 535 | :value nil)
|
|---|
| 536 |
|
|---|
| 537 | (defhvar "Write File Hook"
|
|---|
| 538 | "These functions are called when a buffer has been written. Each function
|
|---|
| 539 | must take the buffer as an argument.")
|
|---|
| 540 |
|
|---|
| 541 | (defun write-buffer-file (buffer pathname)
|
|---|
| 542 | "Write's buffer to pathname. This assumes pathname is somehow related to
|
|---|
| 543 | the buffer's pathname, and if the buffer's write date is not the same as
|
|---|
| 544 | pathname's, then this prompts the user for confirmation before overwriting
|
|---|
| 545 | the file. This consults \"Add Newline at EOF on Writing File\" and
|
|---|
| 546 | interacts with the user if necessary. This sets \"Pathname Defaults\", and
|
|---|
| 547 | the buffer is marked unmodified. The buffer's pathname and write date are
|
|---|
| 548 | updated, and the buffer is renamed according to the new pathname if possible.
|
|---|
| 549 | This invokes \"Write File Hook\"."
|
|---|
| 550 | (let ((buffer-pn (buffer-pathname buffer)))
|
|---|
| 551 | (let ((date (buffer-write-date buffer))
|
|---|
| 552 | (file-date (when (probe-file pathname) (file-write-date pathname))))
|
|---|
| 553 | (when (and buffer-pn date file-date
|
|---|
| 554 | (equal (make-pathname :version nil :defaults buffer-pn)
|
|---|
| 555 | (make-pathname :version nil :defaults pathname))
|
|---|
| 556 | (/= date file-date))
|
|---|
| 557 | (unless (prompt-for-yes-or-no :prompt (list
|
|---|
| 558 | "File has been changed on disk since it was read.~%Overwrite ~A anyway? "
|
|---|
| 559 | (namestring buffer-pn))
|
|---|
| 560 | :help
|
|---|
| 561 | "Type No to abort writing the file or Yes to overwrite the disk version."
|
|---|
| 562 | :default nil)
|
|---|
| 563 | (editor-error "Write aborted."))))
|
|---|
| 564 | (let ((val (value add-newline-at-eof-on-writing-file)))
|
|---|
| 565 | (when val
|
|---|
| 566 | (let ((end (buffer-end-mark buffer)))
|
|---|
| 567 | (unless (start-line-p end)
|
|---|
| 568 | (when (if (eq val :ask-user)
|
|---|
| 569 | (prompt-for-y-or-n
|
|---|
| 570 | :prompt
|
|---|
| 571 | (list "~A~%File does not have a newline at EOF, add one? "
|
|---|
| 572 | (buffer-name buffer))
|
|---|
| 573 | :default t)
|
|---|
| 574 | t)
|
|---|
| 575 | (insert-character end #\newline)
|
|---|
| 576 | (message "Added newline at EOF."))))))
|
|---|
| 577 | (setv pathname-defaults pathname)
|
|---|
| 578 | (write-file (buffer-region buffer) pathname)
|
|---|
| 579 | (let ((tn (truename pathname)))
|
|---|
| 580 | (message "~A written." (namestring tn))
|
|---|
| 581 | (setf (buffer-modified buffer) nil)
|
|---|
| 582 | (unless (equal tn buffer-pn)
|
|---|
| 583 | (setf (buffer-pathname buffer) tn))
|
|---|
| [810] | 584 | (setf (buffer-write-date buffer) (file-write-date tn))
|
|---|
| [12859] | 585 | (let ((name (pathname-to-buffer-name tn)))
|
|---|
| [6] | 586 | (unless (getstring name *buffer-names*)
|
|---|
| 587 | (setf (buffer-name buffer) name)))))
|
|---|
| [12859] | 588 | (invoke-hook write-file-hook buffer))
|
|---|
| [6] | 589 |
|
|---|
| [7502] | 590 | (defcommand "Write File" (p &optional (buffer (current-buffer)))
|
|---|
| [12859] | 591 | "Prompts for a filename, changes the buffer pathname to it and saves it.
|
|---|
| 592 | The prefix argument is ignored."
|
|---|
| [7502] | 593 | (declare (ignore p))
|
|---|
| 594 | (hemlock-ext:save-hemlock-buffer buffer :pathname :prompt))
|
|---|
| [12859] | 595 |
|
|---|
| [7502] | 596 | (defcommand "Save To File" (p &optional (buffer (current-buffer)))
|
|---|
| [6] | 597 | "Prompts for a filename and writes a copy of the buffer to it. Buffer's
|
|---|
| 598 | pathname (and modified state) is unchanged.
|
|---|
| [798] | 599 | The prefix argument is ignored."
|
|---|
| [6] | 600 | (declare (ignore p))
|
|---|
| [8428] | 601 | (hemlock-ext:save-hemlock-buffer buffer :pathname :prompt :copy t))
|
|---|
| [12859] | 602 |
|
|---|
| [6] | 603 | (defcommand "Save File" (p &optional (buffer (current-buffer)))
|
|---|
| 604 | "Writes the contents of the current buffer to the associated file. If there
|
|---|
| 605 | is no associated file, one is prompted for."
|
|---|
| 606 | (declare (ignore p))
|
|---|
| 607 | (when (buffer-modified buffer)
|
|---|
| 608 | (hemlock-ext:save-hemlock-buffer buffer)))
|
|---|
| 609 |
|
|---|
| 610 | (defhvar "Save All Files Confirm"
|
|---|
| 611 | "When non-nil, prompts for confirmation before writing each modified buffer."
|
|---|
| 612 | :value t)
|
|---|
| 613 |
|
|---|
| 614 | (defcommand "Save All Files" (p)
|
|---|
| 615 | "Saves all modified buffers in their associated files.
|
|---|
| 616 | If a buffer has no associated file it is ignored even if it is modified.."
|
|---|
| 617 | "Saves each modified buffer that has a file."
|
|---|
| 618 | (declare (ignore p))
|
|---|
| 619 | (let ((saved-count 0))
|
|---|
| 620 | (dolist (b *buffer-list*)
|
|---|
| 621 | (let ((pn (buffer-pathname b))
|
|---|
| 622 | (name (buffer-name b)))
|
|---|
| 623 | (when
|
|---|
| 624 | (and (buffer-modified b)
|
|---|
| 625 | pn
|
|---|
| 626 | (or (not (value save-all-files-confirm))
|
|---|
| 627 | (prompt-for-y-or-n
|
|---|
| 628 | :prompt (list
|
|---|
| 629 | "Write ~:[buffer ~A as file ~S~;file ~*~S~], ~
|
|---|
| 630 | Y or N: "
|
|---|
| 631 | (string= (pathname-to-buffer-name pn) name)
|
|---|
| 632 | name (namestring pn))
|
|---|
| 633 | :default t)))
|
|---|
| 634 | (write-buffer-file b pn)
|
|---|
| 635 | (incf saved-count))))
|
|---|
| 636 | (if (zerop saved-count)
|
|---|
| 637 | (message "No files were saved.")
|
|---|
| 638 | (message "Saved ~S file~:P." saved-count))))
|
|---|
| 639 |
|
|---|
| 640 | (defcommand "Backup File" (p)
|
|---|
| 641 | "Write the buffer to a file without changing the associated name."
|
|---|
| 642 | "Write the buffer to a file without changing the associated name."
|
|---|
| 643 | (declare (ignore p))
|
|---|
| 644 | (let ((file (prompt-for-file :prompt "Backup to File: "
|
|---|
| 645 | :help
|
|---|
| 646 | "Name of a file to backup the current buffer in."
|
|---|
| 647 | :default (buffer-default-pathname (current-buffer))
|
|---|
| 648 | :must-exist nil)))
|
|---|
| 649 | (write-file (buffer-region (current-buffer)) file)
|
|---|
| 650 | (message "~A written." (namestring (truename file)))))
|
|---|
| 651 |
|
|---|
| 652 |
|
|---|
| 653 | |
|---|
| 654 |
|
|---|
| 655 | ;;;; Buffer hacking commands:
|
|---|
| 656 |
|
|---|
| 657 |
|
|---|
| 658 | (defcommand "Buffer Not Modified" (p)
|
|---|
| [6572] | 659 | "Make the current buffer not modified."
|
|---|
| [6] | 660 | "Make the current buffer not modified."
|
|---|
| 661 | (declare (ignore p))
|
|---|
| 662 | (setf (buffer-modified (current-buffer)) nil)
|
|---|
| 663 | (message "Buffer marked as unmodified."))
|
|---|
| 664 |
|
|---|
| 665 |
|
|---|
| 666 |
|
|---|
| 667 | (defcommand "Set Buffer Read-Only" (p)
|
|---|
| 668 | "Toggles the read-only flag for the current buffer."
|
|---|
| 669 | "Toggles the read-only flag for the current buffer."
|
|---|
| 670 | (declare (ignore p))
|
|---|
| 671 | (let ((buffer (current-buffer)))
|
|---|
| 672 | (message "Buffer ~S is now ~:[read-only~;writable~]."
|
|---|
| 673 | (buffer-name buffer)
|
|---|
| 674 | (setf (buffer-writable buffer) (not (buffer-writable buffer))))))
|
|---|
| 675 |
|
|---|
| 676 | (defcommand "Set Buffer Writable" (p)
|
|---|
| 677 | "Make the current buffer modifiable."
|
|---|
| 678 | "Make the current buffer modifiable."
|
|---|
| 679 | (declare (ignore p))
|
|---|
| 680 | (let ((buffer (current-buffer)))
|
|---|
| 681 | (setf (buffer-writable buffer) t)
|
|---|
| 682 | (message "Buffer ~S is now writable." (buffer-name buffer))))
|
|---|
| 683 |
|
|---|