Repository: kivy/kivy

Issue:
Can't delete space between two long words in TextInput using Backspace
**Software Versions**
* Python: 3.8.5
* OS: Linux Mint 20 (based on Ubuntu 20.04 LTS)
* Kivy: 2.0.0rc3

**Describe the bug**
Suppose I have two long words ('a'*60), separated by space. If I move the cursor to the beginning of the second word and press Backspace, the space doesn't disappear but the cursor moves to the end of the first word.

**Code and Logs and screenshots**
```python
from kivy.app import App
from kivy.uix.textinput import TextInput

class Sample(App):
    def build(self):
        return TextInput(text='a'*60+' '+'a'*60)

Sample().run()
```

**Initial widget**
![bug4_initial](https://user-images.githubusercontent.com/73047043/100487580-5b372100-3101-11eb-9c79-4ebef9f24882.png)

**Current behavior**
![bug4_after](https://user-images.githubusercontent.com/73047043/100487604-7013b480-3101-11eb-8485-6a25f54101a0.png)

**Expected behavior**
![bug4_expected](https://user-images.githubusercontent.com/73047043/100487703-f92aeb80-3101-11eb-93ea-b79731fb3501.png)




Discussion:

Resolving patch:
diff --git a/kivy/uix/textinput.py b/kivy/uix/textinput.py
--- a/kivy/uix/textinput.py
+++ b/kivy/uix/textinput.py
@@ -723,6 +723,9 @@ def insert_text(self, substring, from_undo=False):
         '''Insert new text at the current cursor position. Override this
         function in order to pre-process text for input validation.
         '''
+        _lines = self._lines
+        _lines_flags = self._lines_flags
+
         if self.readonly or not substring or not self._lines:
             return
 
@@ -746,7 +749,7 @@ def insert_text(self, substring, from_undo=False):
 
         col, row = self.cursor
         cindex = self.cursor_index()
-        text = self._lines[row]
+        text = _lines[row]
         len_str = len(substring)
         new_text = text[:col] + substring + text[col:]
         if mode is not None:
@@ -758,48 +761,59 @@ def insert_text(self, substring, from_undo=False):
                     return
         self._set_line_text(row, new_text)
 
-        wrap = (self._get_text_width(
-            new_text,
-            self.tab_width,
-            self._label_cached) > (self.width - self.padding[0] -
-                                   self.padding[2]))
-        if len_str > 1 or substring == u'\n' or wrap:
+        if len_str > 1 or substring == u'\n' or\
+            (substring == u' ' and _lines_flags[row] != FL_IS_LINEBREAK) or\
+            (row + 1 < len(_lines) and
+             _lines_flags[row + 1] != FL_IS_LINEBREAK) or\
+            (self._get_text_width(
+                new_text,
+                self.tab_width,
+                self._label_cached) > (self.width - self.padding[0] -
+                                       self.padding[2])):
             # Avoid refreshing text on every keystroke.
             # Allows for faster typing of text when the amount of text in
             # TextInput gets large.
 
             (
-                start, finish, lines, lineflags, len_lines
+                start, finish, lines, lines_flags, len_lines
             ) = self._get_line_from_cursor(row, new_text)
 
             # calling trigger here could lead to wrong cursor positioning
             # and repeating of text when keys are added rapidly in a automated
             # fashion. From Android Keyboard for example.
             self._refresh_text_from_property(
-                'insert', start, finish, lines, lineflags, len_lines
+                'insert', start, finish, lines, lines_flags, len_lines
             )
 
         self.cursor = self.get_cursor_from_index(cindex + len_str)
         # handle undo and redo
         self._set_unredo_insert(cindex, cindex + len_str, substring, from_undo)
 
-    def _get_line_from_cursor(self, start, new_text):
+    def _get_line_from_cursor(self, start, new_text, lines=None,
+                              lines_flags=None):
         # get current paragraph from cursor position
+        if lines is None:
+            lines = self._lines
+        if lines_flags is None:
+            lines_flags = self._lines_flags
         finish = start
-        lines = self._lines
-        linesflags = self._lines_flags
-        if start and not linesflags[start]:
+        _next = start + 1
+        if start > 0 and lines_flags[start] != FL_IS_LINEBREAK:
             start -= 1
-            new_text = u''.join((lines[start], new_text))
-        try:
-            while not linesflags[finish + 1]:
-                new_text = u''.join((new_text, lines[finish + 1]))
-                finish += 1
-        except IndexError:
-            pass
-        lines, lineflags = self._split_smart(new_text)
+            new_text = lines[start] + new_text
+        i = _next
+        for i in range(_next, len(lines_flags)):
+            if lines_flags[i] == FL_IS_LINEBREAK:
+                finish = i - 1
+                break
+        else:
+            finish = i
+
+        new_text = new_text + u''.join(lines[_next:finish + 1])
+        lines, lines_flags = self._split_smart(new_text)
+
         len_lines = max(1, len(lines))
-        return start, finish, lines, lineflags, len_lines
+        return start, finish, lines, lines_flags, len_lines
 
     def _set_unredo_insert(self, ci, sci, substring, from_undo):
         # handle undo and redo
@@ -878,8 +892,12 @@ def do_undo(self):
                 self._selection = True
                 self.delete_selection(True)
             elif undo_type == 'bkspc':
-                substring = x_item['undo_command'][2:][0]
+                substring = x_item['undo_command'][2][0]
+                mode = x_item['undo_command'][3]
                 self.insert_text(substring, True)
+                if mode == 'del':
+                    self.cursor = self.get_cursor_from_index(
+                        self.cursor_index() - 1)
             elif undo_type == 'shiftln':
                 direction, rows, cursor = x_item['undo_command'][1:]
                 self._shift_lines(direction, rows, cursor, True)
@@ -906,17 +924,21 @@ def do_backspace(self, from_undo=False, mode='bkspc'):
             return
         col, row = self.cursor
         _lines = self._lines
+        _lines_flags = self._lines_flags
         text = _lines[row]
         cursor_index = self.cursor_index()
-        text_last_line = _lines[row - 1]
 
         if col == 0 and row == 0:
             return
-        _lines_flags = self._lines_flags
         start = row
         if col == 0:
-            substring = u'\n' if _lines_flags[row] else u' '
-            new_text = text_last_line + text
+            if _lines_flags[row] == FL_IS_LINEBREAK:
+                substring = u'\n'
+                new_text = _lines[row - 1] + text
+            else:
+                substring = _lines[row - 1][-1] if len(_lines[row - 1]) > 0 \
+                    else u''
+                new_text = _lines[row - 1][:-1] + text
             self._set_line_text(row - 1, new_text)
             self._delete_line(row)
             start = row - 1
@@ -933,22 +955,24 @@ def do_backspace(self, from_undo=False, mode='bkspc'):
         # avoid trigger refresh, leads to issue with
         # keys/text send rapidly through code.
         self._refresh_text_from_property(
-            'del', start, finish, lines, lineflags, len_lines
+            'insert' if col == 0 else 'del', start, finish,
+            lines, lineflags, len_lines
         )
 
         self.cursor = self.get_cursor_from_index(cursor_index - 1)
         # handle undo and redo
-        self._set_undo_redo_bkspc(
+        self._set_unredo_bkspc(
             cursor_index,
             cursor_index - 1,
-            substring, from_undo)
+            substring, from_undo, mode)
 
-    def _set_undo_redo_bkspc(self, ol_index, new_index, substring, from_undo):
+    def _set_unredo_bkspc(self, ol_index, new_index, substring, from_undo,
+                          mode):
         # handle undo and redo for backspace
         if from_undo:
             return
         self._undo.append({
-            'undo_command': ('bkspc', new_index, substring),
+            'undo_command': ('bkspc', new_index, substring, mode),
             'redo_command': ol_index})
         # reset redo when undo is appended to
         self._redo = []
@@ -1387,26 +1411,30 @@ def delete_selection(self, from_undo=False):
         text = self.text
         a, b = sorted((self._selection_from, self._selection_to))
 
-        self.cursor = (start_col, start_row) = self.get_cursor_from_index(a)
-        end_col, end_row = self.get_cursor_from_index(b)
+        start = self.get_cursor_from_index(a)
+        finish = self.get_cursor_from_index(b)
+        cur_line = self._lines[start[1]][:start[0]] +\
+            self._lines[finish[1]][finish[0]:]
+
+        self._set_line_text(start[1], cur_line)
+        start_del, finish_del, lines, lines_flags, len_lines = \
+            self._get_line_from_cursor(start[1], cur_line,
+                                       lines=(self._lines[:(start[1] + 1)] +
+                                              self._lines[(finish[1] + 1):]),
+                                       lines_flags=(
+                                           self._lines_flags[:(start[1] + 1)] +
+                                           self._lines_flags[(finish[1] + 1):])
+                                       )
+        self._refresh_text_from_property('del', start_del,
+                                         finish_del + (finish[1] - start[1]),
+                                         lines, lines_flags, len_lines)
 
-        cur_line = (
-            self._lines[start_row][:start_col]
-            + self._lines[end_row][end_col:]
-        )
-        lines, lineflags = self._split_smart(cur_line)
-
-        if start_row == end_row:
-            self._set_line_text(start_row, cur_line)
-        else:
-            self._refresh_text_from_property(
-                'del', start_row, end_row, lines, lineflags, len(lines)
-            )
         self.scroll_x = scroll_x
         self.scroll_y = scroll_y
         # handle undo and redo for delete selection
         self._set_unredo_delsel(a, b, text[a:b], from_undo)
         self.cancel_selection()
+        self.cursor = self.get_cursor_from_index(a)
 
     def _set_unredo_delsel(self, a, b, substring, from_undo):
         # handle undo and redo for backspace
@@ -2152,17 +2180,12 @@ def _refresh_text(self, text, *largs):
             self._lines[:] = _lines
         elif mode == 'del':
             if finish > start:
-                self._insert_lines(start,
-                                   finish if start == finish else (finish + 1),
-                                   len_lines, _lines_flags,
-                                   _lines, _lines_labels, _line_rects)
+                self._insert_lines(start, finish + 1, len_lines,
+                                   _lines_flags, _lines, _lines_labels,
+                                   _line_rects)
         elif mode == 'insert':
-            self._insert_lines(
-                start,
-                finish if (start == finish and not len_lines)
-                else (finish + 1),
-                len_lines, _lines_flags, _lines, _lines_labels,
-                _line_rects)
+            self._insert_lines(start, finish + 1, len_lines, _lines_flags,
+                               _lines, _lines_labels, _line_rects)
 
         min_line_ht = self._label_cached.get_extents('_')[1]
         # with markup texture can be of height `1`
@@ -2669,16 +2692,23 @@ def _tokenize(self, text):
         # Tokenize a text string from some delimiters
         if text is None:
             return
-        delimiters = u' ,\'".;:\n\r\t'
-        oldindex = 0
+        delimiters = u' .,:;!?\'"\r\t'
+        old_index = 0
+        prev_char = ''
         for index, char in enumerate(text):
             if char not in delimiters:
-                continue
-            if oldindex != index:
-                yield text[oldindex:index]
-            yield text[index:index + 1]
-            oldindex = index + 1
-        yield text[oldindex:]
+                if char != u'\n':
+                    if index > 0 and (prev_char in delimiters):
+                        if old_index < index:
+                            yield text[old_index:index]
+                        old_index = index
+                else:
+                    if old_index < index:
+                        yield text[old_index:index]
+                    yield text[index:index + 1]
+                    old_index = index + 1
+            prev_char = char
+        yield text[old_index:]
 
     def _split_smart(self, text):
         """


Test patch: