diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 49353d89f..f955c989e 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -512,6 +512,22 @@ class CmdCreate(ObjManipCommand): caller.msg(string) +def _desc_load(caller): + return caller.db.evmenu_target.db.desc or "" + +def _desc_save(caller, buf): + """ + Save line buffer to the desc prop. This should + return True if successful and also report its status to the user. + """ + caller.db.evmenu_target.db.desc = buf + caller.msg("Saved.") + return True + +def _desc_quit(caller): + caller.attributes.remove("evmenu_target") + caller.msg("Exited editor.") + class CmdDesc(MuxCommand): """ describe an object @@ -543,19 +559,24 @@ class CmdDesc(MuxCommand): return def load(caller): - return obj.db.desc or "" + return caller.db.evmenu_target.db.desc or "" def save(caller, buf): """ Save line buffer to the desc prop. This should return True if successful and also report its status to the user. """ - obj.db.desc = buf + caller.db.evmenu_target.db.desc = buf caller.msg("Saved.") return True + def quit(caller): + caller.attributes.remove("evmenu_target") + caller.msg("Exited editor.") + + self.caller.db.evmenu_target = obj # launch the editor - EvEditor(self.caller, loadfunc=load, savefunc=save, key="desc") + EvEditor(self.caller, loadfunc=_desc_load, savefunc=_desc_save, quitfunc=_desc_quit, key="desc", persistent=True) return def func(self): diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 1a584d852..75ed40f9c 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -36,9 +36,10 @@ and initialize it: from builtins import object import re +import inspect from django.conf import settings from evennia import Command, CmdSet -from evennia.utils import is_iter, fill, dedent +from evennia.utils import is_iter, fill, dedent, logger from evennia.commands import cmdhandler # we use cmdhandler instead of evennia.syscmdkeys to @@ -59,9 +60,9 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH _HELP_TEXT = \ """ - any non-command is appended to the end of the buffer. - : - view buffer or only line - :: - view buffer without line numbers or other parsing - ::: - print a ':' as the only character on the line... + : - view buffer or only line(s) + :: - raw-view buffer or only line(s) + ::: - escape - enter ':' as the only character on the line. :h - this help. :w - save the buffer (don't quit) @@ -73,13 +74,13 @@ _HELP_TEXT = \ :uu - (redo) step forward in undo history :UU - reset all changes back to initial state - :dd - delete line + :dd - delete last line or line(s) :dw - delete word or regex in entire buffer or on line - :DD - clear buffer + :DD - clear entire buffer - :y - yank (copy) line to the copy buffer - :x - cut line and store it in the copy buffer - :p - put (paste) previously copied line directly after + :y - yank (copy) line(s) to the copy buffer + :x - cut line(s) and store it in the copy buffer + :p - put (paste) previously copied line(s) directly after :i - insert new text at line . Old line will move down :r - replace line with text :I - insert text at the beginning of line @@ -94,26 +95,26 @@ _HELP_TEXT = \ :echo - turn echoing of the input on/off (helpful for some clients) Legend: - - line numbers, or range lstart:lend, e.g. '3:7'. - - one word or several enclosed in quotes. - - longer string, usually not needed to be enclosed in quotes. + - line number, like '5' or range, like '3:7'. + - a single word, or multiple words with quotes around them. + - longer string, usually not needing quotes. """ _ERROR_LOADFUNC = \ """ {error} -{rBuffer load function error. Could not load initial data.{n +|rBuffer load function error. Could not load initial data.|n """ _ERROR_SAVEFUNC = \ """ {error} -{rSave function returned an error. Buffer not saved.{n +|rSave function returned an error. Buffer not saved.|n """ -_ERROR_NO_SAVEFUNC = "{rNo save function defined. Buffer cannot be 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." @@ -122,11 +123,21 @@ _ERROR_QUITFUNC = \ """ {error} -{rQuit function gave an error. Skipping.{n +|rQuit function gave an error. Skipping.|n """ -_MSG_NO_UNDO = "Nothing to undo" -_MSG_NO_REDO = "Nothing to redo" +_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 +""" + + +_MSG_NO_UNDO = "Nothing to undo." +_MSG_NO_REDO = "Nothing to redo." _MSG_UNDO = "Undid one step." _MSG_REDO = "Redid one step." @@ -155,11 +166,11 @@ class CmdSaveYesNo(Command): self.caller.cmdset.remove(SaveYesNoCmdSet) if self.raw_string.strip().lower() in ("no", "n"): # answered no - self.caller.msg(self.caller.ndb._lineeditor.quit()) + self.caller.msg(self.caller.ndb._eveditor.quit()) else: # answered yes (default) - self.caller.ndb._lineeditor.save_buffer() - self.caller.ndb._lineeditor.quit() + self.caller.ndb._eveditor.save_buffer() + self.caller.ndb._eveditor.quit() class SaveYesNoCmdSet(CmdSet): @@ -204,8 +215,15 @@ class CmdEditorBase(Command): """ linebuffer = [] - if self.editor: - linebuffer = self.editor.get_buffer().split("\n") + 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, @@ -285,6 +303,27 @@ class CmdEditorBase(Command): 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)) + 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) + for key, value in saved_options[1].iteritems(): + 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. @@ -296,7 +335,9 @@ class CmdLineInput(CmdEditorBase): """ Adds the line without any formatting changes. """ - editor = self.editor + caller = self.caller + editor = caller.ndb._eveditor + buf = editor.get_buffer() # add a line of text to buffer @@ -328,7 +369,8 @@ class CmdEditorGroup(CmdEditorBase): efficient presentation. """ caller = self.caller - editor = self.editor + editor = caller.ndb._eveditor + linebuffer = self.linebuffer lstart, lend = self.lstart, self.lend cmd = self.cmdstring @@ -532,6 +574,9 @@ class EvEditorCmdSet(CmdSet): "CmdSet for the editor commands" key = "editorcmdset" mergetype = "Replace" + def at_cmdset_creation(self): + self.add(CmdLineInput()) + self.add(CmdEditorGroup()) #------------------------------------------------------------ # @@ -548,7 +593,7 @@ class EvEditor(object): """ def __init__(self, caller, loadfunc=None, savefunc=None, - quitfunc=None, key=""): + quitfunc=None, key="", persistent=False): """ Args: caller (Object): Who is using the editor. @@ -568,13 +613,17 @@ class EvEditor(object): 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 functions (not methods) + since they have to able to pickle. """ self._key = key self._caller = caller - self._caller.ndb._lineeditor = self + self._caller.ndb._eveditor = self self._buffer = "" self._unsaved = False + self._persistent = persistent if loadfunc: self._loadfunc = loadfunc @@ -590,19 +639,6 @@ class EvEditor(object): else: self._quitfunc = lambda caller: caller.msg(_DEFAULT_NO_QUITFUNC) - # Create the commands we need - cmd1 = CmdLineInput() - cmd1.editor = self - cmd1.obj = self - cmd2 = CmdEditorGroup() - cmd2.obj = self - cmd2.editor = self - # Populate cmdset and add it to caller - editor_cmdset = EvEditorCmdSet() - editor_cmdset.add(cmd1) - editor_cmdset.add(cmd2) - self._caller.cmdset.add(editor_cmdset) - # store the original version self._pristine_buffer = self._buffer self._sep = "-" @@ -615,6 +651,25 @@ class EvEditor(object): # copy buffer self._copy_buffer = [] + if persistent: + # save in tuple {kwargs, other options} + try: + caller.attributes.add("_eveditor_saved",( + {"loadfunc":loadfunc, "savefunc": savefunc, + "quitfunc": quitfunc, "key": key, "persistent": persistent}, + {"_pristine_buffer": self._pristine_buffer, + "_sep": self._sep})) + caller.attributes.add("_eveditor_buffer_temp", (self._buffer, self._undo_buffer)) + except Exception, err: + caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err)) + logger.log_trace("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.") + persistent = False + + # Create the commands we need + caller.cmdset.add(EvEditorCmdSet, permanent=persistent) + # echo inserted text back to caller self._echo_mode = True @@ -628,6 +683,8 @@ class EvEditor(object): try: self._buffer = self._loadfunc(self._caller) except Exception as e: + from evennia.utils import logger + logger.log_trace() self._caller.msg(_ERROR_LOADFUNC.format(error=e)) def get_buffer(self): @@ -654,6 +711,8 @@ class EvEditor(object): self._buffer = buf self.update_undo() self._unsaved = True + if self._persistent: + self._caller.attributes.add("_eveditor_buffer_temp", (self._buffer, self._undo_buffer)) def quit(self): """ @@ -664,13 +723,14 @@ class EvEditor(object): self._quitfunc(self._caller) except Exception as e: self._caller.msg(_ERROR_QUITFUNC.format(error=e)) - del self._caller.ndb._lineeditor + self._caller.nattributes.remove("_eveditor") + self._caller.attributes.remove("_eveditor_buffer_temp") + self._caller.attributes.remove("_eveditor_saved") self._caller.cmdset.remove(EvEditorCmdSet) def save_buffer(self): """ - Saves the content of the buffer. The 'quitting' argument is a bool - indicating whether or not the editor intends to exit after saving. + Saves the content of the buffer. """ if self._unsaved: @@ -742,8 +802,8 @@ class EvEditor(object): nchars = len(buf) sep = self._sep - header = "{n" + sep * 10 + "Line Editor [%s]" % self._key + sep * (_DEFAULT_WIDTH-25-len(self._key)) - footer = "{n" + sep * 10 + "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars) \ + header = "|n" + sep * 10 + "Line Editor [%s]" % self._key + sep * (_DEFAULT_WIDTH-25-len(self._key)) + footer = "|n" + sep * 10 + "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars) \ + sep * 12 + "(:h for help)" + sep * 23 if linenums: main = "\n".join("{b%02i|{n %s" % (iline + 1 + offset, line) for iline, line in enumerate(lines))