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 /// Dylan mode //--- We should really have that gets used by //--- both and define open class () end class ; define method initialize-major-mode (mode :: , #key command-set = mode-command-set(mode)) => () next-method(); let command-set = install-dylan-mode-bindings(mode, command-set); // Install a few more things for Dylan mode only let nothing = 0; let shift = $shift-key; let control = $control-key; let meta = $meta-key; let control+shift = logior($control-key, $shift-key); select (command-set-name(command-set)) #"emacs" => let command-table = standard-command-table(command-set); add-commands!(command-table, vector('C', control, evaluate-region), vector('C', meta, evaluate-buffer), vector('M', control, macroexpand-region), vector(#"f7", nothing, build-project), vector(#"f7", control, clean-build-project), vector(#"f7", control+shift, evaluate-region)); let command-table = control-X-command-table(command-set); add-commands!(command-table, vector('c', control, build-project), vector('c', nothing, clean-build-project)); let command-table = escape-command-table(command-set); add-commands!(command-table, vector('C', nothing, evaluate-buffer)); #"windows" => let command-table = standard-command-table(command-set); add-commands!(command-table, vector(#"f7", nothing, build-project), vector(#"f7", control, clean-build-project), vector(#"f7", control+shift, evaluate-region)); otherwise => #[]; end end method initialize-major-mode; define method install-dylan-mode-bindings (mode :: , command-set :: ) => (command-set :: ) let nothing = 0; let shift = $shift-key; let control = $control-key; let meta = $meta-key; let control+meta = logior($control-key, $meta-key); let control+shift = logior($control-key, $shift-key); let meta+shift = logior($meta-key, $shift-key); let command-set = copy-command-set(command-set); mode-command-set(mode) := command-set; select (command-set-name(command-set)) #"emacs" => let command-table = standard-command-table(command-set); add-commands!(command-table, vector('f', control+meta, forward-expression), vector('b', control+meta, backward-expression), vector('u', control+meta, up-expression), vector('d', control+meta, down-expression), vector('a', control+meta, start-of-definition), vector('e', control+meta, end-of-definition), vector(#"right", meta, forward-expression), vector(#"left", meta, backward-expression), vector(#"up", meta, up-expression), vector(#"down", meta, down-expression), vector(#"prior", meta, start-of-definition), vector(#"next", meta, end-of-definition), vector(#"delete", control+meta, delete-expression), vector(#"backspace", control+meta, rubout-expression), vector('k', control+meta, delete-expression), vector('t', control+meta, transpose-expressions), vector(';', control, insert-comment), vector(#"tab", control, insert-tab), vector(#"tab", nothing, indent-line), vector('\\', control+meta, indent-region), vector('q', control+meta, indent-expression), vector(')', meta, dylan-insert-block-end), vector('.', meta, edit-definition), vector('i', control+meta, complete-name), vector('A', control, show-arglist), vector('D', control, show-documentation), vector('V', control, describe-object), vector('O', control+meta, browse-object), vector('C', control+meta, browse-class), //---*** Clashes with Debugger's stepping bindings //---*** vector(#"f12", nothing, complete-name), vector(#"f2", nothing, browse-object), vector(#"f2", control, browse-class), vector(#"f2", shift, edit-definition)); let command-table = control-X-command-table(command-set); add-commands!(command-table, vector(';', control, comment-region)); let command-table = escape-command-table(command-set); add-commands!(command-table, vector('f', control, forward-expression), vector('b', control, backward-expression), vector('u', control, up-expression), vector('d', control, down-expression), vector('a', control, start-of-definition), vector('e', control, end-of-definition), vector(#"delete", control, delete-expression), vector(#"backspace", control, rubout-expression), vector('k', control, delete-expression), vector('t', control, transpose-expressions), vector('\\', control, indent-region), vector('q', control, indent-expression), vector(')', nothing, dylan-insert-block-end), vector('.', nothing, edit-definition), vector('i', control, complete-name), vector('O', control, browse-object), vector('C', control, browse-class)); #"windows" => let command-table = standard-command-table(command-set); add-commands!(command-table, vector(#"right", meta, forward-expression), vector(#"left", meta, backward-expression), vector(#"up", meta, up-expression), vector(#"down", meta, down-expression), vector(#"prior", meta, start-of-definition), vector(#"next", meta, end-of-definition), vector(#"tab", nothing, indent-region), vector('i', control, insert-tab), //---*** Clashes with Debugger's stepping bindings //---*** vector(#"f12", nothing, complete-name), vector(#"f1", meta, show-documentation), vector(#"f2", nothing, browse-object), vector(#"f2", control, browse-class), vector(#"f2", shift, edit-definition)); otherwise => #[]; end; command-set end method install-dylan-mode-bindings; begin gethash(*keyword->major-mode*, #"dylan") := ; gethash(*file-type->major-mode*, #"dylan") := end; define method mode-name (mode :: ) => (name :: ) "Dylan" end method mode-name; define method source-file-type (mode :: ) => (file-type) #"dylan" end method source-file-type; /// Dylan sections define sealed class () // The line in which the define word is contained sealed slot section-defining-line :: false-or() = #f, init-keyword: defining-line:; sealed slot %signature :: false-or() = #f; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define sealed inline method section-definition-signature (section :: ) => (signature :: false-or()) section.%signature | compute-section-signature(section) end method section-definition-signature; define sealed method section-definition-name (section :: ) => (name :: false-or()) let signature = section-definition-signature(section); signature & signature-name(signature) end method section-definition-name; define sealed method section-definition-type (section :: ) => (type :: false-or()) let signature = section-definition-signature(section); signature & signature-type(signature) end method section-definition-type; define method do-note-line-changed (mode :: , line :: ) => (line :: ) let section = line-section(line); when (section & line == section-defining-line(section)) section.%signature := #f; // Make each window redisplay the Dylan definition line for this section do-associated-windows (window :: = *editor-frame*) let buffer = window-buffer(window); when (buffer & buffer-contains-section?(buffer, section)) let definition-line = section-start-line(section); when (structural-diagram-line?(definition-line)) line-modification-tick(definition-line) := tick(); queue-redisplay(window, $display-line, line: definition-line, index: 0) end end end end; next-method() end method do-note-line-changed; /// Dylan definition nodes define sealed class () end class ; define sealed domain make (singleton()); define sealed domain initialize (); define sealed class () sealed slot node-module-name :: false-or() = #f, init-keyword: module-name:; end class ; define sealed domain make (singleton()); define sealed domain initialize (); /// Dylan "divider lines" // A divider line that appears at the beginning of each Dylan section, // except for the header section define sealed class () end class ; define sealed domain make (singleton()); define sealed domain initialize (); define variable $dylan-definition-line-color = make-color(150, 150, 150); define sealed method line-visible-in-window? (line :: , window :: ) => (visible? :: ) ~window-hide-section-separators?(window) end method line-visible-in-window?; // Note that this method is intentionally not on because // it's possible to get s into non-Dylan buffers define sealed method display-line (line :: , mode :: , window :: , x :: , y :: , #key start: _start :: = 0, end: _end, align-y = #"top") => () ignore(_end); unless (window-hide-section-separators?(window)) when (_start = 0) // no icon on continuation lines let name = section-definition-name(line-section(line)); let font = window-default-font(window); let color = $dylan-definition-line-color; // Make the definition line only as wide as the visible part of the screen let (width, height) = window-viewport-size(window); ignore(height); let xoff1 = 5; let xoff2 = 6; let (fw, fh, fa, fd) = font-metrics(window, font); ignore(fw, fd); let height = fh; let baseline = fa; when (align-y == #"baseline") // Normalize the Y coordinate as though we are using 'align-y: #"top"' dec!(y, baseline) end; let line-y = y + floor/(height, 2); if (name) // Here is the layout of the graphic we are drawing: // xoff1 line-width xoff2 name-width xoff2 line-width xoff1 // |<-->|<--->|<-------->|<--->|<-------->|<--->|<-------->|<--->| // 0 x width let name-width = string-size(window, name, font: font); let line-width = max(0, floor/(width - x - name-width, 2) - xoff1 - xoff2); draw-line(window, x + xoff1, line-y, x + xoff1 + line-width, line-y, thickness: 1, color: color); draw-line(window, width - xoff1, line-y, width - xoff1 - line-width, line-y, thickness: 1, color: color); draw-string(window, name, x + xoff1 + line-width + xoff2, y, font: font, color: color, align-y: #"top") else draw-line(window, x + xoff1, line-y, x + width - xoff1, line-y, thickness: 1, color: color); end end end end method display-line; // Same deal -- this method is intentionally not on define sealed method line-size (line :: , mode :: , window :: , #key start: _start, end: _end) => (width :: , height :: , baseline :: ) ignore(_start, _end); let (width, height) = window-viewport-size(window); if (window-hide-section-separators?(window)) values(width, 0, 0) else // Definition lines are only as wide as the visible part of the screen ignore(height); let (fw, fh, fa, fd) = font-metrics(window, window-default-font(window)); ignore(fw, fd); let height = fh; let baseline = fa; values(width, height, baseline) end end method line-size; /// Presentations define method do-cursor-at-position (mode :: , window :: , x :: , y :: ) => (cursor) let over-icon? = x < $breakpoint-image-width; over-icon? & #"default" end method do-cursor-at-position; define method do-presentation-at-position (mode :: , window :: , x :: , y :: , #key button = $left-button, modifiers = 0, event-type = #"press") => (presentation :: false-or(), bp :: false-or(), move-point? :: ) let (bp, dline) = position->bp(window, x, y); let line = bp & bp-line(bp); let over-icon? = line & x < $breakpoint-image-width; let dylan-line? = line & instance?(line, ); let (sbp, ebp) = bp & ~over-icon? & ~dylan-line? & atom-under-bp(bp); let menu? = gesture-matches?($menu-gesture, button, modifiers, event-type: event-type); case over-icon? & (menu? | (event-type == #"release" & ~gesture-matches?($menu-gesture, button, modifiers))) => // Mouse-right (press) on a breakpoint icon gets a presentation for a menu // Mouse-other (release) on a breakpoint icon also gets a presentation // Strictly speaking, we don't need to return #t as the third value, // but it makes the point-moving behavior more consistent... values(make(, object: line, type: ), bp, #t); dylan-line? & (menu? | (event-type == #"double-click" & gesture-matches?($move-gesture, button, modifiers))) => // Mouse-right (press) on a dylan line gets a presentation for a menu values(make(, object: line, type: ), bp, #f); ~(sbp & ebp) | gesture-matches?($move-gesture, button, modifiers, event-type: event-type) | gesture-matches?($copy-gesture, button, modifiers, event-type: event-type) => // Copy and move are handled at a lower level... values(#f, bp, #t); menu? & dline & x > display-line-width(dline) + 10 => // Mouse-right (press) on blank area gets a presentation for a menu values(make(, object: #f, type: ), bp, #f); event-type == #"press" | event-type == #"release" & sbp & ebp => // If it's a button press or release that is not a copy or move, // give it a presentation as well values(make(, object: make-interval(sbp, ebp), type: ), bp, ~menu?); // don't move point if a menu is requested otherwise => // Everything else just gets a raw BP values(#f, bp, #t); end end method do-presentation-at-position; define sealed class () end class ; define sealed domain make (singleton()); define sealed domain initialize (); define variable $cycle-break-gesture :: = make(, button: $left-button, modifiers: 0, event-type: #"release"); define variable $cycle-trace-gesture :: = make(, button: $left-button, modifiers: $shift-key, event-type: #"release"); define variable $step-gesture :: = make(, button: $left-button, modifiers: $control-key, event-type: #"release"); define method do-handle-presentation-event (mode :: , window :: , line :: , type == , #key bp, x, y, button = $left-button, modifiers = 0, event-type = #"press", menu-function = dylan-breakpoint-menu) => () let old-state = line-breakpoint?(mode, line); let menu? = gesture-matches?($menu-gesture, button, modifiers, event-type: event-type); when (old-state) case menu? => // Pop up a menu of operations for the breakpoint menu-function(window, mode, line, bp: bp, x: x, y: y); gesture-matches?($cycle-break-gesture, button, modifiers, event-type: event-type) => // Cycle between breakpoint states // Note that we don't ever cycle back to #"none", because // that could easily lose carefully constructed state let new-state = select (old-state) #"none" => #"enabled-break"; #"enabled-break" => #"disabled-break"; #"disabled-break" => #"enabled-break"; otherwise => #f; end; when (new-state) line-breakpoint?(mode, line) := new-state end; gesture-matches?($cycle-trace-gesture, button, modifiers, event-type: event-type) => let new-state = #"enabled-trace"; line-breakpoint?(mode, line) := new-state; gesture-matches?($step-gesture, button, modifiers, event-type: event-type) => let new-state = #"step"; line-breakpoint?(mode, line) := new-state; end; queue-redisplay(window, $display-line, line: line, index: 0, centering: #f); redisplay-window(window) end end method do-handle-presentation-event; define method dylan-breakpoint-menu (window :: , mode :: , line :: , #key bp, x, y) => () ignore(bp, x, y); // This is where DylanWorks mode calls out to the environment let new-state = choose-from-menu(window, #[#["Clear", #"none"], #["Set Breakpoint", #"enabled-break"], #["Step to here", #"step"], #["Set Tracepoint", #"enabled-trace"], #["Set Profile", #"profile"]], label-key: first, value-key: second); when (new-state) line-breakpoint?(mode, line) := new-state end end method dylan-breakpoint-menu; define sealed class () end class ; define sealed domain make (singleton()); define sealed domain initialize (); define method do-handle-presentation-event (mode :: , window :: , atom :: , type == , #key bp, x, y, button = $left-button, modifiers = 0, event-type = #"press", menu-function = dylan-atom-menu) => () let menu? = gesture-matches?($menu-gesture, button, modifiers, event-type: event-type); case gesture-matches?($edit-gesture, button, modifiers, event-type: event-type) => // Move the point so that the right definition is edited move-point!(bp, window: window); // This is where DylanWorks mode calls out to the environment #f; menu? => // Pop up a menu of operations for the atom under the mouse with-temporary-selection (window, bp) menu-function(window, mode, atom, bp: bp, x: x, y: y) selecter select-atom-under-bp(window, bp); end; otherwise => #f; end end method do-handle-presentation-event; define method dylan-atom-menu (window :: , mode :: , atom :: , #key bp, x, y) => () ignore(atom, bp, x, y); let buffer = window-buffer(window); let module = instance?(buffer-start-node(buffer), ) & node-module-name(buffer-start-node(buffer)); // This is where DylanWorks mode calls out to the environment choose-from-menu(window, #[#["Edit", #"edit"], #["Browse", #"browse"], #["Browse Class", #"browse-class"], #["Properties", #"properties"]], label-key: first, value-key: second) end method dylan-atom-menu; define method do-handle-presentation-event (mode :: , window :: , line :: , type == , #key bp, x, y, button = $left-button, modifiers = 0, event-type = #"press", menu-function = dylan-definition-line-menu) => () let menu? = gesture-matches?($menu-gesture, button, modifiers, event-type: event-type); case menu? => // Pop up a menu of operations for the atom menu-function(window, mode, line, bp: bp, x: x, y: y); otherwise => #f; end end method do-handle-presentation-event; define method dylan-definition-line-menu (window :: , mode :: , line :: , #key bp, x, y) => () ignore(line, bp, x, y); // This is where DylanWorks mode calls out to the environment choose-from-menu(window, #[#["Evaluate Definition", #"compile"], #["Browse Definition", #"browse"], #["Properties", #"properties"]], label-key: first, value-key: second) end method dylan-definition-line-menu; /// Dylan sectionizer /* //---*** Remove when debugging is complete define constant $debug-sectionizer? :: = #f; define constant $sectionizer-debug-message :: = if ($debug-sectionizer?) debug-message else ignore end; */ // First break the source container into sections, then fill the // the buffer with section nodes containing those sections //--- Note that we just ignore the issue of long comments (/* ... */) //---*** 'sectionize-buffer' methods should maintain identity of sections so //---*** that other client nodes don't lose when the old sections disappear define method do-sectionize-buffer (mode :: , buffer :: ) => (sectionized? :: ) // First split the buffer's lines up into sections let container = buffer-source-container(buffer); let (sections, header-section, module-line) = split-lines-into-sections(mode, buffer, container); insert-at!(sections, header-section, 0); container-sections(container) := sections; // Then fill the buffer with section nodes buffer-start-node(buffer) := #f; buffer-end-node(buffer) := #f; add-dylan-section-nodes!(buffer, sections, header-section: header-section, module-line: module-line); /* when ($debug-sectionizer?) let start-bp = interval-start-bp(buffer); let start-line = start-bp & bp-line(start-bp); let end-bp = interval-end-bp(buffer); let end-line = end-bp & bp-line(end-bp); // If the following fails, you can add ", verbose?: #t" to get // some useful(?) diagnostics. dynamic-bind (*buffer* = buffer) assert(last-line-from(start-line, in-buffer?: #t, verbose?: #t) == end-line, "Buffer line link structure incorrect after sectionize-buffer"); // If the following fails, you can add ", verbose?: #t" to get // some useful(?) diagnostics. assert(check-buffer-bps(buffer, verbose?: #t), "Buffer BPs incorrect after sectionize-buffer") end end; */ #t end method do-sectionize-buffer; //---*** Implement this, make 'do-sectionize-buffer' use it define method do-sectionize-container (mode :: , container :: ) => (sectionized? :: ) end method do-sectionize-container; define method do-resectionize-section (mode :: , section :: ) => (resectionized? :: ) // The idea here is to examine the current section to see if it needs // to be split into two. When we split, the existing section gets reused // for whichever part that has the same definition name. The initial // part of the section might also need to be merged with the previous // section, but we never need to merge with the following section. //---*** This will completely fall over for containerless buffers let container = section-container(section); let old-sections = container-sections(container); let old-index :: = position(old-sections, section); //---*** For debugging only... let section-buffers = map(node-buffer, section-nodes(section)); let (sections, header-section, module-line) = split-lines-into-sections(mode, section, container); // We keep the header section separate for now, to make the following // merging easier. The "screw case" is when we're resectionizing the // header of a buffer, and there are "extra" lines between it and the // first definition. let n-sections :: = size(sections) + (if (header-section) 1 else 0 end); assert(n-sections ~== 0, "Resectionizing the section resulted in no sections"); let resectionized? :: = (n-sections > 1); local method assign-section (first-line :: , section :: ) for (line = first-line then line-next(line), until: ~line) line-section(line) := section end end method; // The 'sections' we've got back (not including the one we passed in) are // in a temporary container, so now we must move them into 'container'. // If the first new section, after any header, has no defining-line, // merge it into: // (a) the section preceding 'section', unless that section is a // header section (or 'header-section' is non-#f); otherwise... // (b) the following section (the next in 'sections', or the section // following 'section'), if there is one; otherwise... // (c) uh, don't bother merging it, just exit this loop. // Loop until the new first section after merging has a defining-line. //--- Can we go around this loop more than once? let first-section :: false-or() = element(sections, 0, default: #f); block (return) while (first-section & ~section-defining-line(first-section)) local method merge-sections! (section1 :: , section2 :: , into :: ) => () let (start1, end1, start2, end2) = values(section-start-line(section1), section-end-line(section1), section-start-line(section2), section-end-line(section2)); let from :: = if (into == section1) section2 else section1 end; assert(start1 & end1 & start2 & end2, "There's an empty section while merging sections"); // Move all the lines in 'from' to 'into' assign-section(section-start-line(from), into); line-next(end1) := start2; line-previous(start2) := end1; section-start-line(into) := start1; section-end-line(into) := end2; // Update the start/end BPs of any nodes using these sections for (node :: in section-nodes(section1)) move-bp!(interval-end-bp(node), end2, line-length(end2)); node-section(node) := into end; for (node :: in section-nodes(section2)) move-bp!(interval-start-bp(node), start1, line-length(start1)); node-section(node) := into end; into.%n-lines := #f; end method; if (old-index ~== 1 & ~header-section) // Case (a): merge into the preceding section // 'old-index ~== 1' ensures we don't merge into the header section let into :: = old-sections[old-index - 1]; merge-sections!(into, first-section, into); remove!(sections, first-section) else let next-section = element(sections, 1, default: #f) | element(old-sections, old-index + 1, default: #f); if (next-section) // Case (b): merge into the following section let next-section-start-line = section-start-line(next-section); let dylan-definition-line = if (instance?(next-section-start-line, )) remove-line!(next-section, next-section-start-line); //---*** Following line is temporary until 'remove-line!' does it line-section(next-section-start-line) := #f; next-section-start-line else #f end; merge-sections!(first-section, next-section, next-section); when (dylan-definition-line) add-line!(next-section, dylan-definition-line, after: #"start") end; remove!(sections, first-section) else // Case (c): there's no possible following section, so don't merge return() end end; first-section := element(sections, 0, default: #f) end while end block; // Now insert the header with the rest of the sections. when (header-section) insert-at!(sections, header-section, 0) end; let first-section = element(sections, 0, default: #f); if (first-section) // Assign all the lines in the first new section (NB: which new // section is first may just have changed) to the original 'section', // effectively "overwriting" the lines which were there before. let start-line :: = section-start-line(first-section); let end-line :: = section-end-line(first-section); section-start-line(section) := start-line; section-end-line(section) := end-line; // 'first-section' was newly created by 'split-lines-into-sections', so // it doesn't yet have any section nodes for us to worry about. // Update the start and end BPs of any nodes using the existing section for (node :: in section-nodes(section)) move-bp!(interval-start-bp(node), start-line, 0); move-bp!(interval-end-bp(node), end-line, line-length(end-line)) end; //---*** What about any moving BPs? assign-section(section-start-line(first-section), section); section-defining-line(section) := section-defining-line(first-section); section.%signature := first-section.%signature; section.%n-lines := #f; //---*** Fix up 'window-initial-line' like 'remove-line!' does? remove!(sections, first-section); // Remove the remaining sections from their temporary container // and add them into the existing container, after 'section'. Also // add nodes to every buffer where the original section was visible. let first-section = element(sections, 0, default: #f); when (first-section) let after-section :: = section; for (section :: in sections) section-container(section) := #f; // this is how we remove it... add-section!(container, section, after: after-section); after-section := section end; for (node :: in section-nodes(section)) let buffer = node-buffer(node); when (buffer) add-dylan-section-nodes!(buffer, sections, after: section-node(section, buffer: buffer)) end end end else // The contents of the original 'section' have all been merged into // other sections, so we must dispose of 'section'. remove-section!(section-container(section), section); for (node :: in section-nodes(section)) let buffer = node-buffer(node); when (buffer) remove-node!(buffer, node) end end end; /* when ($debug-sectionizer?) for (buffer :: false-or() in section-buffers) when (buffer) let start-bp = interval-start-bp(buffer); let start-line = start-bp & bp-line(start-bp); let end-bp = interval-end-bp(buffer); let end-line = end-bp & bp-line(end-bp); // If the following fails, you can add ", verbose?: #t" to get // some useful(?) diagnostics. dynamic-bind (*buffer* = buffer) assert(last-line-from(start-line, in-buffer?: #t, verbose?: #t) == end-line, "Buffer line link structure incorrect after resectionize-section"); // If the following fails, you can add ", verbose?: #t" to get // some useful(?) diagnostics. assert(check-buffer-bps(buffer, verbose?: #t), "Buffer BPs incorrect after sectionize-section") end end end end; */ resectionized? end method do-resectionize-section; define variable *show-dylan-definition-lines?* :: = #t; define method split-lines-into-sections (mode :: , line-source :: type-union(, ), container :: ) => (sections :: , header-section :: false-or(), module-line :: false-or()) let sectionizing-buffer? :: = instance?(line-source, ); let starts-in-header? :: = sectionizing-buffer? | (line-source == first(container-sections(container))); /* $sectionizer-debug-message ("sectionizing-buffer? == %=; starts-in-header? == %=", sectionizing-buffer?, starts-in-header?); */ unless (sectionizing-buffer?) assert(section-container(line-source) == container, "The section %= is not in the correct container", line-source) end; // We assume that the lines in the interval are all in the same section, // in which case the container provided should contain that section, and // we will create our new sections in a temporary container, and move them // back to the existing container afterwards. Or, we are to ignore which // section the lines are in, in which case we put them in new sections // which have the given container (without needing to be moved). unless (sectionizing-buffer?) container := make(, pathname: "*temporary dylan container*") end; // First pick out the header and make a header section, collecting // the remaining lines for use downstream. let sections :: = make(); let lines :: = make(); let header-lines :: = make(); let header-section :: false-or() = #f; let module-line :: false-or() = #f; // 'collect' will be bound to 'collect-header-line' then later 'collect-line' let collect :: = identity; local method collect-line (line :: ) add!(lines, line) end method, method collect-header-line (line :: ) add!(header-lines, line); when (text-line?(line)) // no funny stuff in the header, please when (line-length(line) = 0) collect := collect-line; /* $sectionizer-debug-message ("Header lines:\n%=", if (empty?(header-lines)) "_None_" else reduce1(concatenate, map(curry(as, ), header-lines)) end); */ end end end method; collect := if (starts-in-header?) collect-header-line else collect-line end; // We have to make sure that we catch and safely dispose of any // s, because node start/end BPs may point to them let old-definition-lines :: = make(); do-lines(method (line, si, ei, last?) ignore(si, ei, last?); if (instance?(line, )) // Must remove these _later_, or we may confuse 'do-lines' add!(old-definition-lines, line) else // 'collect' is either 'collect-header-line' or 'collect-line' collect(line) end end method, line-source, skip-test: #f); for (line :: in old-definition-lines) remove-line!(line-section(line), line); /* $sectionizer-debug-message("Discarded definition-line") */ end; /* $sectionizer-debug-message ("Lines:\n%=", if (empty?(lines)) "_None_" else reduce1(concatenate, map(curry(as, ), lines)) end); */ local method assign-section (lines :: , start-index :: , end-index :: , section :: ) let prev-line :: false-or() = #f; for (i :: from start-index to end-index) let line :: = lines[i]; when (prev-line) line-next(prev-line) := line end; line-previous(line) := prev-line; line-section(line) := section; prev-line := line end; line-next(lines[end-index]) := #f end method; unless (empty?(header-lines)) let si :: = 0; let ei :: = size(header-lines) - 1; let start-line :: = header-lines[si]; let end-line :: = header-lines[ei]; module-line := find-module-line(header-lines); let dylan-section :: = make(, container: container, start-line: start-line, end-line: end-line, defining-line: first(header-lines)); header-section := dylan-section; assign-section(header-lines, si, ei, header-section); /* $sectionizer-debug-message ("Header section:\n%=\n", as(, header-section)) */ end; unless (empty?(lines)) // Now sectionize the rest of the lines in the file as follows: // - Set _this-start_ to the first 'define' form // - Loop doing // - Set _next-start_ to the next 'define' form // - Back up from _next-start_ to the last non-blank, // non-comment line, setting _this-end_ // - The section runs from _prev-end_ to _this-end_, // with the defining line at _this-start_ // - Set _this-start_ to _next-start_, and _prev-end_ // to _this-end_, and loop local method collect-section (si :: , ei :: , defining-line :: false-or()) => (section :: ) let start-line :: = lines[si]; let end-line :: = lines[ei]; let section = make(, container: container, start-line: start-line, end-line: end-line, defining-line: defining-line); assign-section(lines, si, ei, section); add!(sections, section); section end method; block (break) let prev-end = -1; let this-start = find-defining-line(lines, 0); let final = size(lines) - 1; if (~this-start) // Only one section in the rest of the interval let section = collect-section(0, final, #f); /* $sectionizer-debug-message ("Only one, non-definition section:\n%=\n", as(, section)); */ break() else when (~sectionizing-buffer? & this-start >= 1) // Grab any initial "extra" lines by looking backwards from // this-start with 'find-non-blank-line'. They get their own // section with no defining-line, but they'll be moved later, // in 'do-resectionize-section'. let this-end = find-non-blank-line(lines, this-start - 1); when (this-end) prev-end := this-end - 1; let section = collect-section(0, prev-end, #f); /* $sectionizer-debug-message ("'Extra' section:\n%=\n", as(, section)) */ end end; while (#t) // Despite its name, 'find-non-blank-line' can point to a blank line! // Try to make sure that blank lines after this section end up at the // end of this section, not at the beginning of the next one. let next-start = find-defining-line(lines, this-start + 1); let non-blank = next-start & (find-non-blank-line(lines, next-start - 1)); let this-end = if (non-blank & line-empty?(lines[non-blank])) non-blank elseif (non-blank) non-blank - 1 else final end; let section = collect-section(prev-end + 1, this-end, lines[this-start]); // Add section header line if requested when (*show-dylan-definition-lines?*) let diagram = make(); add-line!(section, diagram, after: #"start") end; /* $sectionizer-debug-message ("Next section:\n%=\n", as(, section)); */ if (next-start) this-start := next-start; prev-end := this-end else break() end end end end end; // Note that 'sections' does not contain 'header-section' values(sections, header-section, module-line) end method split-lines-into-sections; define method add-dylan-section-nodes! (buffer :: , sections :: , #key header-section :: false-or(
), module-line :: false-or(), after :: false-or()) => () for (section :: in sections) let start-bp = make(, line: section-start-line(section), index: 0, buffer: buffer); let end-bp = make(, line: section-end-line(section), index: line-length(section-end-line(section)), buffer: buffer, moving?: #t); let node = if (section == header-section) let module-name = module-line & trim-whitespace(copy-sequence(line-contents(module-line), start: 8)); make(, start-bp: start-bp, end-bp: end-bp, section: header-section, module-name: module-name & as-lowercase(module-name)) else make(, start-bp: start-bp, end-bp: end-bp, section: section) end; // Our caller may be adding this section to more than one buffer by // calling us repeatedly, so we must preserve existing section-nodes push!(section-nodes(section), node); if (after) add-node!(buffer, node, after: after); after := node else add-node!(buffer, node) end end end method add-dylan-section-nodes!; /* //---*** Remove when debugging is complete define function last-line-from (line :: , #key in-buffer? :: = #f, verbose? :: = #f) => (last-line :: false-or()) // Follow the line-next chain from line, until we hit #f, // returning the last line we saw. Also check that // previous(next(line)) == line (when next(line) ~== #f), i.e. that // next and previous pointers match up. let (next, previous) = if (in-buffer?) values(rcurry(line-next-in-buffer, *buffer*), rcurry(line-previous-in-buffer, *buffer*)) else values(line-next, line-previous) end; block(return) for (this = line then next(this), until: ~next(this)) when (verbose?) $sectionizer-debug-message("this = %=", line-contents(this)); end; unless(previous(next(this)) == this) when (verbose?) $sectionizer-debug-message ("this.next = %=, this.next.previous = %=", line-contents(this.next), line-contents(this.next.previous)); end when; return(#f); end; finally this end for end end function; //---*** Remove when debugging is complete define function check-buffer-bps (buffer :: , #key verbose? :: = #f) => (okay? :: ) block(return) // Check that the node-{start,end}-bps are indeed at the start/end // of lines which have no line-{previous,next}. for (node = buffer-start-node(buffer) then node-next(node), until: ~node) when (verbose?) $sectionizer-debug-message("node: %=", node.node-definition-name); end when; let start-bp = interval-start-bp(node); let start-line = bp-line(start-bp); let start-index = bp-index(start-bp); let end-bp = interval-end-bp(node); let end-line = bp-line(end-bp); let end-index = bp-index(end-bp); unless ( ~line-previous(start-line) & start-index == 0 & ~line-next(end-line) & end-index == line-length(end-line) & ~moving-bp?(start-bp) & moving-bp?(end-bp) & ~simple-bp?(start-bp) & ~simple-bp?(end-bp)) when (verbose?) ~line-previous(start-line) | $sectionizer-debug-message("start line has a previous line"); start-index == 0 | $sectionizer-debug-message("start index not == 0"); ~line-next(end-line) | $sectionizer-debug-message("end line has a next line"); end-index == line-length(end-line) | $sectionizer-debug-message ("end index %= not == line length %=", end-index, line-length(end-line)); ~moving-bp?(start-bp) | $sectionizer-debug-message("start BP is a "); moving-bp?(end-bp) | $sectionizer-debug-message("end BP is not a "); ~simple-bp?(start-bp) | $sectionizer-debug-message("start BP is not a "); ~simple-bp?(end-bp) | $sectionizer-debug-message("end BP is not a "); end when; return(#f); end unless; end for; // Check that all line-bps are moving-bps, that they're on the // right line and that their indices are in range for the line // they're on. do-lines (method (line, si, ei, last?) ignore(si, ei, last?); for (bp :: in line-bps(line)) let _line = bp-line(bp); let _index = bp-index(bp); unless (moving-bp?(bp) & _line == line & 0 <= _index & _index <= line-length(line)) when (verbose?) moving-bp?(bp) | $sectionizer-debug-message("line BP is not a "); _line == line | $sectionizer-debug-message("line BP is on wrong line"); 0 <= _index | $sectionizer-debug-message("line BP has index < 0"); _index <= line-length(line) | $sectionizer-debug-message("line BP has index > line length"); end when; return(#f); end unless; end for; end method, buffer); // If we got this far, everything's okay. #t end; end function; */ // Search forward for the "Module:" line define method find-module-line (header-lines :: ) => (module-line :: false-or()) block (return) for (line in header-lines) when (text-line?(line)) let length = line-length(line); let contents = line-contents(line); when (length > 7 & string-equal?(contents, "Module:", end1: 7)) return(line) end end end; values(#f, #f) end end method find-module-line; // Search forward for a line starting with 'define' or 'begin', // returning the index of that line define method find-defining-line (lines :: , index :: ) => (line :: false-or()) block (return) let n-lines :: = size(lines); for (i :: from index below n-lines) let line = lines[i]; when (text-line?(line)) let length = line-length(line); let contents = line-contents(line); /* $sectionizer-debug-message ("Seeking define line at line %=/%=: %=", i, n-lines, contents); */ when ( (length > 6 & string-equal?(contents, "define", end1: 6)) // '>=' because 'begin' might stand alone on this line | (length >= 5 & string-equal?(contents, "begin", end1: 5))) /* $sectionizer-debug-message ("Found define line at line %=/%=", i, n-lines); */ return(i) end end end; #f end end method find-defining-line; // Search backward for a non-blank, non-comment line, // returning the index of the following line define method find-non-blank-line (lines :: , index :: ) => (line :: false-or()) block (return) while (index >= 0) let line = lines[index]; when (text-line?(line)) let length = line-length(line); let contents = line-contents(line); let bp = find-key(contents, method (ch) ~whitespace-char?(ch) end, failure: length); when (length - bp > 2 & ~string-equal?(contents, "//", start1: bp, end1: bp + 2)) return(index + 1) end end; index := index - 1 end; #f end end method find-non-blank-line; /// Dylan parsing define sealed method do-relevant-function-interval (mode :: , bp :: ) => (interval :: false-or()) let node = bp-node(bp) | bp-buffer(bp); // First try to find the atom right under the point let sbp = if (atom-syntax(bp-character-before(bp)) == $atom-delimiter) forward-over(bp, #[' ', '\t', '\f'], interval: node) else move-over-atoms(bp, -1, interval: node) end; let ebp = move-over-atoms(sbp, 1, interval: node); if (bp-character-before(sbp) = '.') make-interval(sbp, ebp, in-order?: #t) else let lbp = move-over-lists(move-over-lists(ebp, 1, interval: node), -1, interval: node); unless (bp-character(lbp) = '(') // If that atom doesn't start a function call, move backwards until // we find what might be a function call, and pick _that_ atom sbp := decrement-bp!(backward-until(sbp, #['('], interval: node)); sbp := move-over-atoms(sbp, -1, interval: node); ebp := move-over-atoms(sbp, 1, interval: node) end; make-interval(sbp, ebp, in-order?: #t) end end method do-relevant-function-interval; define method do-atom-under-bp (mode :: , bp :: ) => (sbp :: , ebp :: ) let node = bp-node(bp) | bp-buffer(bp); let sbp = if (atom-syntax(bp-character-before(bp)) == $atom-delimiter) forward-over(bp, #[' ', '\t', '\f'], interval: node) else move-over-atoms(bp, -1, interval: node) end; let ebp = move-over-atoms(sbp, 1, interval: node); // Some characters are magic at the beginning of an atom // Skip them, unless the user is right on top of one when (bp ~= sbp & member?(bp-character(sbp), #['+', '-', '~', '?', '/'])) increment-bp!(sbp) end; values(sbp, ebp) end method do-atom-under-bp; define sealed method do-move-over-expressions (mode :: , bp :: , n :: , #key fixup? = #t, interval = bp-buffer(bp)) => (bp :: false-or()) //---*** Do this for real move-over-lists(bp, n, fixup?: fixup?, interval: interval) end method do-move-over-expressions; define sealed method do-move-up-or-down-expressions (mode :: , bp :: , n :: , #key fixup? = #t, interval = bp-buffer(bp)) => (bp :: false-or()) //---*** Do this for real move-up-or-down-lists(bp, n, fixup?: fixup?, interval: interval) end method do-move-up-or-down-expressions; /// Dylan signatures define sealed method compute-file-signatures (pathname :: , #key editor = frame-editor(*editor-frame*)) => (buffer :: false-or()) // Read the file into a buffer (sectionizing it in the process), // then compute the signatures for the file buffer let buffer = find-buffer-from-pathname(editor, pathname) | do-find-file(editor, pathname, direction: #"input"); buffer & compute-buffer-signatures(buffer); buffer end method compute-file-signatures; define sealed method compute-buffer-signatures (buffer :: ) => (buffer :: ) let container = buffer-source-container(buffer); when (container) do(compute-section-signature, container-sections(container)) end; buffer end method compute-buffer-signatures; define sealed method compute-section-signature (section :: ) => (signature :: false-or()) #f end method compute-section-signature; define sealed method compute-section-signature (section :: ) => (signature :: false-or()) let start-line = section-defining-line(section); let end-line = section-end-line(section); let signature = when (start-line) let contents = line-contents(start-line); let length = line-length(start-line); case string-equal?(contents, "define", start1: 0, end1: min(length, 6)) => let interval = make-interval(line-start(start-line), line-end(end-line), in-order?: #t); let stream :: = make(, interval: interval, direction: #"input"); block () parse-dylan-signature(stream) cleanup close(stream) end; string-equal?(contents, "begin", start1: 0, end1: min(length, 5)) => make(, type: #"top-level-form", name: "begin ... end"); otherwise => #f; end; end; section.%signature := signature; signature end method compute-section-signature; define sealed class () sealed constant slot signature-type :: , required-init-keyword: type:; sealed constant slot signature-name :: , required-init-keyword: name:; sealed constant slot signature-parameters :: = #[], init-keyword: parameters:; end class ; define sealed domain make (singleton()); define sealed domain initialize (); // Note that all definitions whose type is #"top-level-form" will compare as // equal, meaning that such forms will need to be disambiguated at a higher // level, e.g., by comparing the text of sections define sealed method \= (s1 :: , s2 :: ) => (equal? :: ) signature-type(s1) = signature-type(s2) & signature-name(s1) = signature-name(s2) & signature-parameters(s1) = signature-parameters(s2) end method \=; define sealed inline method token-terminator? (ch :: ) => (true? :: ) any-whitespace-char?(ch) | ch == ',' | ch == ';' | ch == '.' | ch == '(' | ch == ')' | ch == '[' | ch == ']' | ch == '{' | ch == '}' end method token-terminator?; define sealed method parse-dylan-signature (stream :: ) => (signature :: false-or()) let buffer :: = make(); let params :: = make(); local method skip-whitespace () => () while (any-whitespace-char?(peek(stream, on-end-of-stream: '\0'))) read-element(stream) end end method, method next-name () => (name :: ) skip-whitespace(); buffer.size := 0; block (return) for (ch = read-element(stream, on-end-of-stream: #f) then read-element(stream, on-end-of-stream: #f)) case ~ch => return(as(, buffer)); ch == '\\' => add!(buffer, read-element(stream, on-end-of-stream: '\0')); token-terminator?(ch) => if (buffer.size = 0) add!(buffer, ch) else unread-element(stream, ch) end; return(as(, buffer)); otherwise => add!(buffer, ch); end end end end method, method next-token () => (token :: ) as(, next-name()) end method, // The idea is to parse simple type expressions such as '', // 'false-or()', 'one-of(#t, #f)', etc. //---*** We need to do a better job than this... next-expression (format-string :: ) => (expression :: ) let name = next-name(); let more = ""; skip-whitespace(); when (peek(stream, on-end-of-stream: #f) == '(') // If this token starts a function call, pick up the arguments let bp1 = stream-position(stream); let bp2 = move-over-lists(bp1, 1, fixup?: #f); when (bp2) let interval = make-interval(bp1, bp2, in-order?: #t); more := as(, interval); stream-position(stream) := increment-bp!(bp2) end end; format-to-string(format-string, concatenate(name, more)) end method; local method parse-function-parameters () => (parameters :: ) if (next-token() ~== #"(") #[] else params.size := 0; block (break) while (#t) // The next token should be either a parameter name or something like #key let token = next-token(); when (member?(token, #[#"#rest", #"#key", #"#all-keys", #"#next"])) break() end; // We got a name, get the next token let next = next-token(); case next == #"," | next == #")" => add!(params, ""); next == #"::" => add!(params, next-expression("%s")); next := next-token(); next == #"==" => add!(params, next-expression("singleton(%s)")); next := next-token(); end; unless (next == #",") break() end end end; as(, params) end end method, method parse-domain-parameters () => (parameters :: ) if (next-token() ~== #"(") #[] else params.size := 0; block (break) while (#t) add!(params, next-expression("%s")); let next = next-token(); unless (next == #",") break() end end end; as(, params) end end method; // We should be looking at a defining form, but if we're not, // just give up and don't return a signature when (next-token() == #"define") block (return) while (#t) let token = next-token(); select (token) #"library", #"module", #"interface", #"protocol", #"macro" => let name = next-name(); return(make(, type: token, name: name)); #"class", #"frame" => let name = next-name(); return(make(, type: token, name: name)); #"generic" => let name = next-name(); return(make(, type: token, name: name, parameters: parse-function-parameters())); #"function", #"method" => let name = next-name(); return(make(, type: token, name: name, parameters: parse-function-parameters())); #"domain" => let name = next-name(); return(make(, type: token, name: name, parameters: parse-domain-parameters())); #"sealed", #"open", #"primary", #"free", #"abstract", #"concrete", #"sideways", #"thread", #"atomic", #"inline", #"inline-only", #"may-inline", #"default-inline", #"not-inline" => #f; otherwise => let name = next-name(); return(make(, type: token, name: name)); end end end end end method parse-dylan-signature; /// Dylan comments and indentation define method do-insert-comment (mode :: , line :: , #key column) => (bp :: false-or()) if (column) //--- This should attempt to insert the comment at the given column let bp = line-end(line); insert-moving!(bp, "\t// ") else let bp = line-start(line); insert-moving!(bp, "// ") end end method do-insert-comment; define method do-comment-region (mode :: , region :: , #key comment?) => () block (return) local method comment (line :: , si, ei, last?) ignore(last?); // Comment out the line only if the start index is zero and // the end index is not zero when (si = 0 & ei ~= 0) insert-into-line(line, 0, "// ") end end method, method uncomment (line :: , si, ei, last?) ignore(last?); when (si = 0 & ei ~= 0) let length = line-length(line); let contents = line-contents(line); if (length >= 3 & string-equal?(contents, "// ", end1: 3)) delete-within-line(line, 0, 3) else return() end end end method; do-lines(if (comment?) comment else uncomment end, region) end end method do-comment-region; define method do-indent-line (mode :: , line :: ) => (bp :: false-or(), dx :: , nchars :: ) indent-dylan-line(mode, line) end method do-indent-line; define sealed method indent-dylan-line (mode :: , line :: , #key definition-type) => (bp :: false-or(), dx :: , nchars :: ) let bp1 = line-start(line); let bp2 = forward-over(bp1, #[' ', '\t']); local method insert-indentation! (line :: , indentation :: ) => (bp :: , nchars :: ) if (text-line?(line)) let interval = make-interval(bp1, bp2, in-order?: #t); let old-n = count-characters(interval); let new-n = max(indentation, 0); delete!(interval); let spaces = make(, size: new-n, fill: ' '); values(insert-moving!(bp1, spaces), new-n - old-n) else values(bp1, 0) end end method; let (old-indentation, new-indentation) = dylan-line-indentation(mode, line, definition-type: definition-type); let dx = new-indentation - old-indentation; if (dx = 0) values(bp2, 0, 0) else let (bp, nchars) = insert-indentation!(line, new-indentation); values(bp, nchars, dx) end end method indent-dylan-line; //---*** We could sure use a better indenter than this! define sealed method dylan-line-indentation (mode :: , line :: , #key definition-type) => (old-indentation :: , new-indentation :: ) let window :: = frame-window(*editor-frame*); let margin = line-margin(line, mode, window); let space-width = string-size(window, " "); local method line-indentation (sbp :: ) => (indentation :: ) if (text-line?(bp-line(sbp))) let indentation = index->position(bp-line(sbp), mode, window, bp-index(sbp)) - margin; floor/(indentation, space-width) else 0 end end method, method looking-at? (contents :: , _start :: , _end :: , string :: ) => (true? :: ) let _end = min(_start + size(string), _end); string-equal?(contents, string, start1: _start, end1: _end) end method, method looking-at-atom? (contents :: , _start :: , _end :: , string :: ) => (true? :: ) let _end = min(_start + size(string), _end); string-equal?(contents, string, start1: _start, end1: _end) /* & (_end >= size(contents) | atom-syntax(contents[_end]) ~= $atom-alphabetic) */ end method, method bp-looking-at-call? (bp :: , string :: ) => (true? :: ) bp-looking-at-atom?(bp, string) & begin let bp = forward-over!(move-over-atoms(bp, 1), #[' ', '\t']); bp-character(bp) == '(' end end method; let section = line-section(line); let type = definition-type | section-definition-type(section); let this-bp = line-start(line); let this-sbp = forward-over(this-bp, #[' ', '\t']); let this-indentation :: = line-indentation(this-sbp); let prev-line = line-previous(line); // stay within this node let prev-bp = prev-line & line-start(prev-line); let prev-sbp = prev-bp & forward-over(prev-bp, #[' ', '\t']); let prev-indentation :: = if (prev-sbp) line-indentation(prev-sbp) else 0 end; let indentation :: = prev-indentation; block (break) when (text-line?(line)) let line = bp-line(this-sbp); let index = bp-index(this-sbp); let length = line-length(line); let contents = line-contents(line); local method maybe-hack-function-indentation () dec!(indentation, 2); when (prev-line) let l = line-length(prev-line); let c = line-contents(prev-line); when (l >= 2 & ((looking-at?(c, l - 2, l, ");") & dec!(l)) | looking-at?(c, l - 1, l, ")"))) let bp = move-over-lists(make-bp(prev-line, l), -2); let bp = move-bp!(bp, bp-line(bp), 0); let bp = forward-over!(bp, #[' ', '\t']); unless (bp-looking-at-call?(bp, "for") | bp-looking-at-call?(bp, "while") | bp-looking-at-call?(bp, "until") | bp-looking-at-call?(bp, "if") | bp-looking-at-call?(bp, "when") | bp-looking-at-call?(bp, "unless")) indentation := line-indentation(bp) - 2; break() end end end end method; when (type == #"function" | type == #"method") end; case looking-at-atom?(contents, index, length, "end") => maybe-hack-function-indentation(); looking-at-atom?(contents, index, length, "else") | looking-at-atom?(contents, index, length, "elseif") => maybe-hack-function-indentation(); looking-at-atom?(contents, index, length, "exception") | looking-at-atom?(contents, index, length, "cleanup") | looking-at-atom?(contents, index, length, "finally") => maybe-hack-function-indentation(); looking-at?(contents, index, length, "= ") | looking-at?(contents, index, length, ":= ") => inc!(indentation, 2); break(); looking-at-atom?(contents, index, length, "define") => // 'define' forms always start at the beginning of the line indentation := 0; break(); type == #"class" & (looking-at-atom?(contents, index, length, "slot") | looking-at-atom?(contents, index, length, "sealed slot") | looking-at-atom?(contents, index, length, "constant slot") | looking-at-atom?(contents, index, length, "sealed constant slot") | looking-at-atom?(contents, index, length, "open slot") | looking-at-atom?(contents, index, length, "virtual slot") | looking-at-atom?(contents, index, length, "keyword")) => indentation := 2; break(); looking-at?(contents, index, length, "=>") => if (bp-line(move-over-lists(this-bp, -2)) == section-defining-line(section)) // Assume '=>' at the beginning of the line is for return values indentation := 1; break() else inc!(indentation, 2) end; (type == #"function" | type == #"method") & length >= 2 & looking-at?(contents, length - 2, length, "=>") => dec!(indentation, 2); end end; when (prev-line & text-line?(prev-line)) let line = bp-line(prev-sbp); let index = bp-index(prev-sbp); let length = line-length(line); let contents = line-contents(line); when (type == #"function" | type == #"method") when (length >= 1 & looking-at?(contents, length - 1, length, ",") & ~bp-looking-at?(forward-over(prev-bp, #[' ', '\t']), "//")) // Comma at the end of a line is probably inside a parameter list, // so indent to one past the enclosing parenthesis let pbp = move-up-or-down-lists(this-bp, -1); indentation := line-indentation(pbp) + 1; break() end; let l = length; when (l >= 2 & ((looking-at?(contents, l - 2, l, ");") & dec!(l)) | looking-at?(contents, l - 1, l, ")"))) // We're probably looking at the end of a function call, // so indent to the same level as the function call let bp = move-over-lists(make-bp(line, l), -2); let bp = move-bp!(bp, bp-line(bp), 0); let bp = forward-over!(bp, #[' ', '\t']); if (bp-looking-at-call?(bp, "for") | bp-looking-at-call?(bp, "while") | bp-looking-at-call?(bp, "until") | bp-looking-at-call?(bp, "if") | bp-looking-at-call?(bp, "when") | bp-looking-at-call?(bp, "unless") | bp-looking-at-call?(bp, "block") | bp-looking-at-call?(bp, "exception") | bp-looking-at-call?(bp, "select") | bp-looking-at-call?(bp, "dynamic-bind") | bp-looking-at-call?(bp, "with-") | bp-looking-at-call?(bp, "without-") | bp-looking-at-call?(bp, "iterate") | bp-looking-at-call?(bp, "collecting")) indentation := line-indentation(bp) + 2 elseif (bp-looking-at?(bp, "local method ")) indentation := line-indentation(bp) + 8 else indentation := line-indentation(bp); when (bp-looking-at?(bp, "= ") | bp-looking-at?(bp, ":= ")) dec!(indentation, 2) end end; break() end; when (length >= 2 & looking-at?(contents, length - 2, length, "=>")) // "=>" at the end of a line is probably for 'case' or 'select' inc!(indentation, 2) end end; when (looking-at?(contents, index, length, "= ")) inc!(index, 2); inc!(indentation, 2) end; case looking-at-atom?(contents, index, length, "define") => inc!(indentation, 4); looking-at-atom?(contents, index, length, "begin") => inc!(indentation, 2); looking-at-atom?(contents, index, length, "block") | looking-at-atom?(contents, index, length, "exception") | looking-at-atom?(contents, index, length, "cleanup") | looking-at-atom?(contents, index, length, "finally") => inc!(indentation, 2); looking-at-atom?(contents, index, length, "when") | looking-at-atom?(contents, index, length, "unless") => inc!(indentation, 2); looking-at-atom?(contents, index, length, "if") | looking-at-atom?(contents, index, length, "else") | looking-at-atom?(contents, index, length, "elseif") => inc!(indentation, 2); looking-at-atom?(contents, index, length, "while") | looking-at-atom?(contents, index, length, "until") => inc!(indentation, 2); looking-at-atom?(contents, index, length, "for") => inc!(indentation, 2); looking-at-atom?(contents, index, length, "case") => inc!(indentation, 2); looking-at-atom?(contents, index, length, "select") => inc!(indentation, 2); looking-at-atom?(contents, index, length, "local method") => inc!(indentation, 8); looking-at-atom?(contents, index, length, "slot") | looking-at-atom?(contents, index, length, "sealed slot") | looking-at-atom?(contents, index, length, "open slot") | looking-at-atom?(contents, index, length, "virtual slot") => inc!(indentation, 2); // Special hack for 'with-XXX' macros, etc. looking-at?(contents, index, length, "with-") | looking-at?(contents, index, length, "without-") | looking-at-atom?(contents, index, length, "dynamic-bind") => inc!(indentation, 2); end end end; values(this-indentation, indentation) end method dylan-line-indentation; define command dylan-insert-block-end (frame) "Insert an 'end' to close the current block." let window :: = frame-window(frame); let buffer :: = frame-buffer(frame); let mode = buffer-major-mode(buffer); let bp1 = point(); let line1 = bp-line(bp1); let new? = ~line-empty?(line1); let section = line-section(bp-line(bp1)); with-change-recording (buffer, , start-bp: bp1) let (name, type) = values(section-definition-name(section), section-definition-type(section)); let bp2 = if (new?) insert-moving!(line-end(line1), '\n') else line-start(line1) end; let line2 = bp-line(bp2); insert-moving!(bp2, "end"); indent-dylan-line(mode, line2); move-bp!(bp2, line2, line-length(line2)); case type == #"function" | type == #"method" => if (string-equal?(line-contents(line2), "end", end1: 3)) // If we're in a function body and there's no indentation, // insert the "end of definition" frob insert-moving!(bp2, format-to-string(" %s %s;", as-lowercase(as(, type)), name)) else // Take a reasonable guess as to whether to insert a ";" let next = line-next(line2); let nbp = next & forward-over!(line-start(next), #[' ', '\t']); when (nbp & ~(bp-looking-at?(nbp, "end") | bp-looking-at?(nbp, "else") | bp-looking-at?(nbp, "elseif"))) insert-moving!(bp2, ";") end end; type == #"top-level-form" => insert-moving!(bp2, ";"); type & name => // Not within a function body, always insert the "end of definition" frob insert-moving!(bp2, format-to-string(" %s %s;", as-lowercase(as(, type)), name)); type => insert-moving!(bp2, format-to-string(" %s;", as-lowercase(as(, type)))); otherwise => #f; end; move-point!(bp2); if (new?) // The following bit is based on 'do-insert-newline' let last? = (line2 == bp-line(interval-end-bp(buffer))); if (last?) queue-redisplay(window, $display-text, centering: 1) else queue-redisplay(window, $display-blt, line: line1, index: 1, centering: 1) end else queue-redisplay(window, $display-line, line: line1, index: 0, centering: 1) end end; frame-last-command-type(frame) := #"insert" end command dylan-insert-block-end; /// Breakpoints define method line-breakpoint? (mode :: , line :: ) => (state :: ) let type = section-definition-type(line-section(line)); when (type == #"function" | type == #"method" | type == #"command") if (line-empty?(line) | bp-looking-at?(line-start(line), "//")) #f else //--- This is where DylanWorks mode calls out to the environment get-property(line-properties(line), #"breakpoint", default: #"none") end end end method line-breakpoint?; define method line-breakpoint?-setter (state :: , mode :: , line :: ) => (state :: ) //--- This is where DylanWorks mode calls out to the environment put-property!(line-properties(line), #"breakpoint", state); state end method line-breakpoint?-setter; /// Dylan shells define open class () end class ; define method initialize-major-mode (mode :: , #key command-set = mode-command-set(mode)) => () let command-set = install-dylan-mode-bindings(mode, command-set); next-method(mode, command-set: command-set) end method initialize-major-mode; begin gethash(*keyword->major-mode*, #"dylan-shell") := end; define method mode-name (mode :: ) => (name :: ) "Dylan shell" end method mode-name; define method do-indent-line (mode :: , line :: ) => (bp :: false-or(), dx :: , nchars :: ) indent-dylan-line(mode, line) end method do-indent-line; define method shell-input-complete? (mode :: , buffer :: , section :: ) => (complete? :: , message :: false-or()) //--- This is where DylanWorks mode decides if there's a complete form values(#t, #f) end method shell-input-complete?; define method do-process-shell-input (mode :: , buffer :: , section :: , #key window = frame-window(*editor-frame*)) => () ignore(window); let bp = line-end(section-end-line(section)); queue-redisplay(window, $display-text); //--- This is where DylanWorks mode evaluates the form and prints the results insert-moving!(bp, "You wish this worked!\n"); move-point!(bp, window: window) end method do-process-shell-input; define variable *dylan-shell-count* :: = 0; define method make-dylan-shell (#key name, anonymous? = #f, buffer-class = , major-mode = find-mode(), section-class = , editor = frame-editor(*editor-frame*)) => (buffer :: ) unless (name) inc!(*dylan-shell-count*); name := format-to-string("Dylan shell %d", *dylan-shell-count*) end; let buffer = make-empty-buffer(buffer-class, name: name, major-mode: major-mode, anonymous?: anonymous?, section-class: section-class, editor: editor); shell-buffer-section-class(buffer) := section-class; buffer end method make-dylan-shell; /// Display within shells //--- Need a more modular way to do this define constant $prompt-image-width :: = 16; define constant $prompt-image-height :: = 16; define constant $prompt-image-offset :: = 2; define sealed method display-line (line :: , mode :: , window :: , x :: , y :: , #key start: _start = 0, end: _end = line-length(line), align-y = #"top") => () let section = line-section(line); let image = case ~shell-section?(section) => #f; line == section-start-line(section) => $prompt-arrow; line == section-output-line(section) => $values-arrow; otherwise => #f; end; when (image & _start = 0) // no icon on continuation lines let image-y = if (align-y == #"top") y else y - $prompt-image-height + 2 end; draw-image(window, standard-images(window, image), x, image-y + $prompt-image-offset) end; next-method(line, mode, window, x + $prompt-image-width, y, start: _start, end: _end, align-y: align-y) end method display-line; define sealed method line-size (line :: , mode :: , window :: , #key start: _start, end: _end) => (width :: , height :: , baseline :: ) ignore(_start, _end); let (width, height, baseline) = next-method(); values(width + $prompt-image-width, height, baseline) end method line-size; define sealed method position->index (line :: , mode :: , window :: , x :: ) => (index :: ) let x = x - $prompt-image-width; if (x < 0) 0 else next-method(line, mode, window, x) end end method position->index; define sealed method index->position (line :: , mode :: , window :: , index :: ) => (x :: ) next-method(line, mode, window, index) end method index->position; define sealed method line-margin (line :: , mode :: , window :: ) => (margin :: ) $prompt-image-width end method line-margin;