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 /// Windows define constant $default-window-border = 0; define constant $default-window-line-spacing = 1; define protocol <> () getter window-buffer (window :: ) => (buffer :: false-or()); setter window-buffer-setter (buffer :: false-or(), window :: ) => (buffer :: false-or()); getter window-point (window :: ) => (point :: ); setter window-point-setter (point :: , window :: ) => (point :: ); getter window-mark (window :: ) => (mark :: false-or()); setter window-mark-setter (mark :: false-or(), window :: ) => (mark :: false-or()); getter window-temporary-mark? (window :: ) => (temporary? :: type-union(, )); setter window-temporary-mark?-setter (temporary? :: type-union(, ), window :: ) => (temporary? :: type-union(, )); getter window-hide-section-separators? (window :: ) => (hide? :: ); getter line-visible-in-window? (line :: , window :: ) => (visible? :: ); // Buffer selection function select-buffer (window :: , buffer :: ) => (); function select-buffer-in-appropriate-window (window :: , buffer :: , #key line, index) => (); // Notifications function window-note-mode-entered (window :: , mode :: ) => (); function window-note-buffer-changed (window :: , buffer :: , modified? :: ) => (); function window-note-buffer-read-only (window :: , buffer :: , read-only? :: ) => (); function window-note-buffer-selected (window :: , buffer :: false-or()) => (); function window-note-selection-changed (window :: , mark :: false-or()) => (); function window-note-search-string (window :: , string :: false-or()) => (); function window-note-undo/redo (window :: , undo? :: , redo? :: ) => (); function window-note-policy-changed (window :: , new-policy :: , old-policy :: false-or()) => (); end protocol <>; // A window is an abstract model of the intuitive notion of a window. // A buffer gets displayed in a window. // Deuce back-ends are expected to supply a concrete implementation class // that is a subclass of . define open abstract class () // The buffer being displayed in this window sealed slot window-buffer :: false-or() = #f, init-keyword: buffer:; // 'point' and 'mark' delimit a contiguous selected region // Both of these will always be permanent BPs pointing into 'window-buffer' sealed slot window-point :: = $null-bp, init-keyword: point:; sealed slot window-mark :: false-or() = #f, init-keyword: mark:; // This is only set by 'with-temporary-selection'... sealed slot window-temporary-mark? :: type-union(, ) = #f; sealed slot window-last-mark :: false-or() = #f; sealed slot window-point-pdl :: = #(); // Goal X position for 'next-line' and 'previous-line' sealed slot window-goal-x-position :: false-or() = #f; // The set of recently selected buffers, in order // Each item in the deque is a sealed slot window-selected-buffers :: = make(); // A vector of the s to display, and the number of active // display lines in the vector sealed slot window-display-lines :: = #[]; sealed slot window-n-display-lines :: = 0; // The width of the widest visible display line sealed slot window-max-line-width :: = 0; // This tells us how much redisplay we have to do sealed slot window-redisplay-degree :: = $display-all; // The line at which to start doing redisplay computations sealed slot window-initial-line :: false-or() = #f; // The size of the window when we last did redisplay sealed slot window-last-size :: = pair(0, 0); sealed slot %line-number :: false-or() = #f, setter: window-line-number-setter; sealed slot %total-lines :: false-or() = #f, setter: window-total-lines-setter; // When the redisplay degree is $display-line, these tell which line // (and at what index) to begin redisplay. When the degree is $display-blt, // these tell what line after which to insert/delete new display lines. sealed slot window-redisplay-line :: false-or() = #f; sealed slot window-redisplay-index :: false-or() = #f; // A cache for 'find-display-line' sealed slot window-display-line-cache :: false-or() = #f; sealed slot window-display-line-hint :: false-or() = #f; // How should the window be recentered? sealed slot window-centering-fraction :: false-or() = #f; // For bracket matching sealed slot window-matching-bp :: false-or() = #f; sealed slot window-matching-string :: = make(, size: 2); // Pointer from the window back to the owning frame sealed slot window-frame :: = $null-editor-frame, init-keyword: frame:; // Inter-line spacing sealed slot window-line-spacing :: = $default-window-line-spacing, init-keyword: line-spacing:; // A cache for the 'line-invisible-in-window?' test slot %line-invisible-test :: = false; // The window's default fonts sealed slot window-default-font :: = $default-font, init-keyword: font:; sealed slot window-default-bold-font :: = $default-bold-font, init-keyword: bold-font:; sealed slot window-default-italic-font :: = $default-italic-font, init-keyword: italic-font:; // Caches for the window back-end sealed slot window-color = #f; // or foreground/background sealed slot window-font = #f; // end class ; define method initialize (window :: , #key) => () next-method(); let windows = editor-windows(frame-editor(window-frame(window))); add!(windows, window); window.%line-invisible-test := rcurry(line-invisible-in-window?, window) end method initialize; // The line number for the initial display line, for scroll bars define method window-line-number (window :: ) => (line-number :: false-or()) window.%line-number | begin let line = window-initial-line(window); when (line) let index = bp->line-index(line-start(line), skip-test: window.%line-invisible-test); window-line-number(window) := index; index end end end method window-line-number; // The total number of lines in the window's buffer, for scroll bars define method window-total-lines (window :: ) => (total-lines :: false-or()) window.%total-lines | begin let buffer = window-buffer(window); when (buffer) let total = count-lines(buffer, skip-test: window.%line-invisible-test, cache-result?: #t); window-total-lines(window) := total; total end end end method window-total-lines; define method window-border (window :: ) => (border :: ) $default-window-border end method window-border; define method window-hide-section-separators? (window :: ) => (hide? :: ) let buffer :: = window-buffer(window); let style = buffer-section-separator-style(buffer); select (style) #"always" => #f; #"never" => #t; #"requested" => let frame = window-frame(window); let policy = editor-policy(frame-editor(frame)); ~show-section-separators?(policy); end end method window-hide-section-separators?; define method line-visible-in-window? (line :: , window :: ) => (visible? :: ) #t end method line-visible-in-window?; define inline function line-invisible-in-window? (line :: , window :: ) => (visible? :: ) ~line-visible-in-window?(line, window) end function line-invisible-in-window?; /// Default notification methods define method window-note-mode-entered (window :: , mode :: ) => () // Install the command table for this major mode let frame = window-frame(window); frame-command-set(frame) := mode-command-set(mode); frame-command-state(frame) := standard-command-table(frame-command-set(frame)) end method window-note-mode-entered; define method window-note-buffer-changed (window :: , buffer :: , modified? :: ) => () #f end method window-note-buffer-changed; define method window-note-buffer-read-only (window :: , buffer :: , read-only? :: ) => () #f end method window-note-buffer-read-only; define method window-note-buffer-selected (window :: , buffer :: ) => () display-buffer-name(window, buffer); window-note-buffer-changed(window, buffer, buffer-modified?(buffer)); window-note-buffer-read-only(window, buffer, buffer-read-only?(buffer)); let history = buffer-undo-history(buffer); when (history) let (n-undo, n-redo) = undo-history-state(history); window-note-undo/redo(window, n-undo ~= 0, n-redo ~= 0) end; window-note-selection-changed(window, window-mark(window)) end method window-note-buffer-selected; define method window-note-buffer-selected (window :: , buffer == #f) => () display-buffer-name(window, #f); window-note-undo/redo(window, #f, #f); window-note-selection-changed(window, window-mark(window)) end method window-note-buffer-selected; define method window-note-selection-changed (window :: , mark :: false-or()) => () #f end method window-note-selection-changed; define method window-note-search-string (window :: , string :: false-or()) => () #f end method window-note-search-string; define method window-note-undo/redo (window :: , undo? :: , redo? :: ) => () #f end method window-note-undo/redo; define method window-note-policy-changed (window :: , new-policy :: , old-policy :: false-or()) => () // Changing the policy may affect the Copy command's enabling, depending // on the selection. let mark? = (mark & #t); let copy-line? = (unselected-copy-policy(new-policy) == #"copy-line"); command-enabled?(window, copy-region) := mark? | copy-line?; // Search command enabling is affected by what command set we're using... let editor = frame-editor(window-frame(window)); window-note-search-string(window, editor-search-string(editor)); // If necessary, update the window's "title-bar". when (~old-policy | show-path-in-title?(old-policy) ~== show-path-in-title?(new-policy)) display-buffer-name(window, window-buffer(window)) end; // Update the font and redisplay, if it changed. when (~old-policy | default-font(new-policy) ~= default-font(old-policy) | show-section-separators?(new-policy) ~= show-section-separators?(old-policy)) set-default-font(window, default-font(new-policy)); // Don't redisplay immediately in case previous methods are also going // to do things requiring redisplay; leave redisplay to the caller queue-redisplay(window, $display-all) end; end method window-note-policy-changed; /// Window back end protocol define constant = one-of(#"line", #"character"); define protocol <> () function window-enabled? (window :: ) => (enabled? :: ); function window-occluded? (window :: ) => (occluded? :: ); // The window size is the size of the Deuce pane, which can be wider // than what is visible on the screen when there is scrolling. The // height of the window might be smaller than the viewport height, but // because Deuce manages its own vertical scrolling, it's never taller. function window-size (window :: ) => (width :: , height :: ); // The window viewport size is the size of the visible part of the // Deuce pane. When there is no scrolling, this is the same as the // window size. function window-viewport-size (window :: ) => (width :: , height :: ); function update-scroll-bar (window :: , which, total-lines :: , position :: , visible-lines :: ) => (); function scroll-position (window :: ) => (x :: , y :: ); function set-scroll-position (window :: , x :: false-or(), y :: false-or()) => (); function window-line-spacing (window :: ) => (spacing :: ); // Message display functions function display-message (window :: , format-string :: , #rest format-args) => (); function display-error-message (window :: , format-string :: , #rest format-args) => (); function display-buffer-name (window :: , buffer :: false-or()) => (); // Text display functions function draw-string (window :: , string :: , x :: , y :: , #key start: _start, end: _end, color, font, align-x, align-y) => (); function string-size (window :: , string :: , #key start: _start, end: _end, font) => (width :: , height :: , baseline :: ); // Other simple graphics function draw-line (window :: , x1 :: , y1 :: , x2 :: , y2 :: , #key color, thickness) => (); function draw-rectangle (window :: , left :: , top :: , right :: , bottom :: , #key color, thickness, filled?) => (); function draw-image (window :: , image, x :: , y :: ) => (); function clear-area (window :: , left :: , top :: , right :: , bottom :: ) => (); // BITBLT function copy-area (window :: , from-x :: , from-y :: , width :: , height :: , to-x :: , to-y :: ) => (); // Cursor and caret function cursor-position (window :: ) => (x :: , y :: ); function set-cursor-position (window :: , x :: , y :: ) => (); function do-with-busy-cursor (window :: , continuation :: ) => (#rest values); function caret-position (window :: ) => (x :: , y :: ); function set-caret-position (window :: , x :: , y :: ) => (); function caret-size (window :: ) => (width :: , height :: ); function set-caret-size (window :: , width :: , height :: ) => (); function show-caret (window :: , #key tooltip?) => (); function hide-caret (window :: , #key tooltip?) => (); // Font metrics function font-metrics (window :: , font :: false-or()) => (width :: , height :: , ascent :: , descent :: ); // Simple menu choose function choose-from-menu (window :: , items :: , #key title, value, label-key, value-key, width, height, multiple-sets?) => (value :: false-or(), success? :: ); function choose-from-dialog (window :: , items :: , #key title, value, label-key, value-key, width, height, selection-mode) => (value :: false-or(), success? :: , width :: false-or(), height :: false-or()); // Canned dialogs function information-dialog (window :: , format-string :: , #rest format-args) => (); function warning-dialog (window :: , format-string :: , #rest format-args) => (); function yes-or-no-dialog (window :: , format-string :: , #rest format-args) => (result :: ); function yes-no-or-cancel-dialog (window :: , format-string :: , #rest format-args) => (result :: type-union(, singleton(#"cancel"))); function open-file-dialog (window :: , #key default, default-type) => (pathname :: false-or()); function new-file-dialog (window :: , #key default, default-type) => (pathname :: false-or()); function save-buffers-dialog (window :: , #key exit-label :: false-or(), reason :: false-or(), buffers :: false-or(), default-buffers :: false-or()) => (buffers :: type-union(, singleton(#f), singleton(#"cancel")), no-buffers? :: ); function choose-buffer-dialog (window :: , #key title, buffer :: false-or(), buffers) => (buffer :: false-or()); function choose-buffers-dialog (window :: , #key title, buffer :: false-or(), buffers) => (buffers :: false-or()); function new-buffer-dialog (window :: , #key title) => (buffer-name); function edit-definition-dialog (window :: , name :: , #key title) => (definition); function choose-string-dialog (window :: , #key default, prompt, title) => (string); function hack-matching-lines-dialog (window :: ) => (string, operation); function goto-position-dialog (window :: , what :: ) => (number :: false-or(), what :: false-or()); // The next two are modeless, and hence return no values function string-search-dialog (window :: , #key string, reverse? :: , case-sensitive? :: , whole-word? :: ) => (); function string-replace-dialog (window :: , #key string, replace, reverse? :: , case-sensitive? :: , whole-word? :: ) => (); function configuration-dialog (window :: ) => (policy :: false-or()); // Clipboard function add-to-clipboard (window :: , data) => (); function get-from-clipboard (window :: , class) => (data); // Commands getter command-enabled? (window :: , command :: ) => (enabled? :: ); setter command-enabled?-setter (enabled? :: , window :: , command :: ) => (enabled? :: ); // Character-stream-like functions for reading from the keyboard function read-character (window :: ) => (character :: ); function read-gesture (window :: ) => (keysym, char, modifiers); end protocol <>; define macro with-busy-cursor { with-busy-cursor (?window:expression) ?:body end } => { begin let with-busy-cursor-body = method () ?body end; do-with-busy-cursor(?window, with-busy-cursor-body) end } end macro with-busy-cursor; /// Colors // Colors are represented as integers, and so can be compared with \= define constant = limited(, min: 0, max: 16777216); define inline function make-color (r :: , g :: , b :: ) => (color :: ) logior(r, ash(g, 8), ash(b, 16)) end function make-color; define inline function color-red (color :: ) => (red :: limited(, min: 0, max: 255)) logand(color, #xFF) end function color-red; define inline function color-green (color :: ) => (green :: limited(, min: 0, max: 255)) logand(ash(color, -8), #xFF) end function color-green; define inline function color-blue (color :: ) => (blue :: limited(, min: 0, max: 255)) logand(ash(color, -16), #xFF) end function color-blue; define variable $black :: = make-color( 0, 0, 0); define variable $white :: = make-color(255, 255, 255); define variable $red :: = make-color(255, 0, 0); define variable $green :: = make-color( 0, 255, 0); define variable $blue :: = make-color( 0, 0, 255); define variable $cyan :: = make-color( 0, 255, 255); define variable $magenta :: = make-color(255, 0, 255); define variable $yellow :: = make-color(255, 255, 0); define constant $default-foreground = #"foreground"; define constant $default-background = #"background"; /// Fonts define function update-caret-from-font (window :: ) => () let (fw, fh, fa, fd) = font-metrics(window, window-default-font(window)); ignore(fh); let width :: = if (fw < 6) 1 else 2 end; let height :: = fa + fd; set-caret-size(window, width, height); end function update-caret-from-font; // Sets the default font for the window // Note that doing a full redisplay is required after doing this define open generic set-default-font (window :: , font) => (); define method set-default-font (window :: , font) => () // Update all of the window's fonts window-default-font(window) := make-font(font-family(font), font-name(font), font-weight(font), font-slant(font), font-size(font)); window-default-bold-font(window) := make-font(font-family(font), font-name(font), #"bold", font-slant(font), font-size(font)); window-default-italic-font(window) := make-font(font-family(font), font-name(font), font-weight(font), #"italic", font-size(font)); // Update the height of the caret update-caret-from-font(window); end method set-default-font; // Sets the default font size for the window // Note that doing a full redisplay is required after doing this define open generic set-default-font-size (window :: , font-size) => (); define method set-default-font-size (window :: , font-size) => () local method merge-size (font :: , size) make-font(font-family(font), font-name(font), font-weight(font), font-slant(font), size) end method; // Update all of the window's fonts window-default-font(window) := merge-size(window-default-font(window), font-size); window-default-bold-font(window) := merge-size(window-default-bold-font(window), font-size); window-default-italic-font(window) := merge-size(window-default-italic-font(window), font-size); // Update the height of the caret update-caret-from-font(window); end method set-default-font-size; /// Canned images define open generic standard-images (window :: , i :: ) => (image); define constant $potential-breakpoint :: = 0; define constant $enabled-breakpoint :: = 1; define constant $disabled-breakpoint :: = 2; define constant $step-breakpoint :: = 3; define constant $test-breakpoint :: = 4; define constant $enabled-tracepoint :: = 5; define constant $disabled-tracepoint :: = 6; define constant $profile-point :: = 7; define constant $current-location :: = 8; define constant $prompt-arrow :: = 9; define constant $values-arrow :: = 10; define constant $warning :: = 11; define constant $serious-warning :: = 12; /// Current buffer, point, mark define constant $point-pdl-size :: = 16; define sealed method push-point-pdl! (window :: , bp :: , #key display-message? = #t) => () let line = bp-line(bp); let index = bp-index(bp); let buffer = bp-buffer(bp); let bp = make(, line: line, index: index, buffer: buffer); let pdl = window-point-pdl(window); if (size(pdl) = $point-pdl-size) kill-bp!(last(pdl)); let last-pair = begin let last-pair = pdl; for (i :: from 0 below size(pdl) - 3) last-pair := tail(last-pair) end; last-pair end; push!(window-point-pdl(window), bp); tail(last-pair) := list(head(tail(last-pair))) else push!(window-point-pdl(window), bp); end; when (display-message?) display-message(window, "Cursor position saved") end end method push-point-pdl!; define sealed method pop-point-pdl! (window :: ) => (bp :: false-or()) unless (empty?(window-point-pdl(window))) pop!(window-point-pdl(window)) end end method pop-point-pdl!; /// Window buffer selection define sealed class () sealed constant slot selection-buffer :: , required-init-keyword: buffer:; sealed slot selection-point :: false-or() = #f; sealed slot selection-mark :: false-or() = #f; sealed slot selection-initial-line :: false-or() = #f; sealed slot selection-goal-x-position :: false-or() = #f; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define sealed method select-buffer (window :: , buffer :: ) => () // Locate the new and old buffers in the buffer history let frame = window-frame(window); let buffers = window-selected-buffers(window); let new-buffer = buffer; let new-entry = find-value(buffers, method (s) selection-buffer(s) == new-buffer end); let new-mode = buffer-major-mode(new-buffer); let old-buffer = window-buffer(window); let old-entry = find-value(buffers, method (s) selection-buffer(s) == old-buffer end); let old-mode = old-buffer & buffer-major-mode(old-buffer); // Update the buffer history unless (buffer == window-buffer(window)) // maybe avoid some work when (new-entry) remove!(buffers, new-entry) end; when (old-entry) let old-entry :: = old-entry; // force tighter type... selection-point(old-entry) := window-point(window); selection-mark(old-entry) := ~window-temporary-mark?(window) & window-mark(window); selection-initial-line(old-entry) := window-initial-line(window); selection-goal-x-position(old-entry) := window-goal-x-position(window) end; let new-entry :: = new-entry | make(, buffer: new-buffer); initialize-redisplay-for-buffer(window, new-buffer, point: selection-point(new-entry), mark: selection-mark(new-entry), line: selection-initial-line(new-entry), goal-x: selection-goal-x-position(new-entry)); set-scroll-position(window, 0, #f); push(buffers, new-entry) end; // Now select the new buffer // We do this regardless of whether we think it changed, // just in case something funny is going on... frame-buffer(frame) := new-buffer; window-buffer(window) := new-buffer; *buffer* := new-buffer; unless (new-mode == old-mode) window-note-mode-entered(window, new-mode) end; window-note-buffer-selected(window, new-buffer) end method select-buffer; // This gives client a chance to implement alternate strategies for // mapping buffers to windows in which they are shown. Note that this // can involve the creation of another window in another thread, so // doing something like calling 'move-point!' directly after calling // 'select-buffer-in-appropriate-window' may not work, because the // binding of *buffer* may apply to the wrong thread. Beware! define method select-buffer-in-appropriate-window (window :: , buffer :: , #key line, index = 0) => () select-buffer(window, buffer); when (line) move-point!(line, index: index, window: window) end; queue-redisplay(window, $display-all) end method select-buffer-in-appropriate-window;