#
# $Id: editabletextlist.icn,v 1.24 2010-06-24 06:17:34 jeffery Exp $
#
# This file is in the public domain.
#
# Author: Robert Parlett (parlett@dial.pipex.com)
#

package gui
link graphics

import undo
import lang

$include "guih.icn"

#
# Remappable keybindings that return string names of methods bound
# to various keys. At present this relies on the method vector
# representation.
#
class Keybindings(keymap)
   method handle_key(e)
      return \ (keymap[e])
   end
   method set_key(e,m,ems[])
      repeat {
         keymap[e] := m
         if not (e := pop(ems) & m := pop(ems)) then break
         }
   end
   # The original/default Unicon EditableTextList keybindings.
   # Resets the keymap, in case other keys have been bound strangely.
   method default_mode()
   keymap := table("handle_default")
   keymap[Key_Home] := "handle_key_home"
   keymap[Key_End] := "handle_key_end"
   keymap[Key_PgUp] := "handle_key_page_up"
   keymap[Key_PgDn] := "handle_key_page_down"
   keymap[Key_Up] := "handle_key_up"
   keymap[Key_Down] := "handle_key_down"
   keymap[Key_Left] := "handle_key_left"
   keymap[Key_Right] := "handle_key_right"
   keymap["\b"] := "handle_delete_left"
   keymap["\r"] := keymap["\l"] := "handle_return"
   keymap["\^k"] := "handle_delete_line"
   keymap["\^a"] := "handle_select_all"
   keymap["\^e"] := "handle_end_of_line"
   keymap["\d"] := keymap["\^d"] := "handle_delete_right"
   keymap["\^x"] := "handle_cut"
   keymap["\^c"] := "handle_copy"
   keymap["\^v"] := "handle_paste"
   keymap["\^z"] := "handle_undo"
   keymap["\^y"] := "handle_redo"
   keymap["\^s"] := "handle_stringsearch"
   end

initially
   default_mode()
end

#
# A scrollable editable text area.
# An CONTENT_CHANGED_EVENT is generated whenever the contents
# are changed by the user, a CURSOR_MOVED_EVENT when the cursor moves,
# and a SELECTION_CHANGED_EVENT whenver the selection changes.
#
class EditableTextList : LineBasedScrollArea(
   contents,
   printable,               # The printable characters
   cursor_x,                #
   cursor_y,                #
   mark_x,                  #
   mark_y,                  #
   direction,               #
   is_held,
   long_line,               #
   undo_manager,
   old_view_list_size,
   old_mw,
   old_cursor_x,
   old_cursor_y,
   old_has_region,
   view_list,
   tab_width,
   wrap_mode,
   line_splitter,
   changed,
   findstr,
   kmap
   )

   method get_view_x_padding()
      return DEFAULT_TEXT_X_SURROUND
   end

   method get_view_y_padding()
      return DEFAULT_TEXT_Y_SURROUND
   end

   method get_line_count()
      return *self.view_list
   end

   method get_contents()
      return self.contents
   end

   #
   # Set the component to be transparent
   #
   method set_transparent()
      self.transparent := 1
      view.set_transparent()

   end

   #
   # Set the component to be non-transparent
   #
   method clear_transparent()
      self.transparent := &null
      view.clear_transparent()
   end


   #
   # Set the contents of the component.
   #
   # @param x the contents, as a list of strings
   #
   method set_contents(x)
      self.contents := x
      contents_changed()
   end

   #
   # Call this method if the contents list, previously
   # set with {set_contents()}, has changed.
   #
   method contents_changed()
      if *self.contents = 0 then
         #
         # Must have somewhere for the cursor to go.
         #
         self.contents := [""]

      clear_view_list()
      clear_mark()
      if is_dialog_open() then {
         self.cursor_y >:= *self.contents
         self.cursor_x >:= *self.contents[self.cursor_y] + 1
         compute_and_invalidate()
         constrain_line()
      }
      undo_manager.clear()
   end

   #
   # Move cursor y to line n, and constrain x within range of that line.
   #
   # @p
   method set_cursor_y(n)
      local d, i, dest

      # Search for the nearest x position on the new line corresponding to
      # the current x position.
      d := TextWidthEx(cwin, contents[cursor_y], 1, cursor_x, tab_width)
      dest := self.contents[n]
      i := 1
      while (i <= *dest) & (TextWidthEx(cwin, dest, 1, i, tab_width) < d) do
         i +:= 1

      self.cursor_x := i
      self.cursor_y := n
      return n
   end

   #
   # Move cursor so that it is in the text area, if possible.  May not be
   # possible if cursor at end of line to the left of the text area.
   #
   # @p
   method constrain_cursor()
      local vi, v, s, i, j, l

      vi := get_view_list_index()

      if vi < self.get_first_line() then {
         v := view_list[self.get_first_line()]
         self.cursor_y := v.index
         self.cursor_x := v.first
         }
      else if vi >= self.get_first_line() + self.get_max_lines() then {
         v := view_list[self.get_first_line() + self.get_max_lines() - 1]
         self.cursor_y := v.index
         self.cursor_x := v.first
         }
      else {
         v := view_list[vi]
         }

      s := v.str
      i := TextWidthEx(cwin, s, 1, cursor_x - v.first + 1, tab_width)
      j := i + CharWidth(self.cwin, s[self.cursor_x - v.first + 1])
      l := self.get_left_pos()
      if self.view.x - l > i then {
         while (self.cursor_x - v.first + 1 < *s) &
            (TextWidthEx(cwin, s, 1, cursor_x - v.first + 1, tab_width) <
             view.x - l) do {
            cursor_x +:= 1
            }
         }
      else if self.view.x - l + self.view.w < j then {
         while (self.cursor_x - v.first + 1 > 1) &
            TextWidthEx(cwin, s, 1, cursor_x - v.first + 2, tab_width) >
             view.x - l + view.w do {
            cursor_x -:= 1
            }
      }
   end

   #
   # Move the text area displayed so that the cursor is on the screen.
   #
   # @p
   method constrain_line()
      local vi, v, s, i, j, l

      if not (vi := get_view_list_index()) then {
         # serious error, what to do?
         fail
         }

      if \vi < self.get_first_line() then
         self.vsb.set_value(self.line_height * (\vi - 1))
      else if \vi > self.get_last_line() then
         self.vsb.set_value(self.line_height * (\vi - self.get_max_lines()))

      if not ( v := view_list[\vi] ) then {
         # serious error, what to do?
         fail
         }
      s := (\v).str
      i := TextWidthEx(cwin, s, 1, cursor_x - v.first + 1, tab_width)
      j := i + CharWidth(self.cwin, s[self.cursor_x - v.first + 1])
      l := self.get_left_pos()
      if \self.hsb then {
      if self.view.x - l > \i then
         self.hsb.set_value(i)
      else if self.view.x - l + self.view.w < \j then
         self.hsb.set_value(j - self.view.w)
      }
   end

   method handle_cut(e)
      start_handle(e)
      if has_region() then {
         get_clipboard().set_content(get_region())
         delete_region(e)
      }
      end_handle(e)
   end

   method handle_copy(e)
      start_handle(e)
      if has_region() then {
         get_clipboard().set_content(get_region())
      }
      end_handle(e)
   end

   method can_undo()
      return undo_manager.can_undo()
   end

   method can_redo()
      return undo_manager.can_redo()
   end

   method get_pasteable_clipboard()
      local x, t, s, c
      x := get_clipboard().get_content()
      t := string(x) | fail
      # Apply the filter to the string to paste
      s := ""
      every c := !t do {
         if member(printable, c) then
            s ||:= c
      }
      if *s = 0 then fail
      return s
   end

   method handle_paste(e)
      local s, ce, ed

      start_handle(e)
      if s := get_pasteable_clipboard() then {
         ce := CompoundEdit()

         if has_region() then {
            ed := EditableTextListDeleteRegionEdit(self)
            ed.redo()
            ce.add_edit(ed)
         }
         ed := EditableTextListPasteEdit(self, s)
         ed.redo()
         ce.add_edit(ed)
         ce.close()
         undo_manager.add_edit(ce)
         changed := 1
      }
      end_handle(e)
   end

   method on_vsb(ev)
      start_handle()
      self.constrain_cursor()
      self.refresh()
      end_handle(ev)
   end

   method on_hsb(ev)
      start_handle()
      self.constrain_cursor()
      self.refresh()
      end_handle(ev)
   end

   method start_handle()
      old_view_list_size := *view_list
      old_mw := long_line.tw
      old_cursor_x := cursor_x
      old_cursor_y := cursor_y
      old_has_region := has_region() | &null
      changed := &null
   end

   method end_handle(e)
      local hr, moved
      if \changed then {
         if (*view_list ~= old_view_list_size) |
            (old_mw ~= long_line.tw) then {
            #
            # Contents changed.  Re-compute all internal fields, ensure on
            # screen and re-display whole object.
            #
            self.set_internal_fields()
            self.constrain_line()
            self.invalidate()
            }
         else {
            self.constrain_line()
            self.refresh(1)
            }
         fire(CONTENT_CHANGED_EVENT, e)
         }

      if (cursor_x ~= old_cursor_x) | (cursor_y ~= old_cursor_y) then {
         moved := 1
         if /changed then {
            self.constrain_line()
            self.refresh(1)
         }
         fire(CURSOR_MOVED_EVENT, e)
      }

      #
      # Deduce a region change from looking for a change in whether there
      # was/is a region; or if there is and was a region and if the cursor
      # has moved, or the content changed.
      #
      hr := has_region()
      if (/old_has_region & \hr) | (\old_has_region & /hr) |
         (\hr & (\moved | \changed)) then {
         self.invalidate()
         fire(SELECTION_CHANGED_EVENT, e)
         }
   end

   method handle_event(e)
      (\self.vsb).handle_event(e)
      (\self.hsb).handle_event(e)

      if e === (&lpress | &rpress | &mpress) then
         handle_press(e)
      else if e === (&ldrag | &rdrag | &mdrag) then
         handle_drag(e)
      else if e === (&lrelease | &rrelease | &mrelease) then
         handle_release(e)
      else if \self.has_focus then {
         self.__m[\ (kmap.handle_key(e))](self,e)
      }
   end

   #
   # Set cursor from the current &x, &y
   #
   # @p
   method set_cursor_from_pos()
      local l, nlines, s, i, v
      l := (&y - self.view.y) / self.line_height
      nlines := self.get_curr_lines()
      if nlines = 0 then # there are no lines of text, hence no cursor
         fail
      l <:= 0
      l >:= nlines - 1

      v := self.view_list[l + self.get_first_line()]
      self.cursor_y := v.index

      s := v.str
      i := 1
      l := self.get_left_pos()
      while (i < *s) & (TextWidthEx(cwin, s, 1, i + 1, tab_width) < &x - l) do
         i+:= 1

      self.cursor_x := v.first + i - 1
   end

   method clear_mark()
      mark_x := mark_y := &null
   end

   method has_region()
      return \mark_x & (mark_x ~= cursor_x | mark_y ~= cursor_y)
   end

   method handle_press(e)
      start_handle(e)
      if ((x <= &x < x + view.w + 2 * DEFAULT_TEXT_X_SURROUND) &
          (y  <= &y < y + view.h + 2 * DEFAULT_TEXT_Y_SURROUND)) then {
         #
         # Button down in region - move to cursor position.
         #
         set_cursor_from_pos()
         self.mark_y := self.cursor_y
         self.mark_x := self.cursor_x
         self.is_held := 1
      }
      end_handle(e)
   end

   method handle_undo(e)
      start_handle(e)
      if undo_manager.can_undo() then {
         undo_manager.undo()
         changed := 1
      }
      end_handle(e)
   end

   method handle_redo(e)
      start_handle(e)
      if undo_manager.can_redo() then {
         undo_manager.redo()
         changed := 1
      }
      end_handle(e)
   end

   #
   # String search. Used to be called handle_files() in ide.icn
   #
   method handle_stringsearch()
      local oldwindow, first_x, second_x
      /findstr := ""
      dialogFor := "find"
      oldwindow := &window
      &window := parent_dialog.win
#      cursor_x := cursor_y := 1
      if TextDialog("String to seek: ",,findstr,1)=="Okay" then {
         parent_dialog.resize()
         if findstr ~===:= dialog_value[1] then
             findstring(findstr, 1, 1)
         else
             findstring(findstr, , cursor_x+1)
         }
      first_x := (cursor_x-*dialog_value[1])
      second_x := cursor_x
      # no event
      handle_select_str(&null, first_x, second_x, cursor_y)
      &window := oldwindow
   end

   #
   #
   #
   method findstring(s, startline, startcol)
      local L, i, j
      L := get_contents()
      /startline := cursor_y
      /startcol := cursor_x
      i := startline
      foundline := &null
      while L[i] do {
         if j := find(s, L[i], startcol) then {
            foundline := i
            foundcol := j
            goto_line(i,1)
            cursor_x := j + *s
            constrain_line()
            refresh(1)
            return
            }
         i +:= 1
         startcol := 1
         }
   end

   method goto_line(ln,cn)
      if 1 <= ln <= *contents then {
         cursor_y := ln
         cursor_x := cn
         constrain_line()
         refresh(1)
         return
         }
   end

   #
   # handle_select_str - having found a string, select it.
   #
   method handle_select_str(e, first_x, second_x, y_position)
      start_handle(e)
      mark_x := first_x
      mark_y := cursor_y := y_position
      cursor_x := second_x
      end_handle(e)
   end

   method handle_find_next()
      if \findstr then {
         findstring(findstr, cursor_y, cursor_x+1)
         handle_select_str(&null, (cursor_x-*findstr), cursor_x, cursor_y)
         }
   end

   method handle_drag(e)
      start_handle(e)
      if \self.is_held then {
         if &y < self.y then
            direction := "up"
         else if &y >= self.y + self.view.h + 2 * DEFAULT_TEXT_Y_SURROUND then
            direction := "down"
         else if &x < self.x then
            direction := "left"
         else if &x >= self.x + self.view.w + 2 * DEFAULT_TEXT_X_SURROUND then
            direction := "right"
         else
            direction := &null

         if /direction then {
            stop_ticker()
            set_cursor_from_pos()
            }
         else {
            is_ticking() | set_ticker(30)
         }
      }
      end_handle(e)
   end

   method tick()
      local l, s, v, vi, e
      start_handle(e)
      vi := get_view_list_index()

      case self.direction of {
         "up" : {
            if vi > 1 then {
               v := view_list[vi - 1]
               self.cursor_y := v.index
               self.cursor_x := v.first
               }
            }
         "down" : {
            if vi < *view_list then {
               v := view_list[vi + 1]
               self.cursor_y := v.index
               self.cursor_x := v.first
               }
            }
         "left" : {
            v := view_list[vi]
            l := self.get_left_pos()
            s := v.str
            while (self.cursor_x - v.first + 1 > 1) &
               TextWidthEx(cwin, s, 1, cursor_x - v.first + 1, tab_width) >=
                view.x - l do {
               cursor_x -:= 1
               }
            }
         "right" : {
            v := view_list[vi]
            l := self.get_left_pos()
            s := v.str
            while (self.cursor_x - v.first + 1 < *s) &
               TextWidthEx(cwin, s, 1, cursor_x - v.first + 1, tab_width) <
                view.x - l + view.w do {
               cursor_x +:= 1
               }
            }
         }
      end_handle(e)
   end

   method handle_release(e)
      start_handle(e)
      if \is_held then {
         #
         # Mouse released after being held down.  Clear flag.  If there
         # is no region (mouse released where it was pressed), then clear
         # the mark.  This prevents selecting when using the scrollbars
         # after release.
         #
         is_held := &null
         has_region() | clear_mark()
         stop_ticker()
      }
      end_handle(e)
   end

   method keyboard_mark()
      if &shift then {
         /mark_x := cursor_x
         /mark_y := cursor_y
         }
      else
         clear_mark()
   end

   method handle_start_of_line(e)
      start_handle(e)
      keyboard_mark()
      cursor_x := 1
      end_handle(e)
   end

   method handle_end_of_line(e)
      start_handle(e)
      keyboard_mark()
      cursor_x := *contents[cursor_y] + 1
      end_handle(e)
   end

   method handle_key_up(e)
      start_handle(e)
      keyboard_mark()
      self.set_cursor_y(0 < self.cursor_y - 1)
      end_handle(e)
   end

   method handle_key_home(e)
      start_handle(e)
      keyboard_mark()
      cursor_y := cursor_x := 1
      end_handle(e)
   end

   method handle_key_end(e)
      start_handle(e)
      keyboard_mark()
      cursor_y := *contents
      cursor_x := *contents[cursor_y] + 1
      end_handle(e)
   end

   method handle_select_all(e)
      start_handle(e)
      mark_x := mark_y := 1
      cursor_y := *contents
      cursor_x := *contents[cursor_y] + 1
      end_handle(e)
   end

   method handle_key_down(e)
      start_handle(e)
      keyboard_mark()
      self.set_cursor_y(*self.contents >= self.cursor_y + 1)
      end_handle(e)
   end

   method handle_key_left(e)
      start_handle(e)
      keyboard_mark()
      if self.cursor_x = 1 then {
         if self.cursor_y > 1 then {
            self.cursor_y -:= 1
            self.cursor_x := *self.contents[self.cursor_y] + 1
            }
         }
      else {
         self.cursor_x -:= 1
         }
      end_handle(e)
   end

   method handle_key_right(e)
      start_handle(e)
      keyboard_mark()
      if self.cursor_x = *self.contents[self.cursor_y] + 1 then {
         if self.cursor_y < *self.contents then {
            self.cursor_x := 1
            self.cursor_y +:= 1
            }
         }
      else {
         self.cursor_x +:= 1
         }
      end_handle(e)
   end

   method handle_key_page_up(e)
      local i
      start_handle(e)
      keyboard_mark()
      if i := (\self.vsb).get_value() then {
         self.vsb.set_value(i - self.vsb.page_size)
         self.constrain_cursor()
         self.refresh()
      }
      end_handle(e)
   end

   method handle_key_page_down(e)
      local i
      start_handle(e)
      keyboard_mark()
      if i := (\self.vsb).get_value() then {
         self.vsb.set_value(i + self.vsb.page_size)
         self.constrain_cursor()
         self.refresh()
      }
      end_handle(e)
   end

   method handle_delete_line(e)
      local ed
      start_handle(e)
      if (cursor_y < *self.contents) | (*self.contents[cursor_y] > 0) then {
         ed := EditableTextListDeleteLineEdit(self)
         undo_manager.add_edit(ed)
         ed.redo()
         changed := 1
      }
      end_handle(e)
   end

   method get_region()
      local r

      r := ""
      if self.mark_y < self.cursor_y then {
         r := self.contents[self.mark_y][self.mark_x:0] || "\n"
         every r ||:= self.contents[self.mark_y + 1 to self.cursor_y - 1] || "\n"
         r ||:= self.contents[self.cursor_y][1:self.cursor_x]
      } else if self.mark_y > self.cursor_y then {
         r := self.contents[self.cursor_y][self.cursor_x:0] || "\n"
         every r ||:= self.contents[self.cursor_y + 1 to self.mark_y - 1] || "\n"
         r ||:= self.contents[self.mark_y][1:self.mark_x]
      } else {
         # mark_y = cursor_y
         if self.mark_x < self.cursor_x then {
            r := self.contents[self.cursor_y][self.mark_x:self.cursor_x]
         } else {
            r := self.contents[self.cursor_y][self.cursor_x:self.mark_x]
         }
      }
      return r
   end

   method delete_region(e)
      local ed
      ed := EditableTextListDeleteRegionEdit(self)
      undo_manager.add_edit(ed)
      ed.redo()
      changed := 1
   end

   method handle_delete_left(e)
      local ed

      start_handle(e)
      if has_region() then {
         delete_region(e)
      } else {
         if (self.cursor_x > 1) | (self.cursor_y > 1) then {
            ed := EditableTextListDeleteLeftEdit(self)
            undo_manager.add_edit(ed)
            ed.redo()
            changed := 1
         }
      }
      end_handle(e)
   end

   method handle_delete_right(e)
      local ed

      start_handle(e)
      if has_region() then {
         delete_region(e)
      } else {
         if (self.cursor_x <= *contents[cursor_y]) | (self.cursor_y < *contents) then {
            ed := EditableTextListDeleteRightEdit(self)
            undo_manager.add_edit(ed)
            ed.redo()
            changed := 1
         }
      }
      end_handle(e)
   end

   method handle_return(e)
      local ed
      start_handle(e)
      ed := EditableTextListReturnEdit(self)
      undo_manager.add_edit(ed)
      ed.redo()
      changed := 1
      end_handle(e)
   end

   method handle_default(e)
      local ce, ed

      start_handle(e)
      #
      # Add any printable character at cursor position
      #
      if type(e) == "string" & not(&control | &meta) & any(printable, e) then {
         if has_region() then {
            ed := EditableTextListDeleteRegionEdit(self)
            ed.redo()
            undo_manager.add_edit(ed)
            ed := EditableTextListDefaultEdit(self, e)
            ed.redo()
            undo_manager.add_edit(ed)
         } else {
            ed := EditableTextListDefaultEdit(self, e)
            ed.redo()
            undo_manager.add_edit(ed)
         }
         changed := 1
      }
      end_handle(e)
   end

   method resize()
      if \wrap_mode == "width" then
         clear_view_list()

      self.ScrollArea.resize()
      self.constrain_line()
   end

   method set_internal_fields()
      local had_vsb

      had_vsb := self.vsb

      if /view_list then
         create_view_list()

      self.ScrollArea.set_internal_fields()

      if \wrap_mode == "width" & had_vsb ~=== self.vsb then {
         create_view_list()
         set_internal_fields()
      }
   end

   method draw(subject_x, subject_y, vx, vy, vw, vh)
      local rev, first_line, last_line, xp, yp, i, dwin

      #
      # Which lines to draw
      #
      first_line := get_first_line()
      last_line := get_last_line()
      last_line >:= get_line_count()

      #
      # Where to draw them
      #
      yp := vy + self.line_height / 2

      #
      # Left offset
      #
      xp := vx - subject_x


      if /self.transparent then
         dwin:=self.cbwin
      else
         dwin:=self.cwin

      #
      # Write the lines
      #
      every i := first_line to last_line do {
         draw_line(xp, yp, i, dwin)
         yp +:= self.line_height
      }
      return
   end

   method draw_line(xp, yp, i, dwin)
      local si, s, off, off1, off2, v, r, cw, ds, r1, do_r1, fc

      v := self.view_list[i]
      s := v.str
      ds := detab(s, self.tab_width + 1)
      si := self.cursor_x - v.first + 1

      if (v.index = \self.cursor_y) & (v.first <= \self.cursor_x <= v.last) then {
         if \self.has_focus then {
#           if s[si] ~== "\t" then
#              left_string(cw, xp + off, yp, s[si])
            CursorLine(dwin,
                      xp + TextWidthEx(dwin, s, 1, si, self.tab_width),
                      1 + yp - self.line_height / 2,
                      CharWidth(dwin, s[si]),
                      self.line_height)
         }
      }

      if \self.mark_y then {
         if (self.mark_y < v.index < self.cursor_y) | (self.mark_y > v.index > self.cursor_y) then {
            # Whole line selected
            r := [v.first, v.last]
         } else if v.index = self.mark_y = self.cursor_y then {
            # Part of line
            if self.mark_x < self.cursor_x then {
               r := range(v.first, v.last, self.mark_x, self.cursor_x)
            }
            else if self.mark_x > self.cursor_x then {
               r := range(v.first, v.last, self.cursor_x + 1 , self.mark_x)
               do_r1 := 1
            }
         } else if v.index = self.mark_y then {
            if self.mark_y < self.cursor_y then {
               r := range(v.first, v.last, self.mark_x,
                          *self.contents[v.index] + 1)
            } else {
               r := range(v.first, v.last, 1, self.mark_x)
            }
         } else if v.index = self.cursor_y then {
            if self.mark_y > self.cursor_y then {
               r := range(v.first, v.last, self.cursor_x + 1,
                          *self.contents[v.index] + 1)
               do_r1 := 1
            } else {
               r := range(v.first, v.last, 1, self.cursor_x)
            }
         }

         if \r then {
            if /do_r1 then r1:=r[1]
            else r1 := 1<=r[1]-1 | 1

            off1 := TextWidthEx(dwin, s, 1, r1 - v.first + 1,
                                self.tab_width)
            off2 := TextWidthEx(dwin, s, 1, r[2] - v.first + 1,
                                self.tab_width)

            fc := Fg(dwin)
            Fg(dwin, "blue")

            FillRectangle(dwin, xp + off1, 1 + yp - self.line_height / 2,
                          off2 - off1, self.line_height)

            Fg(dwin, "white")
            left_string(dwin, xp+off1, yp, ds[r1:r[2]])

            Fg(dwin, fc)
            left_string(dwin, xp, yp, ds[1:r1])
            left_string(dwin, xp+off2, yp, ds[r[2]:0])
           }
          else
            left_string(dwin, xp, yp, ds)
        }
       else
          left_string(dwin, xp, yp, ds)

   end

   #
   # Return the intersection of the range f1-t1 and f2-t2, or
   # fail if the ranges don't intersect.  Example:
   # range(2,20,10,30) = [10,20]
   # @p
   method range(f1, t1, f2, t2)
      if f1 < f2 then {
         if t1 >= f2 then {
            if t1 >= t2 then {
               return [f2, t2]
            }
            else {
               return [f2, t1]
            }
         }
      } else {
         if f1 <= t2 then {
            if t1 >= t2 then {
               return [f1, t2]
            }
            else {
               return [f1, t1]
            }
         }
      }
   end

   method lost_focus(e)
      clear_mark()
      self.Component.lost_focus(e)
   end

   method get_line_height()
      return WAttrib(self.cwin, "fheight")
   end

   method keeps(e)
      # This component keeps all events.
      return
   end

   method get_subject_width()
      return self.long_line.tw
   end

   #
   # Return the index into view_list containing the
   # content position x, y.  Binary search is used.
   # @p
   method get_view_list_index(x, y)
      local i, j, mid, v

      /x := self.cursor_x
      /y := self.cursor_y

      i := 1
      j := *view_list

      while i <= j do {
         mid := (i+j) / 2
         v := view_list[mid]
         if (v.index = y) & (v.first <= x <= v.last) then
            return mid
         if (v.index < y) | (v.index = y & v.last < x) then
            i := mid + 1
         else
            j := mid - 1
      }
   end

   #
   # Create the view_list from the contents.
   # @p
   method create_view_list()
      local s, i, pos, j, p

      view_list := []
      every i := 1 to *self.contents do {
         pos := 1
         s := self.contents[i] || " "
         every j := line_splitter.split(s) do {
            p := s[pos:j]
            put(view_list, ViewLine(i, p, pos, j - 1,
                                    TextWidthEx(cwin, p,,, tab_width)))
            pos := j
         }
      }

      find_long_line()
   end

   #
   # Clear the view list, forcing it to be re-created next
   # time it's needed.
   # @p
   method clear_view_list()
      view_list := long_line := &null
   end

   #
   # Run through the view list looking for the longest line.
   # @p
   method find_long_line()
      local v
      long_line := view_list[1]
      every v := !view_list do {
         if v.tw > long_line.tw then
            long_line := v
      }
   end

   #
   # Update the view list, given that the contents have changed.
   # This takes care to minimize the work that has to be done.  The
   # first param is an index into the contents list.  The second param
   # indicates some view_list lines corresponding to those beginning
   # with index should be removed; the third param says that some should
   # be added.
   #
   method update_view_list(index, del, add)
      local vi, i, pos, j, v, p, s

      vi := get_view_list_index(1, index)

      while view_list[vi].index <= index + del - 1 do {
         if view_list[vi] === long_line then
            long_line := &null
         delete(view_list, vi)
      }

      if /long_line then
         find_long_line()

      every i := index to index + add - 1 do {
         pos := 1
         s := self.contents[i] || " "
         every j := line_splitter.split(s) do {
            p := s[pos:j]
            v := ViewLine(i, p, pos, j - 1, TextWidthEx(cwin, p,,, tab_width))

            if vi > *view_list then
               put(view_list, v)
            else
               insert(view_list, vi, v)

            if /long_line | (v.tw > long_line.tw) then
               long_line := v
            pos := j
            vi +:= 1
         }
      }

      #
      # Adjust index values
      #
      if del ~= add then
         every view_list[vi to *view_list].index +:= (add - del)
   end

   #
   # Set the word-wrap mode, which should be one of the following
   # values :-
   #
   # &null, "false", "off", "no", meaning don't do word-wrap (the default).
   #
   # an integer (eg 20), meaning wrap at that line length.
   #
   # "width", meaning wrap at the width of the component.
   #
   method set_wrap_mode(s)
      local wc
      self.wrap_mode := s
      if /s | s == ("" | "false" | "off" | "no") then
         line_splitter := DefaultLineSplitter(self)
      else if wc := integer(s) then
         line_splitter := ColumnLineSplitter(self, wc)
      else if s == "width" then
         line_splitter := WidthLineSplitter(self)
      else
         field_error("Bad wrap mode:" || s)

      clear_view_list()
      if is_dialog_open() then {
         compute_and_invalidate()
         constrain_line()
      }
   end

   #
   # Set the tab width to use.  Default is eight chars.
   #
   method set_tab_width(n)
      self.tab_width := n
      clear_view_list()
      if is_dialog_open() then {
         compute_and_invalidate()
         constrain_line()
      }
   end

   method set_one(attr, val)
      case attr of {
         "wrap_mode": set_wrap_mode(string_val(attr, val))
         "tab_width": set_tab_width(int_val(attr, val))
         "contents" : set_contents(val)
         default: self.LineBasedScrollArea.set_one(attr, val)
      }
   end

initially(a[])
   self.LineBasedScrollArea.initially()
   self.set_accepts_focus()
   undo_manager := UndoManager()
   printable := cset(&cset[33:0]) ++ '\t\n'
   tab_width := 8
   set_wrap_mode("off")
   cursor_x := cursor_y := 1
   set_fields(a)
   kmap := Keybindings()
end

class ViewLine:Object(index,
                      str,
                      first,
                      last,
                      tw)
   method to_string()
      return "ViewLine(" || index || "," || str || "," || first || "," || last || ")"
   end
end

class EditableTextListEdit:UndoableEdit(parent,
                                        cursor_x,
                                        cursor_y,
                                        mark_x,
                                        mark_y
                                        )
   method redo()
      restore()
      self.redo_impl()
   end

   method undo()
      self.undo_impl()
      restore()
   end

   abstract method redo_impl()
   abstract method undo_impl()

   method save()
      self.cursor_x := parent.cursor_x
      self.cursor_y := parent.cursor_y
      self.mark_x := parent.mark_x
      self.mark_y := parent.mark_y
   end

   method restore()
      parent.cursor_x := self.cursor_x
      parent.cursor_y := self.cursor_y
      parent.mark_x := self.mark_x
      parent.mark_y := self.mark_y
   end

   initially(parent)
      self.parent := parent
      save()
end

class EditableTextListDefaultEdit:EditableTextListEdit(s)
   method add_edit(other)
      if is_instance(other, "gui::EditableTextListDefaultEdit") &
         (other.cursor_y = self.cursor_y) &
         (other.cursor_x = self.cursor_x + *s) then {
            s ||:= other.s
            return
      }
   end

   method redo_impl()
      if parent.cursor_x = 1 then
         parent.contents[parent.cursor_y] := s || parent.contents[parent.cursor_y]
      else
         parent.contents[parent.cursor_y][parent.cursor_x - 1] ||:= s
      parent.cursor_x +:= *s
      parent.clear_mark()
      parent.update_view_list(parent.cursor_y, 1, 1)
   end

   method undo_impl()
      parent.contents[self.cursor_y][self.cursor_x +: *s] := ""
      parent.update_view_list(self.cursor_y, 1, 1)
   end

   initially(parent, e)
      self.EditableTextListEdit.initially(parent)
      self.s := e
end

class EditableTextListReturnEdit:EditableTextListEdit()
   method redo_impl()
      local s
      s := parent.contents[parent.cursor_y]
      parent.contents[parent.cursor_y] := s[1:parent.cursor_x]
      parent.contents := parent.contents[1:parent.cursor_y + 1] ||| [s[parent.cursor_x:0]] ||| parent.contents[parent.cursor_y + 1:0]
      parent.update_view_list(parent.cursor_y, 1, 2)

      if (\ (parent.mark_y) > parent.cursor_y) |
         (\ (parent.mark_y) = parent.cursor_y &
          \ (parent.mark_x) >= parent.cursor_x) then {
             # Mark was after insertion. Push the mark forward.
             parent.mark_y +:= 1
             parent.mark_x -:= (parent.cursor_x-1)
             }

      parent.cursor_y +:= 1
      parent.cursor_x := 1
   end

   method undo_impl()
      parent.contents[self.cursor_y] ||:= parent.contents[self.cursor_y + 1]
      delete(parent.contents, self.cursor_y + 1)
      parent.update_view_list(self.cursor_y, 2, 1)
   end

   initially(parent)
      self.EditableTextListEdit.initially(parent)
end

class EditableTextListDeleteRightEdit:EditableTextListEdit(ch)
   method redo_impl()
      if parent.cursor_x = *parent.contents[parent.cursor_y] + 1 then {
         # We know cursor_y < *contents from the handle method above.
         parent.contents[parent.cursor_y] ||:= parent.contents[parent.cursor_y + 1]
         parent.contents := parent.contents[1:parent.cursor_y + 1] ||| parent.contents[parent.cursor_y + 2 : 0]
         parent.update_view_list(parent.cursor_y, 2, 1)
      } else {
         # Cursor not at end of line
         ch := parent.contents[parent.cursor_y][parent.cursor_x]
         parent.contents[parent.cursor_y][parent.cursor_x] := ""
         parent.update_view_list(parent.cursor_y, 1, 1)
      }
      parent.clear_mark()
   end

   method undo_impl()
      local t
      if /ch then {
         t := parent.contents[self.cursor_y][self.cursor_x:0]
         parent.contents[self.cursor_y][self.cursor_x:0] := ""
         insert(parent.contents, self.cursor_y + 1, t)
         parent.update_view_list(self.cursor_y, 1, 2)
      } else {
         if self.cursor_x >  *parent.contents[self.cursor_y] then
            parent.contents[self.cursor_y] ||:= ch
         else
            parent.contents[self.cursor_y][self.cursor_x] := ch || parent.contents[self.cursor_y][self.cursor_x]
         parent.update_view_list(self.cursor_y, 1, 1)
      }
   end

   initially(parent)
      self.EditableTextListEdit.initially(parent)
end

class EditableTextListDeleteLeftEdit:EditableTextListEdit(ch, cut)
   method redo_impl()
      if parent.cursor_x = 1 then {
         # We know parent.cursor_y > 1 from the handle method
         cut := parent.cursor_x := *parent.contents[parent.cursor_y - 1] + 1
         parent.contents[parent.cursor_y - 1] ||:= parent.contents[parent.cursor_y]
         parent.contents := parent.contents[1:parent.cursor_y] ||| parent.contents[parent.cursor_y + 1 : 0]
         parent.update_view_list(parent.cursor_y - 1, 2, 1)
         parent.cursor_y -:= 1
      } else {
         # parent.cursor_x > 1
         ch := parent.contents[parent.cursor_y][parent.cursor_x - 1]
         parent.contents[parent.cursor_y][parent.cursor_x - 1] := ""
         parent.cursor_x -:= 1
         parent.update_view_list(parent.cursor_y, 1, 1)
      }
      parent.clear_mark()
   end

   method undo_impl()
      local t
      if /ch then {
         t :=  parent.contents[self.cursor_y - 1][cut:0]
         parent.contents[self.cursor_y - 1][cut:0] := ""
         insert(parent.contents, self.cursor_y, t)
         parent.update_view_list(self.cursor_y - 1, 1, 2)
      } else {
         if self.cursor_x - 1 > *parent.contents[self.cursor_y] then
            parent.contents[self.cursor_y] ||:= ch
         else
            parent.contents[self.cursor_y][self.cursor_x - 1] := ch || parent.contents[self.cursor_y][self.cursor_x - 1]
         parent.update_view_list(self.cursor_y, 1, 1)
      }
   end

   initially(parent)
      self.EditableTextListEdit.initially(parent)
end

class EditableTextListDeleteLineEdit:EditableTextListEdit(s)
   method redo_impl()
      s := parent.contents[parent.cursor_y]
      if parent.cursor_y = *parent.contents then {
         parent.contents[parent.cursor_y] := ""
         parent.update_view_list(parent.cursor_y, 1, 1)
      } else {
         delete(parent.contents, parent.cursor_y)
         parent.update_view_list(parent.cursor_y, 1, 0)
      }
      parent.cursor_x := 1
      parent.clear_mark()
   end

   method undo_impl()
      if self.cursor_y = *parent.contents then {
         parent.contents[self.cursor_y] := s
         parent.update_view_list(self.cursor_y, 1, 1)
      } else {
         insert(parent.contents, self.cursor_y, s)
         parent.update_view_list(self.cursor_y, 0, 1)
      }
   end

   initially(parent)
      self.EditableTextListEdit.initially(parent)
end

class EditableTextListDeleteRegionEdit:EditableTextListEdit(l, pos)
   method redo_impl()
      l := []
      if parent.mark_y < parent.cursor_y then {
         pos := parent.mark_y
         put(l, parent.contents[parent.mark_y])
         parent.contents[parent.mark_y] := parent.contents[parent.mark_y][1:parent.mark_x] || parent.contents[parent.cursor_y][parent.cursor_x:0]
         every parent.mark_y + 1 to parent.cursor_y do {
            put(l, parent.contents[parent.mark_y + 1])
            delete(parent.contents, parent.mark_y + 1)
         }
         parent.cursor_x := parent.mark_x
         parent.cursor_y := parent.mark_y
      } else if parent.mark_y > parent.cursor_y then {
         pos := parent.cursor_y
         put(l,  parent.contents[parent.cursor_y])
         parent.contents[parent.cursor_y] := parent.contents[parent.cursor_y][1:parent.cursor_x] || parent.contents[parent.mark_y][parent.mark_x:0]
         every parent.cursor_y + 1 to parent.mark_y do {
            put(l, parent.contents[parent.cursor_y + 1])
            delete(parent.contents, parent.cursor_y + 1)
         }
      } else {
         # parent.mark_y = cursor_y
         pos := parent.cursor_y
         put(l, parent.contents[parent.cursor_y])
         if parent.mark_x < parent.cursor_x then {
            parent.contents[parent.cursor_y][parent.mark_x:parent.cursor_x] := ""
            parent.cursor_x := parent.mark_x
         } else {
            parent.contents[parent.cursor_y][parent.cursor_x:parent.mark_x] := ""
         }
      }
      parent.update_view_list(pos, *l, 1)
      parent.clear_mark()
   end

   method undo_impl()
      local n
      n := *l
      delete(parent.contents, pos)
      if pos > *parent.contents then
         while put(parent.contents, pop(l))
      else
         while insert(parent.contents, pos, pull(l))
      parent.update_view_list(pos, 1, n)
   end

   initially(parent)
      self.EditableTextListEdit.initially(parent)
end

class EditableTextListPasteEdit:EditableTextListEdit(s, pre, n)
   method redo_impl()
      local t, nl

      n := 0
      pre := parent.contents[parent.cursor_y]

      s ? repeat {
         t := tab(upto('\n') | 0)
         if any('\n') then {
            nl := parent.contents[parent.cursor_y][parent.cursor_x:0]
            parent.contents[parent.cursor_y] := parent.contents[parent.cursor_y][1:parent.cursor_x] || t
            if parent.cursor_y = *parent.contents then
               put(parent.contents, nl)
            else
               insert(parent.contents, parent.cursor_y + 1, nl)
            n +:= 1
            parent.cursor_y +:= 1
            parent.cursor_x := 1
            move(1)
            }
         else {
            parent.contents[parent.cursor_y] := parent.contents[parent.cursor_y][1:parent.cursor_x] || t || parent.contents[parent.cursor_y][parent.cursor_x:0]
            parent.cursor_x +:= *t
            break
         }
      }
      parent.update_view_list(self.cursor_y, 1, n + 1)
   end

   method undo_impl()
      parent.contents[self.cursor_y] := pre
      every 1 to n do
         delete(parent.contents, self.cursor_y + 1)
      parent.update_view_list(self.cursor_y, n + 1, 1)
   end

   initially(parent, s)
      self.EditableTextListEdit.initially(parent)
      self.s := s
end

class LineSplitter(parent)
   abstract method split(s)

   initially(parent)
      self.parent := parent
end

class ColumnLineSplitter:LineSplitter(wrap_col)
   method split(s)
      local ch, ds, p

      ds := ""
      s ? {
         while ch := move(1) do {
            ds ||:= ch
            if *detab(ds, parent.tab_width + 1) > wrap_col then {
               if *ds > 1 then {
                  ds[-1] := ""
                  move(-1)
               }
               if upto('\t ', ds) then {
                  while any(~'\t ', ds[-1]) do {
                     ds[-1] := ""
                     move(-1)
                  }
               }
               p := &pos
               suspend p
               ds := ""
            }

         }
         if p ~=== &pos then
            suspend .&pos
      }
   end

   initially(parent, wrap_col)
      self.LineSplitter.initially(parent)
      self.wrap_col := wrap_col
end

class DefaultLineSplitter()
   method split(s)
      return *s + 1
   end
end

class WidthLineSplitter:LineSplitter()
   method split(s)
      local ch, ds, p, width

      if \parent.draw_border_flag then {
         width := parent.w - 2 * parent.get_view_x_padding()
      } else {
         width := parent.w
      }
      if \parent.vsb then
         width -:= SB_SIZE

      ds := ""
      s ? {
         while ch := move(1) do {
            ds ||:= ch
            if TextWidthEx(parent.cwin, ds,,, parent.tab_width) > width then {
               if *ds > 1 then {
                  ds[-1] := ""
                  move(-1)
               }
               if upto('\t ', ds) then {
                  while any(~'\t ', ds[-1]) do {
                     ds[-1] := ""
                     move(-1)
                  }
               }
               p := &pos
               suspend p
               ds := ""
            }

         }
         if p ~=== &pos then
            suspend .&pos
      }
   end

   initially(parent)
      self.LineSplitter.initially(parent)
end
