"""This plug-in provides a basic emulation of the vi editor's command line.

It is not meant as a vi emulation, ie it will not emulate the insertion
and command modes for instance.
Instead, it provides a way to open a command line (the default key
shortcut is control-:, by analogy with vi's ":" prompt.
In this command line, you can type some of vi's command, to modify the
current editor. The format of the command line is

  [from[,to]]cmd[params]

where "from,to" defines a range of lines other that the command should
act on. If none of "from" and "to" are specified, the command only applies
to the current line.  If only "from" is specified, the command applies to
the line specified by "from". If both are specified, the command applies to
the range of lines defined by those, including those two lines.

"from" and "to" can both be one of:
   - ".": indicates the line on which the cursor currently is
   - "$": indicates the last line in the buffer
   - "<": indicates the start of the current selection
   - ">": indicates the end of the current selection
   - a number: indicates a specific line in the buffer
   - "+" and a number: indicates a line relative to the current line, or
     to the line specified by "from"
   - "-" and a number: indicates a line relative to the current line, or
     to the line specified by "from"
In addition, "from,to" can be replaced by "%" to indicate the whole
buffer.

The supported commands are:
   - s/str/repl/
     replaces the first occurrence of the regular expression "str" by
     "repl". The latter can contain \1, \2,... to reference parenthesis
     groups in "str".
     If a range of lines was specified, the replacement is done for all
     occurrences within that range. If a single line was specified (or
     none, which defaults to the current line), only the first occurrence
     on that line is replaced. If an extra "g" is put after the closing "/",
     all occurrences on that line are replaced.
     For convenience, the separator can be any character, not just "/".
     An extra "i" on the command line will make the search case-insensitive.
     You can set the option "ignorecase" below to be in case-insensitive
     mode by default.

   - d
     deletes the lines in the specied range

Here are a few examples:
   %s/foo/bar
      Replaces all instances of "foo" by "bar" in the whole buffer
   .,$s/foo/bar
      Replaces all instances of "foo" by "bar", in all the lines after the
      current one
   67,120s/\d+/DIGIT:\1
      Replaces all numbers from line 67 to 120 inclusive by adding a
      prefix "DIGIT:" to them

The command window that pops up when you press <control-:> has a history: if
you use the <up> and <down> keys, previous commands can be executed again,
or modified and then executed.
In addition, pression <control-.> will execute the previous command without
displaying the command window. This is by analogy with vi's <.> command,
although the scope is less ambitious here.
"""


#############################################################################
## No user customization below this line
#############################################################################

from GPS import *
import re

Preference ("Plugins/vi/bgcolor").create (
  "Background color", "color",
  """Color to use for the command line window""",
  "red")

Preference ("Plugins/vi/ignorecase").create (
  "Ignore case", "boolean",
  """If enabled, searching will ignore casing by default""",
  False)

try:
   ## If we have PyGTK installed, we'll do the highlighting of the next
   ## matches in the background, which makes the interface more responsive
   import gobject
   has_pygtk = 1
except:
   has_pygtk = 0

def on_gps_started (hook_name):
  parse_xml ("""
  <action name='vi_command_line' category="Editor" output="none">
     <description />
     <filter id="Source editor" />
     <shell lang="python">vi.CmdLine()</shell>
  </action>
  <action name='vi_repeat_cmd' category="Editor" output="none">
     <description />
     <filter id="Source editor" />
     <shell lang="python">vi.CmdLine.repeat_command()</shell>
  </action>
  <menu action='vi_command_line' after="Refill">
    <title>/Edit/Vi command line</title>
  </menu>
  <key action="vi_command_line">control-colon</key>
  <key action="vi_repeat_cmd">control-period</key>
""")

class CmdLine (CommandWindow):
   history = []

   def __init__ (self):
     try:
       self.loc      = EditorBuffer.get().current_view ().cursor ()
       CommandWindow.__init__ (self,
                               prompt      = self.prompt(),
                               on_cancel   = self.on_cancel,
                               on_key      = self.on_key,
                               on_activate = self.on_activate)
       self.set_background (Preference ("Plugins/vi/bgcolor").get())

       self.current_in_history = -1
       self.current_cmd_line = ""  # Before moving in the history

     except:
       pass

   def prompt (self):
     """Return the prompt to use for the command window"""
     return "Cmd:"

   def on_key (self, input, key, cursor_pos):
     if key.lower() == "up":
        if self.current_in_history == 0:
           self.current_cmd_line = input
        if self.current_in_history < len (CmdLine.history) - 1:
           self.current_in_history = self.current_in_history + 1
           self.write (CmdLine.history [self.current_in_history])

     elif key.lower() == "down":
        if self.current_in_history > 0:
           self.current_in_history = self.current_in_history - 1
           self.write (CmdLine.history [self.current_in_history])
        elif self.current_cmd_line != "":
           self.current_in_history = -1
           self.write (CmdLine.current_cmd_line)
           self.current_cmd_line = ""


   def on_activate (self, input):
     """The user has pressed enter"""
     if input != "":
        CmdLine.history.insert (0, input)
        CmdLine.repeat_command (self.loc)

   @staticmethod
   def get_loc (cmd, loc, buffer):
     """Parse the first location described in ref, and returns it and
        the remaining of the command. LOC is used when the location is
        "." or relative"""
     if cmd[0] == ".":
        return (loc, cmd[1:])
     elif cmd[0] == "$":
        return (buffer.end_of_buffer(), cmd[1:])
     elif cmd[0] == "<":
        return (buffer.selection_start(), cmd[1:])
     elif cmd[0] == ">":
        return (buffer.selection_end(), cmd[1:])
     else:
        match = re.search ("([-+]?\d+)(.*)", cmd)
        if match:
           line  = int (match.group(1))
           if cmd[0] == "+" or cmd[0] == "-":
              return (loc.forward_line (line), match.group(2))
           else:
              return (EditorLocation (buffer, line, 1), match.group(2))
        else:
           return (loc,cmd)


   @staticmethod
   def get_scope (cmd, current, buffer):
     """return a tuple: (cmd_to_execute, FROM, TO).
        TO is set to FROM if the command should be executed only once"""
     if cmd [0] == "%":
        return (cmd[1:], buffer.beginning_of_buffer(), buffer.end_of_buffer())

     comma = cmd.find (",")
     if comma < 0:
        l, c = CmdLine.get_loc (cmd, current, buffer)
        return (c, l, l)  ## Execute only once
     else:
        l, c = CmdLine.get_loc (cmd[:comma], current, buffer)
        e, c = CmdLine.get_loc (cmd[comma + 1:], l,   buffer)
        return (c, l, e)

   @staticmethod
   def do_replace (arg, loc, maxloc):
     buffer = loc.buffer()
     params = arg[1:].split (arg [0])
     if len (params) == 3:
        pattern,replace,options = params
     elif len (params) == 2:
        pattern,replace = params
        options = ""

     count = 1
     icase = Preference ("Plugins/vi/ignorecase").get() or (options.find ("i") < 0)
     if loc == maxloc:
        maxloc = loc.end_of_line ()  ## On whole line by default
     else:
        count = 10000000

     if options.find ("g") >= 0:
        count = 100000000            ## as many times as needed

     while count > 0:
        result = loc.search \
           (pattern, regexp=True, \
            dialog_on_failure=False, case_sensitive=icase)
        if not result:
           return
        else:
           start,last = result
           if start > maxloc:
              return

           # Add support for \1,.. in the replacement string
           found = buffer.get_chars (start,last-1)
           if icase:
              r = re.compile (pattern)
           else:
              r = re.compile (pattern, re.IGNORECASE)
           repl = r.sub (replace, found)
           buffer.delete (start, last - 1)
           buffer.insert (start, repl)
           loc = start + len (repl)
           buffer.current_view().goto (loc)

        count = count - 1

   @staticmethod
   def do_delete_line (arg, loc, maxloc):
      loc = loc.beginning_of_line ()
      maxloc = maxloc.end_of_line ()
      loc.buffer().delete (loc, maxloc)
      loc.buffer().current_view().goto (loc)

   @staticmethod
   def repeat_command (loc=None):
     """Repeat the last command that was executed, in Editor"""
     if not loc:
        loc = EditorBuffer.get().current_view ().cursor ()
     buffer = loc.buffer()
     (cmd, loc, maxloc) = CmdLine.get_scope (CmdLine.history[0], loc, buffer)

     buffer.start_undo_group ()
     if cmd [0] == "s":
        CmdLine.do_replace (cmd[1:], loc, maxloc)
     elif cmd [0] == "d":
        CmdLine.do_delete_line (cmd[1:], loc, maxloc)
     buffer.finish_undo_group ()

   def on_cancel (self, input):
     """The user has cancelled the search"""
     pass

Hook ("gps_started").add (on_gps_started)
