# Console window for Insight
# Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License (GPL) as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.


itcl::body Console::constructor {args} {
  global gdbtk_state
  window_name "Console Window"

  debug "$args"
  _build_win
  eval itk_initialize $args
  add_hook gdb_no_inferior_hook [list $this idle dummy]
  
  # There are a bunch of console prefs that have no UI
  # for the user to modify them.  In the event that the user
  # really wants to change them, they will have to be modified
  # in prefs.tcl or by editing .gdbtkinit.  When these prefs
  # gain a prefs UI, the user may change them dynamically
  # and the console window will need notification that they
  # have changed.  Add them to the following list and
  # Console::_update_option.
  foreach option {gdb/console/wrap} {
    pref add_hook $option [code $this _update_option]
  }

  set gdbtk_state(console) $this
}

itcl::body Console::destructor {} {
  global gdbtk_state
  set gdbtk_state(console) ""
  remove_hook gdb_no_inferior_hook [list $this idle dummy]
}

itcl::body Console::_build_win {} {
  iwidgets::scrolledtext $itk_interior.stext \
    -vscrollmode dynamic -textbackground white

  set _twin [$itk_interior.stext component text]

  _set_wrap [pref get gdb/console/wrap]

  $_twin tag configure prompt_tag -foreground [pref get gdb/console/prompt_fg]
  $_twin tag configure err_tag -foreground [pref get gdb/console/error_fg]
  $_twin tag configure log_tag -foreground [pref get gdb/console/log_fg]
  $_twin tag configure target_tag -foreground [pref get gdb/console/target_fg]
  $_twin configure -font [pref get gdb/console/font] \
    -bg $::Colors(textbg) -fg $::Colors(textfg)
  
  #
  # bind editing keys for console window
  #
  bind $_twin <Return> "$this invoke; break"
  bind_plain_key $_twin Control-m "$this invoke; break"
  bind_plain_key $_twin Control-j "$this invoke; break"

  # History control.
  bind_plain_key $_twin Control-p "[code $this _previous]; break"
  bind $_twin <Up> "[code $this _previous]; break"
  bind_plain_key $_twin Control-n "[code $this _next]; break"
  bind $_twin <Down> "[code $this _next]; break"
  bind $_twin <Meta-less> "[code $this _first]; break"
  bind $_twin <Home> "[code $this _first]; break"
  bind $_twin <Meta-greater> "[code $this _last]; break"
  bind $_twin <End> "[code $this _last]; break"
  bind_plain_key $_twin Control-o "[code $this _operate_and_get_next]; break"

  # Tab completion
  bind_plain_key $_twin KeyPress-Tab "[code $this _complete]; break"
  
  # Don't let left arrow or ^B go over the prompt
  bind_plain_key $_twin Control-b {
    if {[%W compare insert <= {cmdmark + 1 char}]} {
      break
    }
  }
  bind $_twin <Left> [bind $_twin <Control-b>]

  # Don't let Control-h, Delete, or Backspace back up over the prompt.
  bind_plain_key $_twin Control-h "[code $this _delete]; break"

  bind $_twin <BackSpace> "[code $this _delete]; break"
  
  bind $_twin <Delete> "[code $this _delete 1]; break"

  # Control-a moves to start of line.
  bind_plain_key $_twin Control-a {
    %W mark set insert {cmdmark + 1 char}
    %W see {insert linestart}
    break
  }

  # Control-u deletes to start of line.
  bind_plain_key $_twin Control-u {
    %W delete {cmdmark + 1 char} insert
    %W see {insert linestart}
  }

  # Control-w deletes previous word.
  bind_plain_key $_twin Control-w {
    if {[%W compare {insert -1c wordstart} > cmdmark]} {
      %W delete {insert -1c wordstart} insert
      %W see insert
    }
  }

  bind $_twin <Control-Up> "[code $this _search_history]; break"
  bind $_twin <Shift-Up> "[code $this _search_history]; break"
  bind $_twin <Control-Down> "[code $this _rsearch_history]; break"
  bind $_twin <Shift-Down> "[code $this _rsearch_history]; break"

  # Don't allow key motion to move insertion point outside the command
  # area.  This is done by fixing up the insertion point after any key
  # movement.  We only need to do this after events we do not
  # explicitly override.  Note that since the edit line is always the
  # last line, we can't possibly go past it, so we don't bother
  # checking that.  Note also that we check for a binding which is
  # simply `;'; this lets us handle keys already bound via
  # bind_plain_key.
  foreach event [bind Text] {
    if {[string match *Key* $event]
	&& ([bind $_twin $event] == ""
	    || [bind $_twin $event] == ";")} {
      bind $_twin $event [bind Text $event]
      bind $_twin $event {+
	if {[%W compare insert <= {cmdmark + 1 char}]} {
	  %W mark set insert {cmdmark + 1 char}
	}
	break
      }
    }
  }

  # Don't allow mouse to put cursor outside command line.  For some
  # events we do this by noticing when the cursor is outside the
  # range, and then saving the insertion point.  For others we notice
  # the saved insertion point.
  set pretag pre-$_twin
  bind $_twin <1> [format {
    if {[%%W compare [tk::TextClosestGap %%W %%x %%y] <= cmdmark]} {
      %s _insertion [%%W index insert]
    } else {
      %s _insertion {}
    }
  } $this $this]
  bind $_twin <B1-Motion> [format {
    if {[%s _insertion] != ""} {
      %%W mark set insert [%s _insertion]
    }
  } $this $this $this]
  # FIXME: has inside information.
  bind $_twin <ButtonRelease-1> [format {
    tk::CancelRepeat
    if {[%s _insertion] != ""} {
      %%W mark set insert [%s _insertion]
    }
    %s _insertion {}
    break
  } $this $this $this]

  # Don't allow inserting text outside the command line.  FIXME:
  # requires inside information.
  # Also make it a little easier to paste by making the button
  # drags a little "fuzzy".
  bind $_twin <B2-Motion> {
    if {!$tk_strictMotif} {
      if {($tk::Priv(x) - 2 < %x < $tk::Priv(x) + 2) \
	    || ($tk::Priv(y) - 2 < %y < $tk::Priv(y) + 2)} {
	set tk::Priv(mouseMoved) 1
      }
      if {$tk::Priv(mouseMoved)} {
	%W scan dragto %x %y
      }
    }
    break
  }
  bind $_twin <ButtonRelease-2> [format {
    if {!$tk::Priv(mouseMoved) || $tk_strictMotif} {
      %s
      break
    }
  } [code $this _paste 1]]
  bind $_twin <<Paste>> "[code $this _paste 0]; break"
  bind $_twin <<PasteSelection>> "[code $this _paste 0]; break"
  bind_plain_key $_twin Control-c "event generate $_twin <<Copy>>"
  bind_plain_key $_twin Control-v "[code $this _paste 1]; break"

  _setprompt
  pack $itk_interior.stext -expand yes -fill both
    
  focus $_twin

}

itcl::body Console::idle {event} {
  set _running 0
  $_top configure -cursor {}
}

# ------------------------------------------------------------------
#  METHOD: busy - busy event handler
# ------------------------------------------------------------------
itcl::body Console::busy {event} {
  set _running 1
  $_top configure -cursor watch
}

# ------------------------------------------------------------------
#  METHOD:  insert - insert new text in the text widget
# ------------------------------------------------------------------
itcl::body Console::insert {line {tag ""}} {
  if {$_needNL} {
    $_twin insert {insert linestart} "\n"
  }
  # Remove all \r characters from line.
  set line [join [split $line \r] {}]
  $_twin insert {insert -1 line lineend} $line $tag

  set nlines [lindex [split [$_twin index end] .] 0]
  if {$nlines > $throttle} {
    set delta [expr {$nlines - $throttle}]
    $_twin delete 1.0 ${delta}.0
  }

  $_twin see insert
  set _needNL 0
  ::update idletasks
}

# ------------------------------------------------------------------
#  NAME:         ConsoleWin::_operate_and_get_next
#  DESCRIPTION:  Invokes the current command and, if this
#                command came from the history, arrange for
#                the next history command to be inserted once this
#                command is finished.
#
#  ARGUMENTS:    None
#  RETURNS:      Nothing
# ------------------------------------------------------------------
itcl::body Console::_operate_and_get_next {} {
  if {$_histElement >= 0} {
    # _pendingHistElement will be used after the new history element
    # is pushed.  So we must increment it.
    set _pendingHistElement [expr {$_histElement + 1}]
  }
  invoke
}

#-------------------------------------------------------------------
#  METHOD:  _previous - recall the previous command
# ------------------------------------------------------------------
itcl::body Console::_previous {} {
  if {$_histElement == -1} {
    # Save partial command.
    set _partialCommand [$_twin get {cmdmark + 1 char} {cmdmark lineend}]
  }
  incr _histElement
  set text [lindex $_history $_histElement]
  if {$text == ""} {
    # No dice.
    incr _histElement -1
    # FIXME flash window.
  } else {
    $_twin delete {cmdmark + 1 char} {cmdmark lineend}
    $_twin insert {cmdmark + 1 char} $text
  }
}

#-------------------------------------------------------------------
#  METHOD:  _search_history - search history for match
# ------------------------------------------------------------------
itcl::body Console::_search_history {} {
  set str [$_twin get {cmdmark + 1 char} {cmdmark lineend}]

  if {$_histElement == -1} {
    # Save partial command.
    set _partialCommand $str
    set ix [lsearch $_history ${str}*]
  } else {
    set str $_partialCommand
    set num [expr $_histElement + 1]
    set ix [lsearch [lrange $_history $num end] ${str}*]
    incr ix $num
  }

  set text [lindex $_history $ix]
  if {$text != ""} {
    set _histElement $ix
    $_twin delete {cmdmark + 1 char} {cmdmark lineend}
    $_twin insert {cmdmark + 1 char} $text
  }
}

#-------------------------------------------------------------------
#  METHOD:  _rsearch_history - search history in reverse for match
# ------------------------------------------------------------------
itcl::body Console::_rsearch_history {} {
  if {$_histElement != -1} {
    set str $_partialCommand
    set num [expr $_histElement - 1]
    set ix $num
    while {$ix >= 0} {
      if {[string match ${str}* [lindex $_history $ix]]} {
	break
      }
      incr ix -1
    }

    set text ""
    if {$ix >= 0} {
      set text [lindex $_history $ix]
      set _histElement $ix
    } else {
      set text $_partialCommand
      set _histElement -1
    }
    $_twin delete {cmdmark + 1 char} {cmdmark lineend}
    $_twin insert {cmdmark + 1 char} $text
  }
}

#-------------------------------------------------------------------
#  METHOD:  _next - recall the next command (scroll forward)
# ------------------------------------------------------------------
itcl::body Console::_next {} {
  if {$_histElement == -1} {
    # FIXME flash window.
    return
  }
  incr _histElement -1
  if {$_histElement == -1} {
    set text $_partialCommand
  } else {
    set text [lindex $_history $_histElement]
  }
  $_twin delete {cmdmark + 1 char} {cmdmark lineend}
  $_twin insert {cmdmark + 1 char} $text
}

#-------------------------------------------------------------------
#  METHOD:  _last - get the last history element
# ------------------------------------------------------------------
itcl::body Console::_last {} {
  set _histElement 0
  _next
}

#-------------------------------------------------------------------
#  METHOD:  _first - get the first (earliest) history element
# ------------------------------------------------------------------
itcl::body Console::_first {} {
  set _histElement [expr {[llength $_history] - 1}]
  _previous
}



#-------------------------------------------------------------------
#  METHOD:  _setprompt - put a prompt at the beginning of a line
# ------------------------------------------------------------------
itcl::body Console::_setprompt {{prompt {}}} {
  if {$prompt == ""} {
    #set prompt [pref get gdb/console/prompt]
    set prompt [gdb_prompt]
  } elseif {$prompt == "none"} {
    set prompt ""
  }
  
  $_twin delete {insert linestart} {insert lineend}
  $_twin insert {insert linestart} $prompt prompt_tag
  $_twin mark set cmdmark "insert -1 char"
  $_twin see insert

  if {$_pendingHistElement >= 0} {
    set _histElement $_pendingHistElement
    set _pendingHistElement -1
    _next
  }
}

#-------------------------------------------------------------------
#  METHOD:  gets - get a line of input from the console
# ------------------------------------------------------------------
itcl::body Console::gets {} {
  set _input_mode 1
#  _setprompt "(input) "
  _setprompt none
  $_twin delete insert end
  $_twin mark set cmdmark {insert -1 char}

  bind_plain_key $_twin Control-d "$this invoke 1; break"
  bind_plain_key $_twin Control-c "[code $this _cancel]; break"

  vwait [scope _input_result]
  set _input_mode 0
  bind_plain_key $_twin Control-c "event generate $_twin <<Copy>>"
  activate
  if {$_input_error} {
    set _input_error 0
    return -code error ""
  }
  return $_input_result
}

#-------------------------------------------------------------------
#  METHOD:  cancel - cancel input when ^C is hit
# ------------------------------------------------------------------
itcl::body Console::_cancel {} {
  if {$_input_mode} {
    set _needNL 1
    $_twin mark set insert {insert lineend}
    $_twin insert {insert lineend} "^C\n"
    incr _invoking
    set _input_error 1
    set _input_result ""
  }
}

#-------------------------------------------------------------------
#  METHOD:  activate - run this after a command is run
# ------------------------------------------------------------------
itcl::body Console::activate {{prompt {}}} {
  if {$_invoking > 0} {
    incr _invoking -1
    _setprompt $prompt
  }
}

#-------------------------------------------------------------------
#  METHOD:  invoke - invoke a command
# ------------------------------------------------------------------
itcl::body Console::invoke {{controld 0}} {
  global gdbtk_state

  set text [$_twin get {cmdmark + 1 char} end ]

  if { "[string range $text 0 1]" == "tk" } {
    if {! [info complete $text] } {
      $_twin insert {insert lineend} " \\\n"
      $_twin see insert
      return
    }
  }

  incr _invoking

  set text [string trimright $text \n]
  if {$text == ""} {
    set text [lindex $_history 0]
    $_twin insert {insert lineend} $text
  }
  $_twin mark set insert {insert lineend}
  $_twin insert {insert lineend} "\n"

  set ok 0
  if {$_running} {
    if {[string index $text 0] == "!"} {
      set text [string range $text 1 end]
      set ok 1
    }
  }

  if {$_input_mode} {
    if {!$controld} {append text \n}
    set _input_result $text
    set _needNL 1
    return
  }
  
  # Only push new nonempty history items.
  if {$text != "" && [lindex $_history 0] != $text} {
    lvarpush _history $text
  }
  
  set index [$_twin index insert]
  
  # Clear current history element, and current partial element.
  set _histElement -1
  set _partialCommand ""
  
  # Need a newline before next insert.
  set _needNL 1
  
  # run command
  if {$gdbtk_state(readline)} {
    set gdbtk_state(readline_response) $text
    return
  }

  if {!$_running || $ok} {
    set result [catch {gdb_immediate "$text" 1} message]
  } else {
    set result 1
    set message "The debugger is busy."
  }

  # gdb_immediate may take a while to finish.  Exit if
  # our window has gone away.
  if {![winfo exists $_twin]} { return }

  if {$result} {
    global errorInfo
    dbug W "Error: $errorInfo\n"
    $_twin insert end "Error: $message\n" err_tag
  } elseif {$message != ""} {
    $_twin insert $index "$message\n"
  }
  
  # Make the prompt visible again.
  activate
  
  # Make sure the insertion point is visible.
  $_twin see insert
}

#-------------------------------------------------------------------
#  PRIVATE METHOD:  _delete - Handle a Delete of some sort.
# ------------------------------------------------------------------
itcl::body Console::_delete {{right 0}} {

  # If we are deleting to the right, and we have this turned off,
  # delete to the right.
  
  if {$right && ![pref get gdb/console/deleteLeft]} {
    set right 0
  }
  
  if {!$right} {
    set insert_valid [$_twin compare insert > {cmdmark + 1 char}]
    set delete_loc "insert-1c"
  } else {
    set insert_valid [$_twin compare insert > cmdmark]
    set delete_loc "insert"
  }
  
  # If there is a selection on the command line, delete it,
  # If there is a selection above the command line, do a
  # regular delete, but don't delete the prompt.
  # If there is no selection, do the delete.
  
  if {![catch {$_twin index sel.first}]} {
    if {[$_twin compare sel.first <= cmdmark]} {
      if {$insert_valid} {
	$_twin delete $delete_loc
      }
    } else {
      $_twin delete sel.first sel.last
    }
  } elseif {$insert_valid} {
    $_twin delete $delete_loc
  }
}

#-------------------------------------------------------------------
#  PRIVATE METHOD:  _insertion - Set or get saved insertion point
# ------------------------------------------------------------------
itcl::body Console::_insertion {args} {
  if {! [llength $args]} {
    return $_saved_insertion
  } else {
    set _saved_insertion [lindex $args 0]
  }
}

# ------------------------------------------------------------------
#  METHOD:  _paste - paste the selection into the console window
# ------------------------------------------------------------------
itcl::body Console::_paste {{check_primary 1}} {
  set sel {}

  if {!$check_primary || [catch {selection get} sel] || $sel == ""} {
    if {[catch {selection get -selection CLIPBOARD} sel] || $sel == ""} {
      return
    }
  }

  #if there is a selection, insert over it:
  if {![catch {$_twin index sel.first}] 
      && [$_twin compare sel.first > {cmdmark + 1 char}]} {
    set point [$_twin index sel.first]
    $_twin delete sel.first sel.last
    $_twin insert $point $sel
  } else {
    $_twin insert insert $sel
  }
}

# ------------------------------------------------------------------
#  METHOD:  _find_lcp - Return the longest common prefix in SLIST.
#              Can be empty string.
# ------------------------------------------------------------------
itcl::body Console::_find_lcp {slist} {
  # Handle trivial cases where list is empty or length 1
  if {[llength $slist] <= 1} {return [lindex $slist 0]}

  set prefix [lindex $slist 0]
  set prefixlast [expr [string length $prefix] - 1]

  foreach str [lrange $slist 1 end] {
    set test_str [string range $str 0 $prefixlast]
    while {[string compare $test_str $prefix] != 0} {
      incr prefixlast -1
      set prefix [string range $prefix 0 $prefixlast]
      set test_str [string range $str 0 $prefixlast]
    }
    if {$prefixlast < 0} break
  }
  return $prefix
}

# ------------------------------------------------------------------
#  METHOD:  _find_completion - Look through COMPLETIONS to generate
#             the suffix needed to do command
# ------------------------------------------------------------------
itcl::body Console::_find_completion {cmd completions} {
  # Get longest common prefix
  set lcp [_find_lcp $completions]
  set cmd_len [string length $cmd]
  # Return suffix beyond end of cmd
  return [string range $lcp $cmd_len end]
}

# ------------------------------------------------------------------
#  METHOD: _complete - Command line completion
# ------------------------------------------------------------------
itcl::body Console::_complete {} {

  set command_line [$_twin get {cmdmark + 1 char} {cmdmark lineend}]
  set choices [gdb_cmd "complete $command_line" 1]
  set choices [string trimright $choices \n]
  set choices [split $choices \n]

  # Just do completion if this is the first tab
  if {!$_saw_tab} {
    set _saw_tab 1
    set completion [_find_completion $command_line $choices]

    # Here is where the completion is actually done.  If there
    # is one match, complete the command and print a space.
    # If two or more matches, complete the command and beep.
    # If no match, just beep.
    switch [llength $choices] {
      0 {}
      1 {
	$_twin insert end "$completion "
	set _saw_tab 0
	return
      }

      default {
	$_twin insert end $completion
      }
    }
    bell
    $_twin see end
    bind $_twin <KeyPress> [code $this _reset_tab]
  } else {
    # User hit another consecutive tab.  List the choices.
    # Note that at this point, choices may contain commands
    # with spaces.  We have to lop off everything before (and
    # including) the last space so that the completion list
    # only shows the possibilities for the last token.
    set choices [lsort $choices]
    if {[regexp ".* " $command_line prefix]} {
      regsub -all $prefix $choices {} choices
    }
    if {[llength choices] != 0} {
      insert "\nCompletions:\n[join $choices \ ]\n"
      $_twin see end
      bind $_twin <KeyPress> [code $this _reset_tab]
    }
  }
}

# ------------------------------------------------------------------
#  METHOD:  _reset_tab - Helper method for tab completion. Used
#             to reset the tab when a key is pressed.
# ------------------------------------------------------------------
itcl::body Console::_reset_tab {} {
  bind $_twin <KeyPress> {}
  set _saw_tab 0
}


# ------------------------------------------------------------------
#  METHOD:  _set_wrap - Set wrap mode
# ------------------------------------------------------------------
itcl::body Console::_set_wrap {wrap} {
  if { $wrap } {
    set hsm none
    set wv char
  } else {
    set hsm dynamic
    set wv none
  }

  $itk_interior.stext configure -hscrollmode $hsm
  $_twin configure -wrap $wv
}

# ------------------------------------------------------------------
#  METHOD:  _update_option - Update in response to preference change
# ------------------------------------------------------------------
itcl::body Console::_update_option {name value} {
  switch -- $name {
    gdb/console/wrap {
      _set_wrap $value
    }

    gdb/console/prompt_fg {
      $_twin tag configure prompt_tag -foreground $value
    }

    gdb/console/error_fg {
      $_twin tag configure err_tag -foreground $value
    }
  }
}

# ------------------------------------------------------------------
#  NAME:         public method Console::test
#  DESCRIPTION:  Executes the given command
#
#  ARGUMENTS:    Command to run
#  RETURNS:      Return value of command
#
#  NOTES:        This will only run if env(GDBTK_TEST_RUNNING)==1.
#                FOR TESTING ONLY
# ------------------------------------------------------------------
itcl::body Console::test {args} {
  global env

  if {[info exists env(GDBTK_TEST_RUNNING)] && $env(GDBTK_TEST_RUNNING) == 1} {
    return [eval $args]
  }
}

