Repository: evennia/evennia

Issue:
ANSIString.split() error with ANSI.
Splitting on ANSIStrings kills end colour ANSI strings

How to reproduce: 

``` python
    s = ansi.ANSIString("01234{g56789{n")
    s.split("5")
<<<
[ANSIString(u'01234', decoded=True),
 ANSIString(u'\x1b[1m\x1b[32m6789', decoded=True)]

```

Both the output split parts should be non-coloured, since the first string _ends_ with the `{g` and the split happens on the character _after_ it. Instead that `{g` has been shifted to the beginning of the second half of the split, making that part green. Also the second half's `{n` marker is gone completely (probably shifted one step to the right and off the string entirely).



Discussion:
Concerning the reset character, I'm not certain this is wrong. But if it is wrong, I need to know what is right, and how to handle it consistently.

Should any string that ends with a clear character, when split, always have one? Or should every ANSIString always end with a clear character? I could see an argument for that.

Split could be modified to backtrack until it finds a non-code character to fix the other problem. Going to have to think on this.


Resolving patch:
diff --git a/src/utils/ansi.py b/src/utils/ansi.py
--- a/src/utils/ansi.py
+++ b/src/utils/ansi.py
@@ -567,6 +567,11 @@ def _slice(self, slc):
         replayed.
         """
         slice_indexes = self._char_indexes[slc]
+        # If it's the end of the string, we need to append final color codes.
+        if self._char_indexes and self._char_indexes[-1] in slice_indexes:
+            append_tail = self._get_interleving(len(self))
+        else:
+            append_tail = ''
         if not slice_indexes:
             return ANSIString('')
         try:
@@ -584,7 +589,7 @@ def _slice(self, slc):
                 string += self._raw_string[i]
             except IndexError:
                 pass
-        return ANSIString(string, decoded=True)
+        return ANSIString(string + append_tail, decoded=True)
 
     def __getitem__(self, item):
         """
@@ -599,9 +604,15 @@ def __getitem__(self, item):
             # Slices must be handled specially.
             return self._slice(item)
         try:
-            item = self._char_indexes[item]
+            self._char_indexes[item]
         except IndexError:
-            raise IndexError("ANSIString index out of range.")
+            raise IndexError("ANSIString Index out of range")
+        # Get character codes after the index as well.
+        if self._char_indexes[-1] == self._char_indexes[item]:
+            append_tail = self._get_interleving(item + 1)
+        else:
+            append_tail = ''
+        item = self._char_indexes[item]
 
         clean = self._raw_string[item]
         result = ''
@@ -610,38 +621,7 @@ def __getitem__(self, item):
         for index in range(0, item + 1):
             if index in self._code_indexes:
                 result += self._raw_string[index]
-        return ANSIString(result + clean, decoded=True)
-
-    def rsplit(self, sep=None, maxsplit=None):
-        """
-        Like split, but from the end of the string, rather than the beginning.
-        """
-        return self.split(sep, maxsplit, reverse=True)
-
-    def split(self, sep=None, maxsplit=None, reverse=False):
-        """
-        Splits in a manner similar to the standard string split method. First,
-        we split the clean string. Then we measure each section of the result
-        to figure out where they start and end, and replay any escapes that
-        would have occured before that.
-        """
-        if hasattr(sep, 'clean_string'):
-            sep = sep.clean_string
-        args = [sep]
-        if maxsplit is not None:
-            args.append(maxsplit)
-        if reverse:
-            parent_result = self._clean_string.rsplit(*args)
-        else:
-            parent_result = self._clean_string.split(*args)
-        # Might be None.
-        sep = sep or ''
-        current_index = 0
-        result = []
-        for section in parent_result:
-            result.append(self[current_index:current_index + len(section)])
-            current_index += (len(section)) + len(sep)
-        return result
+        return ANSIString(result + clean + append_tail, decoded=True)
 
     def clean(self):
         """
@@ -748,8 +728,84 @@ def _get_indexes(self):
         # that here.
         if end_index in flat_ranges:
             char_indexes.append(end_index)
+            # And in some instances, this may also end up in the code indexes
+            if end_index in code_indexes:
+                code_indexes.pop()
         return code_indexes, char_indexes
 
+    def _get_interleving(self, index):
+        """
+        Get the code characters from the given slice end to the next
+        character.
+        """
+        try:
+            index = self._char_indexes[index - 1]
+        except IndexError:
+            return ''
+        s = ''
+        while True:
+            index += 1
+            if index in self._char_indexes:
+                break
+            elif index in self._code_indexes:
+                s += self._raw_string[index]
+            else:
+                break
+        return s
+
+    def split(self, by, maxsplit=-1):
+        """
+        Stolen from PyPy's pure Python string implementation, tweaked for
+        ANSIString.
+
+        PyPy is distributed under the MIT licence.
+        http://opensource.org/licenses/MIT
+        """
+        bylen = len(by)
+        if bylen == 0:
+            raise ValueError("empty separator")
+
+        res = []
+        start = 0
+        while maxsplit != 0:
+            next = self._clean_string.find(by, start)
+            if next < 0:
+                break
+            # Get character codes after the index as well.
+            res.append(self[start:next] + self._get_interleving(next))
+            start = next + bylen
+            maxsplit -= 1   # NB. if it's already < 0, it stays < 0
+
+        res.append(self[start:len(self)] + self._get_interleving(len(self)))
+        return res
+
+    def rsplit(self, by, maxsplit=-1):
+        """
+        Stolen from PyPy's pure Python string implementation, tweaked for
+        ANSIString.
+
+        PyPy is distributed under the MIT licence.
+        http://opensource.org/licenses/MIT
+        """
+        res = []
+        end = len(self)
+        bylen = len(by)
+        if bylen == 0:
+            raise ValueError("empty separator")
+
+        while maxsplit != 0:
+            next = self._clean_string.rfind(by, 0, end)
+            if next < 0:
+                break
+            # Get character codes after the index as well.
+            res.append(self[next+bylen:end] + self._get_interleving(end))
+            end = next
+            maxsplit -= 1   # NB. if it's already < 0, it stays < 0
+
+        res.append(self[:end] + self._get_interleving(end))
+        res.reverse()
+        return res
+
     @_spacing_preflight
     def center(self, width, fillchar, difference):
         """


Test patch: