text: """
  EvEditor (Evennia Line Editor)
  
  This implements an advanced line editor for editing longer texts
  in-game. The editor mimics the command mechanisms of the "VI" editor
  (a famous line-by-line editor) as far as reasonable.
  
  Features of the editor:
  
   - undo/redo.
   - edit/replace on any line of the buffer.
   - search&replace text anywhere in buffer.
   - formatting of buffer, or selection, to certain width + indentations.
   - allow to echo the input or not, depending on your client.
  
  To use the editor, just import EvEditor from this module
  and initialize it:
  
      from evennia.utils.eveditor import EvEditor
  
      EvEditor(caller, loadfunc=None, savefunc=None, quitfunc=None, key="", persistent=True)
  
   - caller is the user of the editor, the one to see all feedback.
   - loadfunc(caller) is called when the editor is first launched; the
     return from this function is loaded as the starting buffer in the
     editor.
   - safefunc(caller, buffer) is called with the current buffer when
     saving in the editor. The function should return True/False depending
     on if the saving was successful or not.
   - quitfunc(caller) is called when the editor exits. If this is given,
     no automatic quit messages will be given.
   - key is an optional identifier for the editing session, to be
     displayed in the editor.
   - persistent means the editor state will be saved to the database making it
     survive a server reload. Note that using this mode, the load- save-
     and quit-funcs must all be possible to pickle - notable unusable
     callables are class methods and functions defined inside other
     functions. With persistent=False, no such restriction exists.
   - code set to True activates features on the EvEditor to enter Python code.
  
  In addition, the EvEditor can be used to enter Python source code,
  and offers basic handling of indentation.
  
  """
  from builtins import object
  
  import re
  
  from django.conf import settings
  from evennia import Command, CmdSet
  from evennia.utils import is_iter, fill, dedent, logger, justify, to_str
  from evennia.utils.ansi import raw
  from evennia.commands import cmdhandler
  
  # we use cmdhandler instead of evennia.syscmdkeys to
  # avoid some cases of loading before evennia init'd
  _CMD_NOMATCH = cmdhandler.CMD_NOMATCH
  _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
  
  _RE_GROUP = re.compile(r"\".*?\"|\'.*?\'|\S*")
  # use NAWS in the future?
  _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
  
  # -------------------------------------------------------------
  #
  # texts
  #
  # -------------------------------------------------------------
  
  _HELP_TEXT = """
   <txt>  - any non-command is appended to the end of the buffer.
   :  <l> - view buffer or only line(s) <l>
   :: <l> - raw-view buffer or only line(s) <l>
   :::    - escape - enter ':' as the only character on the line.
   :h     - this help.
  
   :w     - save the buffer (don't quit)
   :wq    - save buffer and quit
   :q     - quit (will be asked to save if buffer was changed)
   :q!    - quit without saving, no questions asked
  
   :u     - (undo) step backwards in undo history
   :uu    - (redo) step forward in undo history
   :UU    - reset all changes back to initial state
  
   :dd <l>     - delete last line or line(s) <l>
   :dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
   :DD         - clear entire buffer
  
   :y  <l>        - yank (copy) line(s) <l> to the copy buffer
   :x  <l>        - cut line(s) <l> and store it in the copy buffer
   :p  <l>        - put (paste) previously copied line(s) directly after <l>
   :i  <l> <txt>  - insert new text <txt> at line <l>. Old line will move down
   :r  <l> <txt>  - replace line <l> with text <txt>
   :I  <l> <txt>  - insert text at the beginning of line <l>
   :A  <l> <txt>  - append text after the end of line <l>
  
   :s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
  
   :j <l> <w> - justify buffer or line <l>. <w> is f, c, l or r. Default f (full)
   :f <l>     - flood-fill entire buffer or line <l>: Equivalent to :j left
   :fi <l>    - indent entire buffer or line <l>
   :fd <l>    - de-indent entire buffer or line <l>
  
   :echo - turn echoing of the input on/off (helpful for some clients)
  """
  
  _HELP_LEGEND = """
      Legend:
      <l>   - line number, like '5' or range, like '3:7'.
      <w>   - a single word, or multiple words with quotes around them.
      <txt> - longer string, usually not needing quotes.
  """
  
  _HELP_CODE = """
   :!    - Execute code buffer without saving
   :<    - Decrease the level of automatic indentation for the next lines
   :>    - Increase the level of automatic indentation for the next lines
   :=    - Switch automatic indentation on/off
  """.lstrip("\n")
  
  _ERROR_LOADFUNC = """
  {error}
  
  |rBuffer load function error. Could not load initial data.|n
  """
  
  _ERROR_SAVEFUNC = """
  {error}
  
  |rSave function returned an error. Buffer not saved.|n
  """
  
  _ERROR_NO_SAVEFUNC = "|rNo save function defined. Buffer cannot be saved.|n"
  
  _MSG_SAVE_NO_CHANGE = "No changes need saving"
  _DEFAULT_NO_QUITFUNC = "Exited editor."
  
  _ERROR_QUITFUNC = """
  {error}
  
  |rQuit function gave an error. Skipping.|n
  """
  
  _ERROR_PERSISTENT_SAVING = """
  {error}
  
  |rThe editor state could not be saved for persistent mode. Switching
  to non-persistent mode (which means the editor session won't survive
  an eventual server reload - so save often!)|n
  """
  
  _TRACE_PERSISTENT_SAVING = \
      "EvEditor persistent-mode error. Commonly, this is because one or " \
      "more of the EvEditor callbacks could not be pickled, for example " \
      "because it's a class method or is defined inside another function."
  
  
  _MSG_NO_UNDO = "Nothing to undo."
  _MSG_NO_REDO = "Nothing to redo."
  _MSG_UNDO = "Undid one step."
  _MSG_REDO = "Redid one step."
  
  # -------------------------------------------------------------
  #
  # Handle yes/no quit question
  #
  # -------------------------------------------------------------
  
  
  class CmdSaveYesNo(Command):
      """
      Save the editor state on quit. This catches
      nomatches (defaults to Yes), and avoid saves only if
      command was given specifically as "no" or "n".
      """
      key = _CMD_NOMATCH
      aliases = _CMD_NOINPUT
      locks = "cmd:all()"
      help_cateogory = "LineEditor"
  
      def func(self):
          """Implement the yes/no choice."""
          # this is only called from inside the lineeditor
          # so caller.ndb._lineditor must be set.
  
          self.caller.cmdset.remove(SaveYesNoCmdSet)
          if self.raw_string.strip().lower() in ("no", "n"):
              # answered no
              self.caller.msg(self.caller.ndb._eveditor.quit())
          else:
              # answered yes (default)
              self.caller.ndb._eveditor.save_buffer()
              self.caller.ndb._eveditor.quit()
  
  
  class SaveYesNoCmdSet(CmdSet):
      """Stores the yesno question"""
      key = "quitsave_yesno"
      priority = 1
      mergetype = "Replace"
  
      def at_cmdset_creation(self):
          """at cmdset creation"""
          self.add(CmdSaveYesNo())
  
  
  # -------------------------------------------------------------
  #
  # Editor commands
  #
  # -------------------------------------------------------------
  
  class CmdEditorBase(Command):
      """
      Base parent for editor commands
      """
      locks = "cmd:all()"
      help_entry = "LineEditor"
  
      editor = None
  
      def parse(self):
          """
          Handles pre-parsing
  
          Editor commands are on the form
              :cmd [li] [w] [txt]
  
          Where all arguments are optional.
              li  - line number (int), starting from 1. This could also
                    be a range given as <l>:<l>.
              w   - word(s) (string), could be encased in quotes.
              txt - extra text (string), could be encased in quotes.
          """
  
          editor = self.caller.ndb._eveditor
          if not editor:
              # this will completely replace the editor
              _load_editor(self.caller)
              editor = self.caller.ndb._eveditor
          self.editor = editor
  
          linebuffer = self.editor.get_buffer().split("\n")
  
          nlines = len(linebuffer)
  
          # The regular expression will split the line by whitespaces,
          # stripping extra whitespaces, except if the text is
          # surrounded by single- or double quotes, in which case they
          # will be kept together and extra whitespace preserved. You
          # can input quotes on the line by alternating single and
          # double quotes.
          arglist = [part for part in _RE_GROUP.findall(self.args) if part]
          temp = []
          for arg in arglist:
              # we want to clean the quotes, but only one type,
              # in case we are nesting.
              if arg.startswith('"'):
                  arg.strip('"')
              elif arg.startswith("'"):
                  arg.strip("'")
              temp.append(arg)
          arglist = temp
  
          # A dumb split, without grouping quotes
          words = self.args.split()
  
          # current line number
          cline = nlines - 1
  
          # the first argument could also be a range of line numbers, on the
          # form <lstart>:<lend>. Either of the ends could be missing, to
          # mean start/end of buffer respectively.
  
          lstart, lend = cline, cline + 1
          linerange = False
          if arglist and arglist[0].count(':') == 1:
              part1, part2 = arglist[0].split(':')
              if part1 and part1.isdigit():
                  lstart = min(max(0, int(part1)) - 1, nlines)
                  linerange = True
              if part2 and part2.isdigit():
                  lend = min(lstart + 1, int(part2)) + 1
                  linerange = True
          elif arglist and arglist[0].isdigit():
              lstart = min(max(0, int(arglist[0]) - 1), nlines)
              lend = lstart + 1
              linerange = True
          if linerange:
              arglist = arglist[1:]
  
          # nicer output formatting of the line range.
          lstr = "line %i" % (lstart + 1) if not linerange or lstart + 1 == lend else "lines %i-%i" % (lstart + 1, lend)
  
          # arg1 and arg2 is whatever arguments. Line numbers or -ranges are
          # never included here.
          args = " ".join(arglist)
          arg1, arg2 = "", ""
          if len(arglist) > 1:
              arg1, arg2 = arglist[0], " ".join(arglist[1:])
          else:
              arg1 = " ".join(arglist)
  
          # store for use in func()
  
          self.linebuffer = linebuffer
          self.nlines = nlines
          self.arglist = arglist
          self.cline = cline
          self.lstart = lstart
          self.lend = lend
          self.linerange = linerange
          self.lstr = lstr
          self.words = words
          self.args = args
          self.arg1 = arg1
          self.arg2 = arg2
  
  
  def _load_editor(caller):
      """
      Load persistent editor from storage.
      """
      saved_options = caller.attributes.get("_eveditor_saved")
      saved_buffer, saved_undo = caller.attributes.get("_eveditor_buffer_temp", (None, None))
      unsaved = caller.attributes.get("_eveditor_unsaved", False)
      indent = caller.attributes.get("_eveditor_indent", 0)
      if saved_options:
          eveditor = EvEditor(caller, **saved_options[0])
          if saved_buffer:
              # we have to re-save the buffer data so we can handle subsequent restarts
              caller.attributes.add("_eveditor_buffer_temp", (saved_buffer, saved_undo))
              setattr(eveditor, "_buffer", saved_buffer)
              setattr(eveditor, "_undo_buffer", saved_undo)
              setattr(eveditor, "_undo_pos", len(saved_undo) - 1)
              setattr(eveditor, "_unsaved", unsaved)
              setattr(eveditor, "_indent", indent)
          for key, value in saved_options[1].items():
              setattr(eveditor, key, value)
      else:
          # something went wrong. Cleanup.
          caller.cmdset.remove(EvEditorCmdSet)
  
  
  class CmdLineInput(CmdEditorBase):
      """
      No command match - Inputs line of text into buffer.
      """
      key = _CMD_NOMATCH
      aliases = _CMD_NOINPUT
  
      def func(self):
          """
          Adds the line without any formatting changes.
  
          If the editor handles code, it might add automatic
          indentation.
          """
          caller = self.caller
          editor = caller.ndb._eveditor
          buf = editor.get_buffer()
  
          # add a line of text to buffer
          line = self.raw_string.strip("\r\n")
          if editor._codefunc and editor._indent >= 0:
              # if automatic indentation is active, add spaces
              line = editor.deduce_indent(line, buf)
          buf = line if not buf else buf + "\n%s" % line
          self.editor.update_buffer(buf)
          if self.editor._echo_mode:
              # need to do it here or we will be off one line
              cline = len(self.editor.get_buffer().split('\n'))
              if editor._codefunc:
                  # display the current level of identation
                  indent = editor._indent
                  if indent < 0:
                      indent = "off"
  
                  self.caller.msg("|b%02i|||n (|g%s|n) %s" % (
                      cline, indent, raw(line)))
              else:
                  self.caller.msg("|b%02i|||n %s" % (cline, raw(self.args)))
  
  
  class CmdEditorGroup(CmdEditorBase):
      """
      Commands for the editor
      """
      key = ":editor_command_group"
      aliases = [":", "::", ":::", ":h", ":w", ":wq", ":q", ":q!", ":u", ":uu", ":UU",
                 ":dd", ":dw", ":DD", ":y", ":x", ":p", ":i", ":j",
                 ":r", ":I", ":A", ":s", ":S", ":f", ":fi", ":fd", ":echo",
                 ":!", ":<", ":>", ":="]
      arg_regex = r"\s.*?|$"
  
      def func(self):
          """
          This command handles all the in-editor :-style commands. Since
          each command is small and very limited, this makes for a more
          efficient presentation.
          """
          caller = self.caller
          editor = caller.ndb._eveditor
  
          linebuffer = self.linebuffer
          lstart, lend = self.lstart, self.lend
          cmd = self.cmdstring
          echo_mode = self.editor._echo_mode
  
          if cmd == ":":
              # Echo buffer
              if self.linerange:
                  buf = linebuffer[lstart:lend]
                  editor.display_buffer(buf=buf, offset=lstart)
              else:
                  editor.display_buffer()
          elif cmd == "::":
              # Echo buffer without the line numbers and syntax parsing
              if self.linerange:
                  buf = linebuffer[lstart:lend]
                  editor.display_buffer(buf=buf,
                                        offset=lstart,
                                        linenums=False, options={"raw": True})
              else:
                  editor.display_buffer(linenums=False, options={"raw": True})
          elif cmd == ":::":
              # Insert single colon alone on a line
              editor.update_buffer([":"] if lstart == 0 else linebuffer + [":"])
              if echo_mode:
                  caller.msg("Single ':' added to buffer.")
          elif cmd == ":h":
              # help entry
              editor.display_help()
          elif cmd == ":w":
              # save without quitting
              editor.save_buffer()
          elif cmd == ":wq":
              # save and quit
              editor.save_buffer()
              editor.quit()
          elif cmd == ":q":
              # quit. If not saved, will ask
              if self.editor._unsaved:
                  caller.cmdset.add(SaveYesNoCmdSet)
                  caller.msg("Save before quitting? |lcyes|lt[Y]|le/|lcno|ltN|le")
              else:
                  editor.quit()
          elif cmd == ":q!":
              # force quit, not checking saving
              editor.quit()
          elif cmd == ":u":
              # undo
              editor.update_undo(-1)
          elif cmd == ":uu":
              # redo
              editor.update_undo(1)
          elif cmd == ":UU":
              # reset buffer
              editor.update_buffer(editor._pristine_buffer)
              caller.msg("Reverted all changes to the buffer back to original state.")
          elif cmd == ":dd":
              # :dd <l> - delete line <l>
              buf = linebuffer[:lstart] + linebuffer[lend:]
              editor.update_buffer(buf)
              caller.msg("Deleted %s." % self.lstr)
          elif cmd == ":dw":
              # :dw <w> - delete word in entire buffer
              # :dw <l> <w> delete word only on line(s) <l>
              if not self.arg1:
                  caller.msg("You must give a search word to delete.")
              else:
                  if not self.linerange:
                      lstart = 0
                      lend = self.cline + 1
                      caller.msg("Removed %s for lines %i-%i." % (self.arg1, lstart + 1, lend + 1))
                  else:
                      caller.msg("Removed %s for %s." % (self.arg1, self.lstr))
                  sarea = "\n".join(linebuffer[lstart:lend])
                  sarea = re.sub(r"%s" % self.arg1.strip("\'").strip('\"'), "", sarea, re.MULTILINE)
                  buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:]
                  editor.update_buffer(buf)
          elif cmd == ":DD":
              # clear buffer
              editor.update_buffer("")
  
              # Reset indentation level to 0
              if editor._codefunc:
                  if editor._indent >= 0:
                      editor._indent = 0
                      if editor._persistent:
                          caller.attributes.add("_eveditor_indent", 0)
              caller.msg("Cleared %i lines from buffer." % self.nlines)
          elif cmd == ":y":
              # :y <l> - yank line(s) to copy buffer
              cbuf = linebuffer[lstart:lend]
              editor._copy_buffer = cbuf
              caller.msg("%s, %s yanked." % (self.lstr.capitalize(), cbuf))
          elif cmd == ":x":
              # :x <l> - cut line to copy buffer
              cbuf = linebuffer[lstart:lend]
              editor._copy_buffer = cbuf
              buf = linebuffer[:lstart] + linebuffer[lend:]
              editor.update_buffer(buf)
              caller.msg("%s, %s cut." % (self.lstr.capitalize(), cbuf))
          elif cmd == ":p":
              # :p <l> paste line(s) from copy buffer
              if not editor._copy_buffer:
                  caller.msg("Copy buffer is empty.")
              else:
                  buf = linebuffer[:lstart] + editor._copy_buffer + linebuffer[lstart:]
                  editor.update_buffer(buf)
                  caller.msg("Pasted buffer %s to %s." % (editor._copy_buffer, self.lstr))
          elif cmd == ":i":
              # :i <l> <txt> - insert new line
              new_lines = self.args.split('\n')
              if not new_lines:
                  caller.msg("You need to enter a new line and where to insert it.")
              else:
                  buf = linebuffer[:lstart] + new_lines + linebuffer[lstart:]
                  editor.update_buffer(buf)
                  caller.msg("Inserted %i new line(s) at %s." % (len(new_lines), self.lstr))
          elif cmd == ":r":
              # :r <l> <txt> - replace lines
              new_lines = self.args.split('\n')
              if not new_lines:
                  caller.msg("You need to enter a replacement string.")
              else:
                  buf = linebuffer[:lstart] + new_lines + linebuffer[lend:]
                  editor.update_buffer(buf)
                  caller.msg("Replaced %i line(s) at %s." % (len(new_lines), self.lstr))
          elif cmd == ":I":
              # :I <l> <txt> - insert text at beginning of line(s) <l>
              if not self.raw_string and not editor._codefunc:
                  caller.msg("You need to enter text to insert.")
              else:
                  buf = linebuffer[:lstart] + ["%s%s" % (self.args, line)
                                               for line in linebuffer[lstart:lend]] + linebuffer[lend:]
                  editor.update_buffer(buf)
                  caller.msg("Inserted text at beginning of %s." % self.lstr)
          elif cmd == ":A":
              # :A <l> <txt> - append text after end of line(s)
              if not self.args:
                  caller.msg("You need to enter text to append.")
              else:
                  buf = linebuffer[:lstart] + ["%s%s" % (line, self.args)
                                               for line in linebuffer[lstart:lend]] + linebuffer[lend:]
                  editor.update_buffer(buf)
                  caller.msg("Appended text to end of %s." % self.lstr)
          elif cmd == ":s":
              # :s <li> <w> <txt> - search and replace words
              # in entire buffer or on certain lines
              if not self.arg1 or not self.arg2:
                  caller.msg("You must give a search word and something to replace it with.")
              else:
                  if not self.linerange:
                      lstart = 0
                      lend = self.cline + 1
                      caller.msg("Search-replaced %s -> %s for lines %i-%i." % (self.arg1, self.arg2, lstart + 1, lend))
                  else:
                      caller.msg("Search-replaced %s -> %s for %s." % (self.arg1, self.arg2, self.lstr))
                  sarea = "\n".join(linebuffer[lstart:lend])
  
                  regex = r"%s|^%s(?=\s)|(?<=\s)%s(?=\s)|^%s$|(?<=\s)%s$"
                  regarg = self.arg1.strip("\'").strip('\"')
                  if " " in regarg:
                      regarg = regarg.replace(" ", " +")
                  sarea = re.sub(regex % (regarg, regarg, regarg, regarg, regarg), self.arg2.strip("\'").strip('\"'),
                                 sarea, re.MULTILINE)
                  buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:]
                  editor.update_buffer(buf)
          elif cmd == ":f":
              # :f <l> flood-fill buffer or <l> lines of buffer.
              width = _DEFAULT_WIDTH
              if not self.linerange:
                  lstart = 0
                  lend = self.cline + 1
                  caller.msg("Flood filled lines %i-%i." % (lstart + 1, lend))
              else:
                  caller.msg("Flood filled %s." % self.lstr)
              fbuf = "\n".join(linebuffer[lstart:lend])
              fbuf = fill(fbuf, width=width)
              buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
              editor.update_buffer(buf)
          elif cmd == ":j":
              # :f <l> <w>  justify buffer of <l> with <w> as align (one of
              # f(ull), c(enter), r(ight) or l(left). Default is full.
              align_map = {"full": "f", "f": "f", "center": "c", "c": "c",
                           "right": "r", "r": "r", "left": "l", "l": "l"}
              align_name = {"f": "Full", "c": "Center", "l": "Left", "r": "Right"}
              width = _DEFAULT_WIDTH
              if self.arg1 and self.arg1.lower() not in align_map:
                  self.caller.msg("Valid justifications are [f]ull (default), [c]enter, [r]right or [l]eft")
                  return
              align = align_map[self.arg1.lower()] if self.arg1 else 'f'
              if not self.linerange:
                  lstart = 0
                  lend = self.cline + 1
                  self.caller.msg("%s-justified lines %i-%i." % (align_name[align], lstart + 1, lend))
              else:
                  self.caller.msg("%s-justified %s." % (align_name[align], self.lstr))
              jbuf = "\n".join(linebuffer[lstart:lend])
              jbuf = justify(jbuf, width=width, align=align)
              buf = linebuffer[:lstart] + jbuf.split("\n") + linebuffer[lend:]
              editor.update_buffer(buf)
          elif cmd == ":fi":
              # :fi <l> indent buffer or lines <l> of buffer.
              indent = " " * 4
              if not self.linerange:
                  lstart = 0
                  lend = self.cline + 1
                  caller.msg("Indented lines %i-%i." % (lstart + 1, lend))
              else:
                  caller.msg("Indented %s." % self.lstr)
              fbuf = [indent + line for line in linebuffer[lstart:lend]]
              buf = linebuffer[:lstart] + fbuf + linebuffer[lend:]
              editor.update_buffer(buf)
          elif cmd == ":fd":
              # :fi <l> indent buffer or lines <l> of buffer.
              if not self.linerange:
                  lstart = 0
                  lend = self.cline + 1
                  caller.msg("Removed left margin (dedented) lines %i-%i." % (lstart + 1, lend))
              else:
                  caller.msg("Removed left margin (dedented) %s." % self.lstr)
              fbuf = "\n".join(linebuffer[lstart:lend])
              fbuf = dedent(fbuf)
              buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
              editor.update_buffer(buf)
          elif cmd == ":echo":
              # set echoing on/off
              editor._echo_mode = not editor._echo_mode
              caller.msg("Echo mode set to %s" % editor._echo_mode)
          elif cmd == ":!":
              if editor._codefunc:
                  editor._codefunc(caller, editor._buffer)
              else:
                  caller.msg("This command is only available in code editor mode.")
          elif cmd == ":<":
              # :<
              if editor._codefunc:
                  editor.decrease_indent()
                  indent = editor._indent
                  if indent >= 0:
                      caller.msg("Decreased indentation: new indentation is {}.".format(
                          indent))
                  else:
                      caller.msg("|rManual indentation is OFF.|n Use := to turn it on.")
              else:
                  caller.msg("This command is only available in code editor mode.")
          elif cmd == ":>":
              # :>
              if editor._codefunc:
                  editor.increase_indent()
                  indent = editor._indent
                  if indent >= 0:
                      caller.msg("Increased indentation: new indentation is {}.".format(
                          indent))
                  else:
                      caller.msg("|rManual indentation is OFF.|n Use := to turn it on.")
              else:
                  caller.msg("This command is only available in code editor mode.")
          elif cmd == ":=":
              # :=
              if editor._codefunc:
                  editor.swap_autoindent()
                  indent = editor._indent
                  if indent >= 0:
                      caller.msg("Auto-indentation turned on.")
                  else:
                      caller.msg("Auto-indentation turned off.")
              else:
                  caller.msg("This command is only available in code editor mode.")
  
  
  class EvEditorCmdSet(CmdSet):
      """CmdSet for the editor commands"""
      key = "editorcmdset"
      mergetype = "Replace"
  
      def at_cmdset_creation(self):
          self.add(CmdLineInput())
          self.add(CmdEditorGroup())
  
  # -------------------------------------------------------------
  #
  # Main Editor object
  #
  # -------------------------------------------------------------
  
  
  class EvEditor(object):
      """
      This defines a line editor object. It creates all relevant commands
      and tracks the current state of the buffer. It also cleans up after
      itself.
  
      """
  
      def __init__(self, caller, loadfunc=None, savefunc=None,
                   quitfunc=None, key="", persistent=False, codefunc=False):
          """
          Launches a full in-game line editor, mimicking the functionality of VIM.
  
          Args:
              caller (Object): Who is using the editor.
              loadfunc (callable, optional): This will be called as
                  `loadfunc(caller)` when the editor is first started. Its
                  return will be used as the editor's starting buffer.
              savefunc (callable, optional): This will be called as
                  `savefunc(caller, buffer)` when the save-command is given and
                  is used to actually determine where/how result is saved.
                  It should return `True` if save was successful and also
                  handle any feedback to the user.
              quitfunc (callable, optional): This will optionally be
                  called as `quitfunc(caller)` when the editor is
                  exited. If defined, it should handle all wanted feedback
                  to the user.
              quitfunc_args (tuple, optional): Optional tuple of arguments to
                  supply to `quitfunc`.
              key (str, optional): An optional key for naming this
                  session and make it unique from other editing sessions.
              persistent (bool, optional): Make the editor survive a reboot. Note
                  that if this is set, all callables must be possible to pickle
              codefunc (bool, optional): If given, will run the editor in code mode.
                  This will be called as `codefunc(caller, buf)`.
  
          Notes:
              In persistent mode, all the input callables (savefunc etc)
              must be possible to be *pickled*, this excludes e.g.
              callables that are class methods or functions defined
              dynamically or as part of another function. In
              non-persistent mode no such restrictions exist.
  
  
  
          """
          self._key = key
          self._caller = caller
          self._caller.ndb._eveditor = self
          self._buffer = ""
          self._unsaved = False
          self._persistent = persistent
          self._indent = 0
  
          if loadfunc:
              self._loadfunc = loadfunc
          else:
              self._loadfunc = lambda caller: self._buffer
          self.load_buffer()
          if savefunc:
              self._savefunc = savefunc
          else:
              self._savefunc = lambda caller, buffer: caller.msg(_ERROR_NO_SAVEFUNC)
          if quitfunc:
              self._quitfunc = quitfunc
          else:
              self._quitfunc = lambda caller: caller.msg(_DEFAULT_NO_QUITFUNC)
          self._codefunc = codefunc
  
          # store the original version
          self._pristine_buffer = self._buffer
          self._sep = "-"
  
          # undo operation buffer
          self._undo_buffer = [self._buffer]
          self._undo_pos = 0
          self._undo_max = 20
  
          # copy buffer
          self._copy_buffer = []
  
          if persistent:
              # save in tuple {kwargs, other options}
              try:
                  caller.attributes.add("_eveditor_saved", (
                      dict(loadfunc=loadfunc, savefunc=savefunc, quitfunc=quitfunc,
                           codefunc=codefunc, key=key, persistent=persistent),
                      dict(_pristine_buffer=self._pristine_buffer, _sep=self._sep)))
                  caller.attributes.add("_eveditor_buffer_temp", (self._buffer, self._undo_buffer))
                  caller.attributes.add("_eveditor_unsaved", False)
                  caller.attributes.add("_eveditor_indent", 0)
              except Exception as err:
                  caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err))
                  logger.log_trace(_TRACE_PERSISTENT_SAVING)
                  persistent = False
  
          # Create the commands we need
          caller.cmdset.add(EvEditorCmdSet, permanent=persistent)
  
          # echo inserted text back to caller
          self._echo_mode = True
  
          # show the buffer ui
          self.display_buffer()
  
      def load_buffer(self):
          """
          Load the buffer using the load function hook.
          """
          try:
              self._buffer = self._loadfunc(self._caller)
              if not isinstance(self._buffer, str):
                  self._buffer = to_str(self._buffer)
                  self._caller.msg("|rNote: input buffer was converted to a string.|n")
          except Exception as e:
              from evennia.utils import logger
              logger.log_trace()
              self._caller.msg(_ERROR_LOADFUNC.format(error=e))
  
      def get_buffer(self):
          """
          Return:
              buffer (str): The current buffer.
  
          """
          return self._buffer
  
      def update_buffer(self, buf):
          """
          This should be called when the buffer has been changed
          somehow.  It will handle unsaved flag and undo updating.
  
          Args:
              buf (str): The text to update the buffer with.
  
          """
          if is_iter(buf):
              buf = "\n".join(buf)
  
          if buf != self._buffer:
              self._buffer = buf
              self.update_undo()
              self._unsaved = True
              if self._persistent:
                  self._caller.attributes.add("_eveditor_buffer_temp", (self._buffer, self._undo_buffer))
                  self._caller.attributes.add("_eveditor_unsaved", True)
                  self._caller.attributes.add("_eveditor_indent", self._indent)
  
      def quit(self):
          """
          Cleanly exit the editor.
  
          """
          try:
              self._quitfunc(self._caller)
          except Exception as e:
              self._caller.msg(_ERROR_QUITFUNC.format(error=e))
          self._caller.nattributes.remove("_eveditor")
          self._caller.attributes.remove("_eveditor_buffer_temp")
          self._caller.attributes.remove("_eveditor_saved")
          self._caller.attributes.remove("_eveditor_unsaved")
          self._caller.attributes.remove("_eveditor_indent")
          self._caller.cmdset.remove(EvEditorCmdSet)
  
      def save_buffer(self):
          """
          Saves the content of the buffer.
  
          """
          if self._unsaved or self._codefunc:
              # always save code - this allows us to tie execution to
              # saving if we want.
              try:
                  if self._savefunc(self._caller, self._buffer):
                      # Save codes should return a true value to indicate
                      # save worked. The saving function is responsible for
                      # any status messages.
                      self._unsaved = False
              except Exception as e:
                  self._caller.msg(_ERROR_SAVEFUNC.format(error=e))
          else:
              self._caller.msg(_MSG_SAVE_NO_CHANGE)
  
      def update_undo(self, step=None):
          """
          This updates the undo position.
  
          Args:
              step (int, optional): The amount of steps
                  to progress the undo position to. This
                  may be a negative value for undo and
                  a positive value for redo.
  
          """
          if step and step < 0:
              # undo
              if self._undo_pos <= 0:
                  self._caller.msg(_MSG_NO_UNDO)
              else:
                  self._undo_pos = max(0, self._undo_pos + step)
                  self._buffer = self._undo_buffer[self._undo_pos]
                  self._caller.msg(_MSG_UNDO)
          elif step and step > 0:
              # redo
              if self._undo_pos >= len(self._undo_buffer) - 1 or self._undo_pos + 1 >= self._undo_max:
                  self._caller.msg(_MSG_NO_REDO)
              else:
                  self._undo_pos = min(self._undo_pos + step, min(len(self._undo_buffer), self._undo_max) - 1)
                  self._buffer = self._undo_buffer[self._undo_pos]
                  self._caller.msg(_MSG_REDO)
          if not self._undo_buffer or (self._undo_buffer and self._buffer != self._undo_buffer[self._undo_pos]):
              # save undo state
              self._undo_buffer = self._undo_buffer[:self._undo_pos + 1] + [self._buffer]
              self._undo_pos = len(self._undo_buffer) - 1
  
      def display_buffer(self, buf=None, offset=0, linenums=True, options={"raw": False}):
          """
          This displays the line editor buffer, or selected parts of it.
  
          Args:
              buf (str, optional): The buffer or part of buffer to display.
              offset (int, optional): If `buf` is set and is not the full buffer,
                  `offset` should define the actual starting line number, to
                  get the linenum display right.
              linenums (bool, optional): Show line numbers in buffer.
              options: raw (bool, optional): Tell protocol to not parse
                  formatting information.
  
          """
          if buf is None:
              buf = self._buffer
          if is_iter(buf):
              buf = "\n".join(buf)
  
          lines = buf.split('\n')
          nlines = len(lines)
          nwords = len(buf.split())
          nchars = len(buf)
  
          sep = self._sep
          header = "|n" + sep * 10 + "Line Editor [%s]" % self._key + sep * (_DEFAULT_WIDTH - 24 - len(self._key))
          footer = "|n" + sep * 10 +\
                   "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars) + sep * 12 + "(:h for help)" + sep * (_DEFAULT_WIDTH - 54)
          if linenums:
              main = "\n".join("|b%02i|||n %s" % (iline + 1 + offset, raw(line)) for iline, line in enumerate(lines))
          else:
              main = "\n".join([raw(line) for line in lines])
          string = "%s\n%s\n%s" % (header, main, footer)
          self._caller.msg(string, options=options)
  
      def display_help(self):
          """
          Shows the help entry for the editor.
  
          """
          string = self._sep * _DEFAULT_WIDTH + _HELP_TEXT
          if self._codefunc:
              string += _HELP_CODE
          string += _HELP_LEGEND + self._sep * _DEFAULT_WIDTH
          self._caller.msg(string)
  
      def deduce_indent(self, line, buffer):
          """
          Try to deduce the level of indentation of the given line.
  
          """
          keywords = {
              "elif ": ["if "],
              "else:": ["if ", "try"],
              "except": ["try:"],
              "finally:": ["try:"],
          }
          opening_tags = ("if ", "try:", "for ", "while ")
  
          # If the line begins by one of the given keywords
          indent = self._indent
          if any(line.startswith(kw) for kw in keywords.keys()):
              # Get the keyword and matching begin tags
              keyword = [kw for kw in keywords if line.startswith(kw)][0]
              begin_tags = keywords[keyword]
              for oline in reversed(buffer.splitlines()):
                  if any(oline.lstrip(" ").startswith(tag) for tag in begin_tags):
                      # This line begins with a begin tag, takes the identation
                      indent = (len(oline) - len(oline.lstrip(" "))) / 4
                      break
  
              self._indent = indent + 1
              if self._persistent:
                  self._caller.attributes.add("_eveditor_indent", self._indent)
          elif any(line.startswith(kw) for kw in opening_tags):
              self._indent = indent + 1
              if self._persistent:
                  self._caller.attributes.add("_eveditor_indent", self._indent)
  
          line = " " * 4 * indent + line
          return line
  
      def decrease_indent(self):
          """Decrease automatic indentation by 1 level."""
          if self._codefunc and self._indent > 0:
              self._indent -= 1
              if self._persistent:
                  self._caller.attributes.add("_eveditor_indent", self._indent)
  
      def increase_indent(self):
          """Increase automatic indentation by 1 level."""
          if self._codefunc and self._indent >= 0:
              self._indent += 1
              if self._persistent:
                  self._caller.attributes.add("_eveditor_indent", self._indent)
  
      def swap_autoindent(self):
          """Swap automatic indentation on or off."""
          if self._codefunc:
              if self._indent >= 0:
                  self._indent = -1
              else:
                  self._indent = 0
  
              if self._persistent:
                  self._caller.attributes.add("_eveditor_indent", self._indent)
  