#
# $Id: editabletextlist.icn,v 1.14 2004/11/11 20:00:24 rparlett 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"

#
# 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_contents_size,
   old_mw,
   old_cursor_x,
   old_cursor_y,
   old_has_region,
   changed
   )

   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.contents
   end

   method get_contents()
      return self.contents
   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_mark()
      if \ (\self.parent_dialog).is_open then {
         long_line := &null
         self.cursor_x := self.cursor_y := 1
         compute_and_invalidate()
      }
      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(self.cwin, self.contents[cursor_y], 1, self.cursor_x)
      dest := self.contents[n]
      i := 1
      while (i <= *dest) & (TextWidthEx(self.cwin, dest, 1, i) < 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()
      if self.cursor_y < self.get_first_line() then
         self.set_cursor_y(self.get_first_line())
      else if self.cursor_y >= self.get_first_line() + self.get_max_lines() then
         self.set_cursor_y(self.get_first_line() + self.get_max_lines() - 1)

      s := self.contents[self.cursor_y] || " "
      i := TextWidthEx(self.cwin, s, 1, self.cursor_x)
      j := i + TextWidthEx(self.cwin, s, self.cursor_x)
      l := self.get_left_pos()
      if self.view.x - l > i then {
         while (self.cursor_x < *s) & (TextWidthEx(self.cwin, s, 1, self.cursor_x) < self.view.x - l) do
            self.cursor_x +:= 1
      } else if self.view.x - l + self.view.w < j then {
         while (self.cursor_x > 1) & TextWidthEx(self.cwin, s, 1, self.cursor_x + 1) > self.view.x - l + self.view.w do
            self.cursor_x -:= 1
      }
   end

   #
   # Move the text area displayed so that the cursor is on the screen.
   #
   # @p
   method constrain_line()
      if self.cursor_y < self.get_first_line() then
         self.vsb.set_value(self.line_height * (self.cursor_y - 1))
      else if self.cursor_y > self.get_last_line() then
         self.vsb.set_value(self.line_height * (self.cursor_y - self.get_max_lines()))

      s := self.contents[self.cursor_y] || " "
      i := TextWidthEx(self.cwin, s, 1, self.cursor_x)
      j := i + TextWidthEx(self.cwin, s, self.cursor_x)
      l := self.get_left_pos()
      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)

         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_contents_size := *contents
      old_mw := TextWidthEx(cwin, contents[long_line])
      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 (*contents ~= old_contents_size) | /long_line | (old_mw ~= TextWidthEx(cwin, contents[long_line])) 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 {
         case e of {
            Key_Home : handle_key_home(e)
            Key_End : handle_key_end(e)
            Key_PgUp : handle_key_page_up(e)
            Key_PgDn : handle_key_page_down(e)
            Key_Up : handle_key_up(e)
            Key_Down : handle_key_down(e)
            Key_Left : handle_key_left(e)
            Key_Right : handle_key_right(e)
            "\b" : handle_delete_left(e)
            "\r" | "\l": handle_return(e)
            "\^k" : handle_delete_line(e)
            "\^a" : handle_select_all(e)
            "\^e" : handle_end_of_line(e)
            "\d" | "\^d" : handle_delete_right(e)
            "\^x" :  handle_cut(e)
            "\^c" :  handle_copy(e)
            "\^v" :  handle_paste(e)
            "\^z" :  handle_undo(e)
            "\^y" :  handle_redo(e)
            default : handle_default(e)
         }
      }
   end

   #
   # Set cursor from the current &x, &y
   #
   # @p
   method set_cursor_from_pos()
      local l, nlines, s, i
      l := (&y - self.view.y) / self.line_height
      nlines := self.get_curr_lines()
      l <:= 0
      l >:= nlines - 1
      self.cursor_y := l + self.get_first_line()

      s := self.contents[self.cursor_y] || " "
      i := 1
      l := self.get_left_pos()
      while (i < *s) & (TextWidthEx(self.cwin, s, 1, i + 1) < &x - l) do
         i+:= 1

      self.cursor_x := i
   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 ((self.x <= &x < self.x + self.view.w + 2 * DEFAULT_TEXT_X_SURROUND) & 
          (self.y  <= &y < self.y + self.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

   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

      start_handle(e)

      case self.direction of {
         "up" : {
            l := self.get_first_line() - 1
            if l > 0 then {
               self.set_cursor_y(l) 
            }
         }
         "down" : {
            l := self.get_last_line() + 1
            if l <= *self.contents then {
               self.set_cursor_y(l) 
            }
         }
         "left" : {
            l := self.get_left_pos()
            s := self.contents[self.cursor_y] || " "
            while (self.cursor_x > 1) & TextWidthEx(self.cwin, s, 1, self.cursor_x) >= self.view.x - l do {
               self.cursor_x -:= 1
            }
         }
         "right" : {
            l := self.get_left_pos()
            s := self.contents[self.cursor_y] || " "
            while (self.cursor_x < *s) & TextWidthEx(self.cwin, s, 1, self.cursor_x) < self.view.x - l + self.view.w do {
               self.cursor_x +:= 1
            }
         }
      }
      end_handle(e)
   end

   method handle_release(e)
      start_handle(e)
      if \self.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.
         #
         self.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)
      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)
      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 {
            ce := CompoundEdit()
            ed := EditableTextListDeleteRegionEdit(self)
            ed.redo()
            ce.add_edit(ed)
            ed := EditableTextListDefaultEdit(self, e)
            ed.redo()
            ce.add_edit(ed)
            undo_manager.add_edit(ce)
         } else {
            ed := EditableTextListDefaultEdit(self, e)
            ed.redo()
            undo_manager.add_edit(ed)
         }
         changed := 1
      }
      end_handle(e)
   end

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

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

      #
      # 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

      rev := Clone(self.cbwin, "drawop=reverse")

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

      Uncouple(rev)
      rev := &null
      return
   end

   method draw_line(xp, yp, i, rev)
      local s, off1, off2

      s := self.contents[i]

      left_string(self.cbwin, xp, yp, detab(s))

      if i = \self.cursor_y then {
         s ||:= " "
         if \self.has_focus then {
            cw := Clone(self.cbwin, "bg=red", "fg=white")
            off := TextWidthEx(self.cbwin, s, 1, self.cursor_x)
            EraseRectangle(cw, xp + off, 1 + yp - self.line_height / 2, CharWidth(self.cbwin, s[self.cursor_x]), self.line_height)
            if s[self.cursor_x] ~== "\t" then
               left_string(cw, xp + off, yp, s[self.cursor_x])
            Uncouple(cw)
         } else {
            cw := Clone(self.cbwin, "fg=red")
            Rectangle(cw, xp + TextWidthEx(self.cbwin, s, 1, self.cursor_x), 1 + yp - self.line_height / 2, CharWidth(self.cbwin, s[self.cursor_x]), self.line_height)
            Uncouple(cw)
         }
      }

      if \self.mark_y then {
         if (self.mark_y < i < self.cursor_y) | (self.mark_y > i > self.cursor_y) then {
            # Whole line selected
            off1 := 0
            off2 := TextWidthEx(self.cbwin, s)
         } else if i = self.mark_y = self.cursor_y then {
            # Part of line 
            if self.mark_x < self.cursor_x then {
               off1 := TextWidthEx(self.cbwin, s, 1, self.mark_x)
               off2 := TextWidthEx(self.cbwin, s, 1, self.cursor_x)
            } else if self.mark_x > self.cursor_x then {
               off1 := TextWidthEx(self.cbwin, s, 1, self.cursor_x + 1)
               off2 := TextWidthEx(self.cbwin, s, 1, self.mark_x)
            }
         } else if i = self.mark_y then {
            if self.mark_y < self.cursor_y then {
               off1 := TextWidthEx(self.cbwin, s, 1, self.mark_x)
               off2 := TextWidthEx(self.cbwin, s)
            } else {
               off1 := 0
               off2 := TextWidthEx(self.cbwin, s, 1, self.mark_x)
            }
         } else if i = self.cursor_y then {
            if self.mark_y > self.cursor_y then {
               off1 := TextWidthEx(self.cbwin, s, 1, self.cursor_x + 1)
               off2 := TextWidthEx(self.cbwin, s)
            } else {
               off1 := 0
               off2 := TextWidthEx(self.cbwin, s, 1, self.cursor_x)
            }
         }
         if \off1 then
            FillRectangle(rev, xp + off1, 1 + yp - self.line_height / 2,  off2 - off1, self.line_height)
      }
   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()
      if /long_line then {
         mw := TextWidthEx(cwin, contents[1])
         long_line := 1
         every i := 2 to *self.contents do
            if mw <:= TextWidthEx(cwin, contents[i]) then
               long_line := i
      } else
         mw := TextWidthEx(self.cwin, contents[long_line])
      return mw + TextWidthEx(self.cwin, " ")
   end

   method set_one(attr, val)
      case attr of {
         "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'
      self.cursor_x := self.cursor_y := 1
      set_fields(a)
end

class EditableTextListEdit:UndoableEdit(parent,
                                        cursor_x, 
                                        cursor_y,
                                        mark_x,
                                        mark_y,
                                        long_line
                                        )
   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
      self.long_line := parent.long_line
   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
      parent.long_line := self.long_line
   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
      if TextWidthEx(parent.cwin, parent.contents[parent.cursor_y]) > 
         TextWidthEx(parent.cwin, parent.contents[\parent.long_line]) then
         parent.long_line := parent.cursor_y
      parent.clear_mark()
   end

   method undo_impl()
      parent.contents[self.cursor_y][self.cursor_x +: *s] := ""
   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]
      if parent.long_line = parent.cursor_y then
         parent.long_line := &null
      else if parent.long_line > parent.cursor_y then
         parent.long_line +:= 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)
   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.long_line := &null
      } 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.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)
      } 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]
      }
   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.cursor_y -:= 1
         parent.long_line := &null
      } 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.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)
      } 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]
      }
   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] := ""
      else
         delete(parent.contents, parent.cursor_y)
      if parent.long_line = parent.cursor_y then
         parent.long_line := &null
      else if parent.long_line > parent.cursor_y then
         parent.long_line -:= 1
      parent.cursor_x := 1
      parent.clear_mark()
   end

   method undo_impl()
      if self.cursor_y = *parent.contents then
         parent.contents[self.cursor_y] := s
      else
         insert(parent.contents, self.cursor_y, s)
   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
         parent.long_line := &null
      } 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)
         }
         parent.long_line := &null
      } 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.clear_mark()
   end

   method undo_impl()
      delete(parent.contents, pos)
      if pos > *parent.contents then
         while put(parent.contents, pop(l))
      else
         while insert(parent.contents, pos, pull(l))
   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]
write("s=",image(s))
      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.long_line := &null
   end

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

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