Module: environment-deuce Synopsis: Environment Deuce Author: Scott McKay, Hugh Greene, Andy Armstrong 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 /// Environment editor class define sealed class () end class ; define method initialize (editor :: , #key) next-method(); load-policy-settings(editor-policy(editor)); install-command-set(editor, command-set-policy(editor-policy(editor))); tune-in($project-channel, deuce-editor-project-message-receiver, message-type: ); end method initialize; define function deuce-editor-project-message-receiver (message :: ) => () let project :: false-or() = instance?(message, ) & message-project(message); select (message by instance?) , => deuce-note-active-project-changed(); => deuce-note-project-closed(project); => deuce-note-project-sources-updated(project); => let state = message-breakpoint-state(message); deuce-note-all-breakpoints-changed(project, state); => let breakpoint = message-breakpoint(message); let state = message-breakpoint-state(message); deuce-note-single-breakpoint-changed(project, breakpoint, state); => deuce-note-project-products-changed(project); otherwise => #f; end end function deuce-editor-project-message-receiver; define method deuce-note-active-project-changed () => () // Uncache the buffer->project mapping, to be recached below. for (buffer in editor-buffers($environment-editor)) remove-property!(buffer-properties(buffer), #"project") end; // Notify all editor frames that their frame-current-project // may have changed. call-in-editor-frame-duim-frames(frame-note-project-updated); do-environment-editor-windows (method (window :: ) when (instance?(sheet-frame(window), )) queue-redisplay(window, $display-all); sheet-mapped?(window) & redisplay-window(window) end end method) end method deuce-note-active-project-changed; define sealed method do-environment-editor-windows (function :: , #key project) for (window :: in editor-windows($environment-editor)) when (~project | begin let frame = sheet-frame(window); frame & project == frame-current-project(frame) end) function(window) end end end method do-environment-editor-windows; // Apply FUNCTION to all DUIM frames containing (or being) one of the // $environment-editor's editor frames. Apply it to each DUIM frame only // once (e.g., the debugger contains several editor-frames). define method call-in-editor-frame-duim-frames (function :: , #key project) => () // Other threads might modify the list of editor frames, // so we take a (very) little care to avoid race conditions let _frames = editor-frames($environment-editor); // Deuce frames let frames = make(); // DUIM frames for (_frame in _frames) let window = _frame & frame-window(_frame); let frame = window & sheet-frame(window); frame & add-new!(frames, frame) end; for (frame in frames) let include? = ~project | frame-current-project(frame) = project; include? & frame-mapped?(frame) & function(frame) end end method call-in-editor-frame-duim-frames; /// Environment editor pane classes //---*** Lose this class and everything that messes with 'primary-object-interval' //---*** Only 'dylanworks-breakpoint-menu' and 'set-editor-breakpoint-popup-target' seem to care define abstract class () sealed slot primary-object-interval :: false-or(type-union(, )) = #f end class ; define sealed method primary-object-interval (window :: ) => (interval :: false-or(type-union(, ))) #f end method primary-object-interval; define sealed method primary-object-interval-setter (interval :: false-or(type-union(, )), window :: ) => (interval :: false-or(type-union(, ))) #f end method primary-object-interval-setter; define open abstract class () end class ; define method initialize (window :: , #key) next-method(); // Don't go through 'set-default-font-size', because the sheet // is not grafted to the port yet, nor is there a medium... let font = editor-policy($environment-editor).default-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)); end method initialize; define open class (, ) keyword editor: = $environment-editor; end class ; define sealed class (, ) end class ; /// Environment editor frames define frame (, , , , ) slot %project :: false-or() = #f; constant slot %lines :: false-or() = #f, init-keyword: lines:; constant slot %columns :: false-or() = #f, init-keyword: columns:; constant slot %mode-box :: false-or() = #f; // An alist of (transaction-id . section) for use by c-sh-C slot %transaction-ids :: = make(); pane %window (frame) make(, frame: frame, lines: frame.%lines, columns: frame.%columns); layout (frame) scrolling (scroll-bars: #"both") frame.%window end; tool-bar (frame) make-environment-tool-bar(frame); status-bar (frame) make-deuce-status-bar(frame); command-table (frame) *editor-command-table*; keyword frame-class-name:, init-value: #"editor"; keyword editor: = $environment-editor; // Note that these get overwritten by saved settings keyword lines: = 30; keyword columns: = 72; keyword icon: = $editor-window-small-icon; end frame ; define cascading-window-settings editor-window :: = "Editor Window"; /// Frame start-up and initialization // Note: If you change this method, change the reinitialize-frame method below. define method initialize (frame :: , #key buffer :: false-or(), buffer-pathname: pathname :: false-or(), line, index = 0, new-file? :: = #f, deuce-frame) => () ignore(deuce-frame); next-method(); frame-input-focus(frame) := frame.%window; frame-window(frame) := frame.%window; command-enabled?(frame-edit-search-options, frame) := #t; let sccs = current-source-control-system(); when (sccs) add-command-table-menu-item (*editor-command-table*, sccs-label(sccs), , *editor-source-control-command-table*, after: "Window", //---*** SHOULD BE: after: "Application", error?: #f); disable-unimplemeted-sccs-commands(frame, sccs) end; do-reinitialize-frame(frame, buffer: buffer, buffer-pathname: pathname, line: line, index: index, new-file?: new-file?) end method initialize; // Doing the file-loading work here means that on the first opening of // the frame, the frame won't appear until the file is loaded. It then // makes sense in terms of code reuse to do it in reinitialization too. define method do-reinitialize-frame (frame :: , #key buffer :: false-or(), buffer-pathname: pathname :: false-or(), line, index = 0, new-file? :: = #f) => () //---*** There should maybe be some locking in here, in case re-use //---*** clashes with operations done on the frame's own thread. // 'new-file?' is intended to be mutually-exclusive with 'buffer' and 'buffer-pathname'. dynamic-bind (*editor-frame* = frame) let initial-buffer = buffer | when (pathname) try-to-load-from-pathname(frame, pathname) end | when (new-file?) make-empty-buffer() end | get-initial-buffer(frame); if (frame-mapped?(frame)) try-to-select-buffer(frame, initial-buffer, line: line, index: index); else // We'll call 'try-to-select-buffer' in the handler. frame-buffer(frame) := initial-buffer; end; //--- Remember the most recently opened files let locator = buffer-locator(initial-buffer); when (locator) most-recent-file() := locator end; end end method do-reinitialize-frame; define method frame-top-level (frame :: ) => (#rest values) dynamic-bind (*editor-frame* = frame) let buffer = get-initial-buffer(frame); dynamic-bind (*buffer* = buffer) select-buffer(frame-window(frame), buffer); let top-sheet = top-level-sheet(frame); while (#t) let event = read-event(top-sheet); block () handle-event(event-handler(event-client(event)), event); exception (e :: ) when (command-error-format-string(e)) apply(deuce/display-error-message, command-error-window(e), command-error-format-string(e), command-error-format-arguments(e)) end; #f end end end end end method frame-top-level; define method handle-event (frame :: , event :: ) => () next-method(); // Set up thread variables and initial buffer let window :: false-or() = frame-window(frame); let buffer :: false-or() = frame-buffer(frame); *editor-frame* := frame; *buffer* := buffer; try-to-select-buffer(frame, buffer); // Do initial notifications, other than those which will be done // during buffer-selection //--- Should I do undo/redo here? let policy = editor-policy(frame-editor(frame)); window-note-policy-changed(frame-window(frame), policy, #f); end method handle-event; define method handle-event (frame :: , event :: ) => () next-method(); // off to ... frame-input-focus(frame) := frame.%window end method handle-event; define method frame-target-pane (frame :: ) => (pane :: ) frame.%window end method frame-target-pane; /// Frame shutdown define method exit-editor (frame :: ) => () exit-frame(frame) end method exit-editor; // This function returns #t iff the _frame_ is the only instance of // _class_ in existence. //---*** Something like this would probably belong better in Environment-Framework. //---*** I would use 'find-matching-frames', but it will find only _reusable_ frames. define function only-frame? (frame :: , class :: subclass()) => (only-frame? :: ) // Oh! for dependent types in parameter lists! assert(instance?(frame, class), "'only-frame?' must only be used on frames of the supplied class"); let frames-of-class :: = 0; block (exit) do-frames(method (_frame :: ) when (instance?(_frame, class)) frames-of-class := frames-of-class + 1; end; when (frames-of-class > 1) exit(); end; end method, port: port(frame)); end; (frames-of-class == 1) end function only-frame?; define method frame-can-exit? (frame :: ) => (exit? :: ) let window :: = frame-window(frame); let buffer :: = frame-buffer(frame); let editor = frame-editor(frame); let policy = editor-policy(editor); local method maybe-save-then-maybe-kill-buffer (window, buffer) // Don't let 'kill-buffer' exit this frame, because we're already // in the 'frame-exit' code! let failed? = maybe-save-buffer(window, buffer); unless (failed?) // Kill the buffer kill-buffer(buffer, frame: frame, no-exit-frame: frame); #t end end method; // Bind *editor-frame* in case this is called on a different thread dynamic-bind (*editor-frame* = frame) case only-frame?(frame, ) => // If we're the only editor frame left... let all-buffers = choose(method (b) ~buffer-anonymous?(b) end, editor-buffers(editor)); if (size(all-buffers) = 1 & all-buffers[0] == buffer) // ...then if there is exactly one buffer left, use the simple dialog maybe-save-then-maybe-kill-buffer(window, buffer) else // ...then if there are multiple buffers left, use the "save all" dialog let buffers = save-buffers-dialog(window, exit-label: "&Close"); select (buffers) #f => #t; #"cancel" => #f; otherwise => do-save-all-files(frame, buffers, curry(deuce/display-message, window)); // With the 'fixed-frame-buffer?' policy, we have to kill the buffer // associated with this frame, no matter what, so that it doesn't come // back and haunt us later when (fixed-frame-buffer?(policy)) kill-buffer(buffer, frame: frame, no-exit-frame: frame) end; #t end end; otherwise => // If we're not the only editor frame left... if (fixed-frame-buffer?(policy)) // ...then if the 'fixed-frame-buffer?' policy is in operation, offer // to save just this buffer, and kill it before closing the window maybe-save-then-maybe-kill-buffer(window, buffer) else // ...then if we're just another Emacs-style frame, just close it #t end; end end end method frame-can-exit?; define method handle-event (frame :: , event :: ) => () when (event-destroy-frame?(event)) // If this frame is going away, stop tracking it //--- Deuce should probably provide some function that does this... let frames = editor-frames(frame-editor(frame)); remove!(frames, frame); end; next-method(); end method handle-event; /// Selecting buffers and loading files into buffers // In an editor pane (i.e., a pane in the Editor), obey the selection policy define method select-buffer-in-appropriate-window (window :: , buffer :: , #key line, index = 0) => () let frame = window-frame(window); if (fixed-frame-buffer?(editor-policy(frame-editor(frame)))) // Windows-like policy -- each buffer gets its own window find-deuce-frame(buffer: buffer, line: line, index: index) else // Emacs-like policy -- a window can view multiple buffers select-buffer(window, buffer); when (line) move-point!(line, index: index, window: window) end; queue-redisplay(window, $display-all) end end method select-buffer-in-appropriate-window; // In Deuce gadgets contained in other kinds of frames, always try to select // a proper Editor frame define method select-buffer-in-appropriate-window (window :: , buffer :: , #key line, index = 0) => () find-deuce-frame(buffer: buffer, line: line, index: index) end method select-buffer-in-appropriate-window; // By default, no can do on this command in Deuce gadgets define method choose-buffer (frame :: ) => () let window :: = frame-window(frame); deuce/display-error-message(window, "") end method choose-buffer; // Ditto define method switch-buffers (frame :: ) => () let window :: = frame-window(frame); deuce/display-error-message(window, "") end method switch-buffers; // Ditto define method new-buffer (frame :: ) => () let window :: = frame-window(frame); deuce/display-error-message(window, "") end method new-buffer; define method try-to-select-buffer (frame :: , buffer :: , #key line, index = 0) => () let window :: false-or() = frame-window(frame); when (window) case buffer ~== window-buffer(window) => select-buffer(window, buffer); when (line) move-point!(line, index: index, window: window) end; queue-redisplay(window, $display-all); redisplay-window(window); line => move-point!(line, index: index, window: window); queue-redisplay(window, $display-point, centering: 0); redisplay-window(window); otherwise => #f; end end end method try-to-select-buffer; define method try-to-load-from-pathname (frame :: , pathname :: ) => (buffer :: false-or()) if (frame-showing-pathname?(frame, pathname)) frame-buffer(frame) else let editor = frame-editor(frame); let buffer :: false-or() = find-buffer-from-pathname(editor, pathname) | do-find-file(editor, pathname, direction: #"input"); when (buffer) frame-last-command-type(frame) := #"file"; buffer end end end method try-to-load-from-pathname; define method frame-showing-pathname? (frame :: , pathname :: ) => (showing? :: ) let buffer = frame-buffer(frame); when (buffer & file-buffer?(buffer)) buffer-locator(buffer) = as(, pathname) end end method frame-showing-pathname?; define method get-initial-buffer (frame :: ) => (buffer :: ) let editor = frame-editor(frame); frame-buffer(frame) | element(editor-buffers(editor), 0, default: #f) | make-initial-buffer(editor: editor) end method get-initial-buffer; /// Editor implementations of 'environment-tools' generic functions //--- Ideally Deuce would use locators itself for buffer-pathname... define method buffer-pathname-as-string (buffer :: ) => (pathname :: false-or()) let pathname = buffer-pathname(buffer); pathname & as(, pathname) end method buffer-pathname-as-string; //--- Ideally Deuce would use locators itself for buffer-pathname... define method buffer-locator (buffer :: ) => (locator :: false-or()) let pathname = buffer-pathname(buffer); pathname & as(, pathname) end method buffer-locator; define method buffer-title (buffer :: , #key show-path? :: = #t) => (title :: ) when (file-buffer?(buffer)) let locator = buffer-locator(buffer); when (locator) let directory = show-path? & locator-directory(locator); if (directory) concatenate(locator-name(locator), " (", as(, directory), ")") else locator-name(locator) end end end | buffer-name(buffer) end method buffer-title; define method generate-frame-title (frame :: ) => (title :: ) let editor = frame-editor(frame); let buffer = frame-buffer(frame); let policy = editor-policy(editor); let show-path? = show-path-in-title?(policy); let name = buffer & buffer-title(buffer, show-path?: show-path?); let current-project = frame-current-project(frame); let current-project-name = current-project & environment-object-display-name(current-project, current-project, #f); concatenate(if (name) name else // This case probably won't happen, since there currently can't // be an editor window without a file, but let's handle this // gracefully anyway. "No Name" end, if (current-project-name) concatenate(" - Editing ", current-project-name) else "" end, " - ", release-product-name()) end method generate-frame-title; define method make-environment-tool-bar-buttons (frame :: ) => (buttons :: ) let framem = frame-manager(frame); with-frame-manager (framem) let left-buttons = make-editor-tool-bar-left-buttons(frame); let right-buttons = make-editor-tool-bar-right-buttons(frame); let mode-box = make-editor-tool-bar-mode-box(frame); let buttons :: = make(); when (left-buttons & ~empty?(left-buttons)) add!(buttons, make(, children: left-buttons, spacing: 0)) end; concatenate!(buttons, next-method()); when (right-buttons & ~empty?(right-buttons)) add!(buttons, make(, children: right-buttons, spacing: 0)) end; when (mode-box) add!(buttons, mode-box) end; buttons end end method make-environment-tool-bar-buttons; define method make-editor-tool-bar-left-buttons (frame :: ) => (buttons :: ) vector(make(