Module: deuce-internals Synopsis: The Deuce editor Author: Scott McKay Copyright: Original Code is Copyright (c) 1995-2004 Functional Objects, Inc. All rights reserved. License: Functional Objects Library Public License Version 1.0 Dual-license: GNU Lesser General Public License Warranty: Distributed WITHOUT WARRANTY OF ANY KIND /// Buffers define protocol <> (<>) getter buffer-name (buffer :: ) => (name :: ); setter buffer-name-setter (name :: , buffer :: ) => (name :: ); getter buffer-pathname (buffer :: ) => (pathname :: false-or()); setter buffer-pathname-setter (pathname :: false-or(), buffer :: ) => (pathname :: false-or()); getter buffer-default-pathname (buffer :: ) => (pathname :: ); getter buffer-source-container (buffer :: ) => (container :: false-or()); setter buffer-source-container-setter (container :: false-or(), buffer :: ) => (container :: false-or()); getter buffer-start-node (buffer :: ) => (node :: false-or()); setter buffer-start-node-setter (node :: false-or(), buffer :: ) => (node :: false-or()); getter buffer-end-node (buffer :: ) => (node :: false-or()); setter buffer-end-node-setter (node :: false-or(), buffer :: ) => (node :: false-or()); getter buffer-lock (buffer :: ) => (lock :: false-or()); getter buffer-read-only? (buffer :: ) => (read-only? :: ); setter buffer-read-only?-setter (read-only? :: , buffer :: ) => (read-only? :: ); getter file-buffer? (buffer :: ) => (file? :: ); getter saves-like-file-buffer? (buffer :: ) => (saves? :: ); getter special-purpose-buffer? (buffer :: ) => (special-purpose? :: ); getter buffer-anonymous? // could also be called 'buffer-invisible?' (buffer :: ) => (anonymous? :: ); getter buffer-section-separator-style (buffer :: ) => (style :: ); getter buffer-major-mode (buffer :: ) => (mode :: ); setter buffer-major-mode-setter (mode :: , buffer :: ) => (mode :: ); getter buffer-minor-modes (buffer :: ) => (modes :: ); setter buffer-minor-modes-setter (modes :: , buffer :: ) => (modes :: ); getter buffer-undo-history (buffer :: , #key section :: false-or(
)) => (history :: false-or(), buffer :: false-or()); function add-node! (buffer :: , node :: , #key after) => (); function remove-node! (buffer :: , node :: ) => (); getter buffer-modification-tick (buffer :: ) => (tick :: ); setter buffer-modification-tick-setter (tick :: , buffer :: ) => (tick :: ); getter buffer-save-tick (buffer :: ) => (tick :: ); setter buffer-save-tick-setter (tick :: , buffer :: ) => (tick :: ); getter buffer-modified? (buffer :: ) => (modified? :: ); setter buffer-modified?-setter (modified? :: , buffer :: ) => (modified? :: ); getter buffer-properties (buffer :: ) => (properties :: ); setter buffer-properties-setter (properties :: , buffer :: ) => (properties :: ); getter buffer-contents-properties (buffer :: ) => (properties :: ); setter buffer-contents-properties-setter (properties :: , buffer :: ) => (properties :: ); getter buffer-associated-buffers (buffer :: ) => (buffers :: ); setter buffer-associated-buffers-setter (buffers :: , buffer :: ) => (buffers :: ); function note-buffer-changed (buffer :: ) => (); getter buffer-has-hard-sections? (buffer :: ) => (hard-sections? :: ); getter buffer-contains-section? (buffer :: , section ::
) => (contains? :: ); getter buffer-initial-point (buffer :: , #key point :: false-or()) => (bp :: false-or()); getter buffer-initial-mark (buffer :: , #key mark :: false-or()) => (bp :: false-or()); getter buffer-initial-line (buffer :: , #key line :: false-or()) => (line :: false-or()); // Higher level stuff function sectionize-buffer (buffer :: ) => (sectionized? :: ); function revert-buffer (buffer :: , #key buffer-filler :: false-or(), major-mode) => (reverted? :: ); function save-buffer (buffer :: , #key frame, editor) => (pathname :: false-or(), condition); // #f => failed to save function save-buffer-as (container-class, buffer :: , pathname :: , #key frame, editor, format, if-exists) => (pathname :: false-or(), condition); // #f => failed to save function kill-buffer (buffer :: , #key frame, editor, no-exit-frame) => (); function gc-buffer (buffer :: ) => (); // Navigation function line-next-in-buffer (line :: , buffer :: false-or(), #key skip-test) => (next :: false-or()); function line-previous-in-buffer (line :: , buffer :: false-or(), #key skip-test) => (prev :: false-or()); end protocol <>; // A buffer is used to group some set of data, and can be displayed in // zero or more windows. It contains a linked list of nodes which get // created by some sort of generating function. define open abstract primary class () sealed slot buffer-name :: = "", setter: %buffer-name-setter, init-keyword: name:; sealed slot buffer-start-node :: false-or() = #f, init-keyword: start-node:; sealed slot buffer-end-node :: false-or() = #f, init-keyword: end-node:; sealed slot buffer-read-only? :: = #f, setter: %read-only?-setter, init-keyword: read-only?:; // Buffer modes sealed slot buffer-major-mode :: , required-init-keyword: major-mode:; sealed slot buffer-minor-modes :: = make(), init-keyword: minor-modes:; sealed slot buffer-major-mode-undo-list :: = #(); // The buffer is considered changed when the modification tick is greater // than the save tick. They are both initialized to the same value when // the buffer is reverted. sealed slot buffer-modification-tick :: = *tick*; sealed slot buffer-save-tick :: = *tick*; sealed slot buffer-properties :: = #(), init-keyword: properties:; sealed slot buffer-contents-properties :: = #(); // All of the buffers associated with this buffer, e.g., a file buffer // might have a list of all the composite buffers built from its sections sealed slot buffer-associated-buffers :: = #(); sealed constant slot buffer-lock :: false-or() = #f, init-keyword: lock:; end class ; define method initialize (buffer :: , #key editor = frame-editor(*editor-frame*)) => () next-method(); let buffers = editor-buffers(editor); add!(buffers, buffer) end method initialize; // The currently selected buffer in "this" editor frame // This is always kept in sync with 'frame-buffer(*editor-frame*)' // The type is 'false-or' for bootstrapping reasons... define thread variable *buffer* :: false-or() = #f; // The default pathname to use for new file and save file dialogs // This function exists because it's meant to return a default that // won't cause these dialogs to explode, as would otherwise happen // for hairy composite buffer names define method buffer-default-pathname (buffer :: ) => (pathname :: ) buffer-pathname(buffer) | buffer-name(buffer) end method buffer-default-pathname; define method buffer-name-setter (name :: , buffer :: ) => (name :: ) %buffer-name(buffer) := name; display-buffer-name-everywhere(buffer); name end method buffer-name-setter; define method buffer-modified? (buffer :: ) => (modified? :: ) buffer-save-tick(buffer) < buffer-modification-tick(buffer) end method buffer-modified?; define method buffer-modified?-setter (modified? :: , buffer :: ) => (modified? :: ) unless (modified?) //---*** How do we propagate "unmodifications" to the source container? buffer-save-tick(buffer) := buffer-modification-tick(buffer); note-buffer-changed-everywhere(buffer, #f) end; modified? end method buffer-modified?-setter; define method note-buffer-changed (buffer :: ) => () // Avoid work if the buffer is already marked modified unless (buffer-modified?(buffer)) buffer-modification-tick(buffer) := tick(); note-buffer-changed-everywhere(buffer, #t) end end method note-buffer-changed; define method buffer-read-only?-setter (read-only? :: , buffer :: ) => (read-only? :: ) unless (buffer-read-only?(buffer) == read-only?) buffer.%read-only? := read-only?; note-buffer-read-only-everywhere(buffer, read-only?) end; read-only? end method buffer-read-only?-setter; // Notify every window that a buffer has changed its modification state define method note-buffer-changed-everywhere (buffer :: , modified? :: ) => () do-associated-windows (window :: = *editor-frame*) when (window-buffer(window) == buffer) window-note-buffer-changed(window, buffer, modified?) end end end method note-buffer-changed-everywhere; // Notify every window that a buffer has changed its read-only state define method note-buffer-read-only-everywhere (buffer :: , read-only? :: ) => () do-associated-windows (window :: = *editor-frame*) when (window-buffer(window) == buffer) window-note-buffer-read-only(window, buffer, read-only?) end end end method note-buffer-read-only-everywhere; // Display the name of a buffer in every window showing it define sealed method display-buffer-name-everywhere (buffer :: ) => () do-associated-windows (window :: = *editor-frame*) when (window-buffer(window) == buffer) display-buffer-name(window, buffer) end end end method display-buffer-name-everywhere; define method buffer-section-separator-style (buffer :: ) => (style :: ) #"requested" end method buffer-section-separator-style; define sealed method interval-start-bp (buffer :: ) => (bp :: false-or()) let node = buffer-start-node(buffer); node & interval-start-bp(node) end method interval-start-bp; define sealed method interval-end-bp (buffer :: ) => (bp :: false-or()) let node = buffer-end-node(buffer); node & interval-end-bp(node) end method interval-end-bp; define method buffer-initial-point (buffer :: , #key point :: false-or() = #f) => (bp :: false-or()) point | interval-start-bp(buffer) end method buffer-initial-point; define method buffer-initial-mark (buffer :: , #key mark :: false-or() = #f) => (bp :: false-or()) mark end method buffer-initial-mark; define method buffer-initial-line (buffer :: , #key line :: false-or() = #f) => (line :: false-or()) line | bp-line(buffer-initial-point(buffer)) end method buffer-initial-line; define method revert-buffer (buffer :: , #key buffer-filler :: false-or() = #f, major-mode) => (reverted? :: ) ignore(buffer-filler, major-mode); error("There is no default method for 'revert-buffer'") end method revert-buffer; define sealed method sectionize-buffer (buffer :: ) => (sectionized? :: ) // If the buffer has hard sections, those sections are definitive, // so don't go sectionizing it unless (buffer-has-hard-sections?(buffer)) dynamic-bind (*buffer* = buffer) do-sectionize-buffer(buffer-major-mode(buffer), buffer) end end end method sectionize-buffer; define sealed method resectionize-changed-sections (buffer :: ) => (sectionized? :: ) let sectionized? = #f; for (node = buffer-start-node(buffer) then node-next(node), until: ~node) let section = node-section(node); when (section) sectionized? := resectionize-section(section) | sectionized? end end; sectionized? end method resectionize-changed-sections; define method buffer-contains-section? (buffer :: , section :: ) => (contains? :: ) block (return) for (node = buffer-start-node(buffer) then node-next(node), until: ~node) when (node-section(node) == section) return(#t) end end; #f end end method buffer-contains-section?; // It's meaningless to save anything but file buffers... define method save-buffer (buffer :: , #key frame = *editor-frame*, editor) => (pathname :: false-or(), condition) ignore(frame, editor); values(#f, #f) end method save-buffer; // ... but "Save As" works on just about anything define method save-buffer-as (container-class == , buffer :: , pathname :: , #key frame = *editor-frame*, editor, format, if-exists = #"signal") => (pathname :: false-or(), condition) ignore(editor, format); dynamic-bind (*buffer* = buffer) let window = frame-window(frame); let pathname :: false-or() = pathname; // so we can set it to #f below let condition = #f; for (n :: from 0 below 2) block () with-open-file (stream = pathname, direction: #"output", if-exists: if-exists) local method dump (line :: , si, ei, last?) ignore(si, ei, last?); //--- Uncomment this if we decide to GC lines on save // gc-line(line); dump-line(line, stream) end method; do-lines(dump, buffer); n := 2 // terminate the loop end exception () let save? = (n = 0 & window) & yes-or-no-dialog(window, "%s already exists.\nDo you want to overwrite it?", as(, pathname | "The file")); case save? => if-exists := #"new-version"; // try again, but this time overwrite n = 0 => n := 2; // give up now pathname := #f; otherwise => warning-dialog(window, "Couldn't save %s.\nThe file already exists.", as(, pathname | "the file")); pathname := #f; end; exception (c :: ) warning-dialog(window, "Couldn't save %s.\n%s", as(, pathname | "the file"), condition-to-string(c)); n := 2; pathname := #f; condition := c; end end; when (pathname & file-buffer?(buffer)) buffer-pathname(buffer) := pathname; container-modification-date(buffer-source-container(buffer)) := get-file-property(pathname, #"modification-date", default: current-date()) end; values(pathname, condition) end end method save-buffer-as; // In the one-frame-per-buffer policy, we want to exit when we kill the // buffer. 'exit-editor?' is necessary to prevent infinite recursion // the buffer gets killed via File->Exit as opposed to File->Close. define method kill-buffer (buffer :: , #key frame = *editor-frame*, editor, no-exit-frame :: false-or() = #f) => () let editor = editor | frame-editor(frame); // Call 'exit-mode' to undo any side-effects, and revert to fundamental // mode in case we fail to kill the buffer exit-mode(buffer, buffer-major-mode(buffer)); enter-mode(buffer, find-mode()); // Clean up all the pointers to the nodes in this buffer for (node = buffer-start-node(buffer) then node-next(node), until: ~node) let section = node-section(node); when (section) //--- Should this clean up 'line-bps' of all the lines in the section? section-nodes(section) := remove!(section-nodes(section), node); end end; // Remove any BP's that point into this buffer for (bp keyed-by register in $register-point-table) when (~simple-bp?(bp) & bp-buffer(bp) == buffer) kill-bp!(bp); remove-key!($register-point-table, register) end end; // Remove all other references to this buffer let buffers = remove!(editor-buffers(editor), buffer); do-associated-windows (window :: = frame) let selected-buffers = window-selected-buffers(window); let entry = find-value(selected-buffers, method (s) selection-buffer(s) == buffer end); when (entry) remove!(selected-buffers, entry) end; // If this window's buffer is the one we're killing, we must do something when (buffer == window-buffer(window)) let policy = editor-policy(editor); if (fixed-frame-buffer?(policy)) // One-frame-per-buffer, so exit this window now, unless it will be // exited on our return let frame = window-frame(window); unless (frame == no-exit-frame) exit-editor(frame) end; else // If this window isn't going away, we need a new buffer for it let new-buffer = if (empty?(selected-buffers)) // If it's the last buffer from those ever shown in this // window, try a non-anonymous buffer from any old window. // If there are no other buffers at all, create a new one. let new-buffer = find-value(buffers, method (b) ~buffer-anonymous?(b) end); new-buffer | make-initial-buffer(editor: editor) else // Otherwise, choose buffer most recently shown in this window selection-buffer(selected-buffers[0]) end; select-buffer(window, new-buffer); queue-redisplay(window, $display-all) end end end end method kill-buffer; define method gc-buffer (buffer :: ) => () //--- Can we arrange to GC only the nodes that have been modified? do-lines(gc-line, buffer) end method gc-buffer; /// Non-file buffers // Mixin for buffers that contain random textual/graphical data, // but are not associated with a source container define open abstract class () constant slot buffer-anonymous? :: = #f, init-keyword: anonymous?:; sealed constant slot %undo-history :: = make(); end class ; define method buffer-modified? (buffer :: ) => (modified? :: ) // If there's any data in the buffer, claim that it's modified, // irrespective of the state of the buffer ticks interval-start-bp(buffer) ~= interval-end-bp(buffer) end method buffer-modified?; define method buffer-undo-history (buffer :: , #key section :: false-or(
)) => (history :: false-or(), buffer :: false-or()) ignore(section); values(buffer.%undo-history, buffer) end method buffer-undo-history; define method buffer-has-hard-sections? (buffer :: ) => (hard-sections? :: ) #f end method buffer-has-hard-sections?; define method revert-buffer (buffer :: , #key buffer-filler :: false-or() = #f, major-mode) => (reverted? :: ) ignore(buffer-filler, major-mode); #f end method revert-buffer; define sealed class (, ) end class ; define sealed domain make (singleton()); define sealed domain initialize (); define sealed class (, ) end class ; define method buffer-modified? (buffer :: ) => (modified? :: ) #f end method buffer-modified?; define sealed domain make (singleton()); define sealed domain initialize (); define variable *untitled-buffer-count* :: = 0; define method make-empty-buffer (buffer-class :: subclass(), #rest buffer-initargs, #key buffer, name, major-mode = find-mode(), section-class =
, node-class = , editor = frame-editor(*editor-frame*), #all-keys) => (buffer :: ) ignore(editor); unless (name) inc!(*untitled-buffer-count*); name := format-to-string("Untitled %d", *untitled-buffer-count*) end; with-keywords-removed (buffer-initargs = buffer-initargs, #[name:, major-mode:, section-class:, node-class:]) let buffer = buffer | apply(make, buffer-class, name: name, major-mode: major-mode, buffer-initargs); let node = make-empty-section-node(buffer, section-class: section-class, node-class: node-class); node-buffer(node) := buffer; buffer-start-node(buffer) := node; buffer-end-node(buffer) := node; buffer end end method make-empty-buffer; define function make-initial-buffer (#key editor = frame-editor(*editor-frame*)) => (buffer :: ) let name = "Initial buffer"; let buffer = find-buffer(editor, name) | make-empty-buffer(, name: name, editor: editor); buffer end function make-initial-buffer; /// File buffers // Mixin for buffers that contain textual/graphical data gotten by // reading the contents of some source container. The name is a bit // inaccurate, but hey, I'm a dinosaur. define open abstract class () sealed slot buffer-source-container :: false-or() = #f, init-keyword: container:; end class ; define method initialize (buffer :: , #key name) => () next-method(); unless (name) // Create a reasonable buffer name from the source container let container = buffer-source-container(buffer); when (container) buffer-name(buffer) := pathname->buffer-name(container-pathname(container)) end end end method initialize; define function pathname->buffer-name (pathname :: ) => (name :: ) let locator = as(, pathname); let directory = locator-directory(locator); let name = locator-name(locator); if (directory) concatenate(name, " (", as(, directory), ")") else name end end function pathname->buffer-name; // Buffers are non-file buffers by default define method file-buffer? (buffer :: ) => (file-buffer? :: singleton(#f)) #f end method file-buffer?; define sealed inline method file-buffer? (buffer :: ) => (file-buffer? :: singleton(#t)) #t end method file-buffer?; // By default, if it's a file buffer, it saves out like a file buffer define method saves-like-file-buffer? (buffer :: ) => (saves? :: ) file-buffer?(buffer) end method saves-like-file-buffer?; define method buffer-anonymous? (buffer :: ) => (anonymous? :: ) #f end method buffer-anonymous?; // The pathname of a file buffer is its container's pathname, // if it has a container define sealed method buffer-pathname (buffer :: ) => (pathname :: false-or()) let container = buffer-source-container(buffer); container & container-pathname(container) end method buffer-pathname; define sealed method buffer-pathname-setter (pathname :: , buffer :: ) => (pathname :: ) let container = buffer-source-container(buffer); when (container) container-pathname(container) := pathname end; buffer-name(buffer) := pathname->buffer-name(pathname); // Compare the current mode with the one for the new pathname let old-mode = buffer-major-mode(buffer); let new-mode = find-mode-from-pathname(pathname); unless (old-mode == new-mode) // If they're different, update the buffer and any windows showing it //--- Maybe this could share code with 'revert-buffer'? exit-mode(buffer, old-mode); enter-mode(buffer, new-mode); do-associated-windows (window :: = *editor-frame*) when (window-buffer(window) == buffer) window-note-mode-entered(window, new-mode); // Changing modes might change the display arbitrarily, so refresh queue-redisplay(window, $display-all) end end end; pathname end method buffer-pathname-setter; // Default methods just return #f... define method buffer-pathname (buffer :: ) => (pathname :: singleton(#f)) #f end method buffer-pathname; define method buffer-source-container (buffer :: ) => (container :: singleton(#f)) #f end method buffer-source-container; // File buffers use the lock for the source container define sealed inline method buffer-lock (buffer :: ) => (lock :: false-or()) let container = buffer-source-container(buffer); container & container-lock(container) end method buffer-lock; define sealed method buffer-undo-history (buffer :: , #key section :: false-or(
)) => (history :: false-or(), buffer :: false-or()) ignore(section); let container = buffer-source-container(buffer); values(container & container-undo-history(container), buffer) end method buffer-undo-history; // A file buffer has hard sections if its source container does define method buffer-has-hard-sections? (buffer :: ) => (hard-sections? :: ) let container = buffer-source-container(buffer); container & container-has-hard-sections?(container) end method buffer-has-hard-sections?; // We can do this much faster for file buffers... define sealed method buffer-contains-section? (buffer :: , section :: ) => (contains? :: ) section-container(section) == buffer-source-container(buffer) end method buffer-contains-section?; define sealed method note-buffer-changed (buffer :: ) => () // Don't do any work if the buffer is already modified... unless (buffer-modified?(buffer)) when (*editor-frame*) let window = frame-window(*editor-frame*); // If the container changed on disk the first time we increment // the modification tick, offer to revert the buffer let reverted? = revert-buffer-if-necessary(buffer, window: window); when (reverted?) // Redisplay, then back to the command loop if we reverted redisplay-window(window); signal(make(, window: window)) end end; // The method on will increment the modification tick // and notify all the windows... next-method() end end method note-buffer-changed; define method revert-buffer-if-necessary (buffer :: , #key window) => (reverted? :: ) ignore(window); #f end method revert-buffer-if-necessary; // Offer to revert the buffer iff it has changed on disk define method revert-buffer-if-necessary (buffer :: , #key window) => (reverted? :: ) let window = window | frame-window(*editor-frame*); let container = buffer-source-container(buffer); let pathname = container-pathname(container); if (pathname & file-exists?(pathname)) let cdate = container-modification-date(container); let fdate = get-file-property(pathname, #"modification-date", default: current-date()); when (cdate & cdate ~= fdate) when (yes-or-no-dialog(window, "%s has been modified on disk.\nDo you want to re-read it?", as(, pathname))) let line = bp->line-index(point()); when (revert-buffer(buffer)) restore-previous-position(buffer, window, line); queue-redisplay(window, $display-all) end; #t end end else warning-dialog(window, "%s no longer exists on disk.", as(, pathname)); #f end end method revert-buffer-if-necessary; define method revert-buffer (buffer :: , #key buffer-filler :: false-or() = fill-file-buffer, major-mode) => (reverted? :: ) // Exit the old mode and ensure we are in fundamental mode let old-mode = buffer-major-mode(buffer); exit-mode(buffer, old-mode); enter-mode(buffer, find-mode()); // Reset the timestamps on the buffer let tick = tick(); buffer-modification-tick(buffer) := tick; buffer-save-tick(buffer) := tick; buffer-contents-properties(buffer) := #(); // Reset the buffer's (i.e., the container's) undo history let history = buffer-undo-history(buffer); when (history) reset-undo-history(history) end; // Now go read the contents of the buffer when (buffer-filler) buffer-filler(buffer) end; let container = buffer-source-container(buffer); buffer-read-only?(buffer) := container-read-only?(container); // Determine the major mode of the buffer, and go sectionize it let mode = major-mode | find-mode-from-pathname(container-pathname(container)); enter-mode(buffer, mode); do-associated-windows (window :: = *editor-frame*) when (window-buffer(window) == buffer) unless (mode == old-mode) window-note-mode-entered(window, mode) end; window-note-buffer-selected(window, buffer) end end; do-sectionize-buffer(mode, buffer); #t end method revert-buffer; define method fill-file-buffer (buffer :: ) => () read-container-contents(buffer-source-container(buffer), buffer) end method fill-file-buffer; // Save the buffer to its home source container, first renaming the // existing file to a backup pathname define method save-buffer (buffer :: , #key frame = *editor-frame*, editor) => (pathname :: false-or(), condition) ignore(editor); let container = buffer-source-container(buffer); let pathname = container-pathname(container); let namestring :: = as(, pathname); let cdate = container-modification-date(container); let fdate = get-file-property(pathname, #"modification-date", default: current-date()); // Throw back to the command loop if the user declines to save. // (We mention the full pathname instead of the basic name in case this // happens in the middle of "save-all".) when (cdate & cdate ~= fdate) let window = frame-window(frame); unless (yes-or-no-dialog(window, "%s has been modified on disk.\nDo you want to save it anyway?", as(, pathname))) signal(make(, window: window)) end end; // Try to rename the original file to the backup pathname, // ignoring any file system error we might encounter let backup :: false-or() = backup-file-namestring(pathname); when (backup) block () rename-file(namestring, backup, if-exists: #"replace") exception () #f end end; // Finally, write out the contents of the buffer let (pathname, condition) = save-buffer-as(source-container-class(pathname), buffer, pathname, frame: frame, if-exists: #"new-version"); when (pathname) buffer-modified?(buffer) := #f end; values(pathname, condition) end method save-buffer; //---*** Locators should provide a portable way to generate backup pathnames define function backup-file-namestring (pathname :: ) => (backup-name :: false-or()) concatenate(as(, pathname), "~") end function backup-file-namestring; define method kill-buffer (buffer :: , #key frame = *editor-frame*, editor, no-exit-frame) => () ignore(frame, editor, no-exit-frame); next-method(); let container = buffer-source-container(buffer); when (container) container-buffers(container) := remove!(container-buffers(container), buffer) end end method kill-buffer; define sealed class (, ) end class ; define sealed domain make (singleton()); define sealed domain initialize (); /// Special purpose buffers // Mixin for buffers that contain "special purpose" data, e.g., // a "List Callers", "Methods", or "Shell" buffer define open abstract class () sealed slot buffer-modified? :: = #f; sealed slot buffer-anonymous? :: = #f, init-keyword: anonymous?:; sealed constant slot %undo-history :: false-or() = #f, init-keyword: undo-history:; end class ; // By default, buffers are not special-purpose define method special-purpose-buffer? (buffer :: ) => (special-purpose? :: singleton(#f)) #f end method special-purpose-buffer?; define sealed inline method special-purpose-buffer? (buffer :: ) => (special-purpose? :: singleton(#t)) #t end method special-purpose-buffer?; define method buffer-undo-history (buffer :: , #key section :: false-or(
)) => (history :: false-or(), buffer :: false-or()) let history = buffer.%undo-history; if (history) values(history, buffer) else // This is where things get a bit wierd! We return the history // for the container and its home buffer as well. This is so that // 'undo!' and 'redo!' can establish the right binding for *buffer*, // so that temporary BPs in change records don't end up getting an // unexpected value for 'bp-buffer'. Yow! let container = section & buffer-has-hard-sections?(buffer) & section-container(section); let history = container & container-undo-history(container); let buffer = history & section-home-buffer(section); values(history, buffer) end end method buffer-undo-history; // By default, special purpose buffers have hard sections define method buffer-has-hard-sections? (buffer :: ) => (hard-sections? :: ) #t end method buffer-has-hard-sections?; define method note-buffer-changed (buffer :: ) => () buffer-modified?(buffer) := #t; next-method() end method note-buffer-changed; define open abstract class (, ) end class ; /// Buffer coercions define method as-file-buffer (class :: subclass(), buffer :: , pathname :: , editor :: ) => (file-buffer :: ) buffer end method as-file-buffer; define method as-file-buffer (class :: subclass(), buffer :: , pathname :: , editor :: ) => (file-buffer :: ) let container :: = find-source-container(editor, source-container-class(pathname), pathname); let new-buffer :: = make-empty-buffer(, name: pathname->buffer-name(pathname), major-mode: find-mode(), container: container, editor: editor); local method move-lines (new-buffer :: ) // There's one node and one section in the new buffer -- // fill it in from the old buffer let new-node = buffer-start-node(new-buffer); let new-section = node-section(buffer-start-node(new-buffer)); // First move the lines from the old buffer let first-line :: = bp-line(interval-start-bp(buffer)); let last-line :: = bp-line(interval-end-bp(buffer)); let line :: false-or() = first-line; let next :: false-or() = #f; while (line) next := line-next-in-buffer(line, buffer); line-section(line) := new-section; line := next end; // Then fix up the new buffer's node and section section-start-line(new-section) := first-line; section-end-line(new-section) := last-line; interval-start-bp(new-node) := make(, line: first-line, index: 0, buffer: new-buffer); interval-end-bp(new-node) := make(, line: last-line, index: line-length(last-line), buffer: new-buffer, moving?: #t) end method; revert-buffer(new-buffer, buffer-filler: move-lines); new-buffer end method as-file-buffer; /// Navigation within buffers define method do-lines (function :: , buffer :: , #key from-end? = #f, skip-test = line-for-display-only?) => () let (start-node, end-node, step :: ) = if (from-end?) values(buffer-end-node(buffer), buffer-start-node(buffer), node-previous) else values(buffer-start-node(buffer), buffer-end-node(buffer), node-next) end; block (break) for (node = start-node then step(node)) when (node) do-lines(function, node, from-end?: from-end?, skip-test: skip-test) end; when (~node | node == end-node) break() end end end end method do-lines; define method count-lines (buffer :: , #key skip-test = line-for-display-only?, cache-result? = #f) => (nlines :: ) let start-node = buffer-start-node(buffer); let end-node = buffer-end-node(buffer); let n :: = 0; block (break) for (node = start-node then node-next(node)) inc!(n, count-lines(node, skip-test: skip-test, cache-result?: cache-result?)); when (node == end-node) break() end end end; n end method count-lines; define method as (class :: subclass(), buffer :: ) => (string :: ) let bp1 = interval-start-bp(buffer); let bp2 = interval-end-bp(buffer); if (bp1 & bp2) as(, make-interval(bp1, bp2, in-order?: #t)) else "" end end method as; /// Line navigation within buffers // 'line-next' returns #f for the last line in a section, so you need to // use 'line-next-in-buffer' to navigate through the lines of a buffer. // If a skip-test is supplied, any lines satisfying that test are skipped. define method line-next-in-buffer (line :: , buffer :: false-or(), #key skip-test = line-for-display-only?) => (next :: false-or()) let next-line = line-next(line) | when (buffer) let node = line-node(line, buffer: buffer); let next-node = node & node-next(node); next-node & bp-line(interval-start-bp(next-node)) end; if (next-line & skip-test & skip-test(next-line)) line-next-in-buffer(next-line, buffer, skip-test: skip-test) else next-line end end method line-next-in-buffer; // Same deal as 'line-next-in-buffer' define method line-previous-in-buffer (line :: , buffer :: false-or(), #key skip-test = line-for-display-only?) => (previous :: false-or()) let prev-line = line-previous(line) | when (buffer) let node = line-node(line, buffer: buffer); let prev-node = node & node-previous(node); prev-node & bp-line(interval-end-bp(prev-node)) end; if (prev-line & skip-test & skip-test(prev-line)) line-previous-in-buffer(prev-line, buffer, skip-test: skip-test) else prev-line end end method line-previous-in-buffer; // Returns #t iff line1 is before line2 in the buffer define method line-less? (buffer :: , line1 :: , line2 :: ) => (less? :: ) block (return) let section1 :: false-or() = line-section(line1); let section2 :: false-or() = line-section(line2); case section1 == section2 => // This catches the case where both sections are #f //--- Note that this might be a bad idea! for (line = line1 then line-next(line), until: ~line) when (line == line2) return(#t) end end; #f; section1 & section2 => let section1 :: = section1; // force tighter type... let section2 :: = section2; // force tighter type... // line1 precedes line2 if its section precedes line2's // section in the buffer section-less?(buffer, section1, section2); otherwise => assert(section1 & section2, "The lines %= and %= are in completely unrelated sections in 'line-less?'", line1, line2); end end end method line-less?; // Returns #t iff section1 is before section2 in the buffer define method section-less? (buffer :: , section1 :: , section2 :: ) => (less? :: ) block (return) for (node = section-node(section1, buffer: buffer) then node-next(node), until: ~node) when (node-section(node) == section2) return(#t) end end; #f end end method section-less?; // Given a section and a buffer, return the section's node within the buffer // Returns #f if the section is not in the buffer define method section-node (section :: , #key buffer = *buffer*) => (node :: false-or()) block (return) for (node :: in section-nodes(section)) when (node-buffer(node) == buffer) return(node) end end; #f end end method section-node; define method line-node (line :: , #key buffer = *buffer*) => (node :: false-or()) let section = line-section(line); section & section-node(section, buffer: buffer) end method line-node; define method bp-node (bp :: ) => (node :: false-or()) line-node(bp-line(bp), buffer: bp-buffer(bp)) end method bp-node; /// Adding and removing nodes to buffers define sealed method add-node! (buffer :: , node :: , #key after :: type-union(, one-of(#f, #"start", #"end")) = #"end") => () assert(~node-buffer(node), "The node %= is already in the buffer %=", node, node-buffer(node)); let section = node-section(node); when (section) for (n :: in section-nodes(section)) assert(n == node | node-buffer(n) ~== buffer, "The section for node %= is already in the buffer %=", node, node-buffer(node)) end end; let (next, prev) = select (after) #f, #"start" => values(buffer-start-node(buffer), #f); #"end" => values(#f, buffer-end-node(buffer)); otherwise => assert(node-buffer(after) == buffer, "The 'after' node %= is not in the buffer %=", after, buffer); values(node-next(after), after); end; node-buffer(node) := buffer; node-next(node) := next; node-previous(node) := prev; if (next) node-previous(next) := node else buffer-end-node(buffer) := node end; if (prev) node-next(prev) := node else buffer-start-node(buffer) := node end; update-buffer-line-count(buffer, node) end method add-node!; define sealed method remove-node! (buffer :: , node :: ) => () assert(node-buffer(node) == buffer, "The node %= is not in the buffer %=", node, buffer); let (next, prev) = values(node-next(node), node-previous(node)); if (next) node-previous(next) := prev else buffer-end-node(buffer) := prev end; if (prev) node-next(prev) := next else buffer-start-node(buffer) := next end; node-buffer(node) := #f; node-next(node) := #f; node-previous(node) := #f; update-buffer-line-count(buffer, node) end method remove-node!; define sealed method update-buffer-line-count (buffer :: , node :: ) => () // Update every window which has a buffer containing this node do-associated-windows (window :: = *editor-frame*) when (window-buffer(window) = buffer) // We cache section line counts, so throwing info away won't really be slow window-total-lines(window) := #f end end end method update-buffer-line-count; /// Mode syntax tables define sealed inline method word-syntax-table (buffer :: ) => (table :: ) word-syntax-table(buffer-major-mode(buffer)) end method word-syntax-table; define inline function word-syntax (char :: ) => (syntax :: ) character-syntax(char, word-syntax-table(buffer-major-mode(*buffer*))) end function word-syntax; define sealed inline method atom-syntax-table (buffer :: ) => (table :: ) atom-syntax-table(buffer-major-mode(buffer)) end method atom-syntax-table; define inline function atom-syntax (char :: ) => (syntax :: ) character-syntax(char, atom-syntax-table(buffer-major-mode(*buffer*))) end function atom-syntax; define sealed inline method list-syntax-table (buffer :: ) => (table :: ) list-syntax-table(buffer-major-mode(buffer)) end method list-syntax-table; define inline function list-syntax (char :: ) => (syntax :: ) character-syntax(char, list-syntax-table(buffer-major-mode(*buffer*))) end function list-syntax;