editor: Fix wide characters
- Display correctly. - Fix cursor movement. - Work correctly with tabs.
This commit is contained in:
parent
3dd181b27a
commit
9ac5e0e017
3 changed files with 55 additions and 37 deletions
|
|
@ -16,6 +16,8 @@ import pygments.lexers
|
||||||
import pygments.styles
|
import pygments.styles
|
||||||
import termstr
|
import termstr
|
||||||
|
|
||||||
|
import cwcwidth
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=100)
|
@functools.lru_cache(maxsize=100)
|
||||||
def highlight_str(line, bg_color, transparency=0.6):
|
def highlight_str(line, bg_color, transparency=0.6):
|
||||||
|
|
@ -59,6 +61,12 @@ def _syntax_highlight(text, lexer, style):
|
||||||
return fill3.join("\n", text_widget.text)
|
return fill3.join("\n", text_widget.text)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=500)
|
||||||
|
def expand_str(str_):
|
||||||
|
expanded_str = termstr.TermStr(str_)
|
||||||
|
return str_ if expanded_str.data == str_ else expanded_str
|
||||||
|
|
||||||
|
|
||||||
class Text:
|
class Text:
|
||||||
|
|
||||||
def __init__(self, text, padding_char=" "):
|
def __init__(self, text, padding_char=" "):
|
||||||
|
|
@ -85,7 +93,7 @@ class Text:
|
||||||
self._replace_lines(key, value)
|
self._replace_lines(key, value)
|
||||||
|
|
||||||
def _replace_lines(self, slice_, new_lines):
|
def _replace_lines(self, slice_, new_lines):
|
||||||
fixed_lines = [line.expandtabs() for line in new_lines]
|
fixed_lines = [expand_str(line) for line in new_lines]
|
||||||
max_new_lengths = max(len(line) for line in fixed_lines)
|
max_new_lengths = max(len(line) for line in fixed_lines)
|
||||||
if max_new_lengths > self.max_line_length:
|
if max_new_lengths > self.max_line_length:
|
||||||
padding = self.padding_char * (max_new_lengths - self.max_line_length)
|
padding = self.padding_char * (max_new_lengths - self.max_line_length)
|
||||||
|
|
@ -93,7 +101,7 @@ class Text:
|
||||||
self.max_line_length = max_new_lengths
|
self.max_line_length = max_new_lengths
|
||||||
converted_lines = [self._convert_line(line, self.max_line_length) for line in fixed_lines]
|
converted_lines = [self._convert_line(line, self.max_line_length) for line in fixed_lines]
|
||||||
self.text[slice_], self.actual_text[slice_] = converted_lines, new_lines
|
self.text[slice_], self.actual_text[slice_] = converted_lines, new_lines
|
||||||
new_max_line_length = max(len(line.expandtabs()) for line in self.actual_text)
|
new_max_line_length = max(len(expand_str(line)) for line in self.actual_text)
|
||||||
if new_max_line_length < self.max_line_length:
|
if new_max_line_length < self.max_line_length:
|
||||||
clip_width = self.max_line_length - new_max_line_length
|
clip_width = self.max_line_length - new_max_line_length
|
||||||
self.text = [line[:-clip_width] for line in self.text]
|
self.text = [line[:-clip_width] for line in self.text]
|
||||||
|
|
@ -155,16 +163,12 @@ def highlight_part(line, start, end):
|
||||||
line[end:])
|
line[end:])
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=100)
|
@functools.lru_cache(maxsize=500)
|
||||||
def expandtabs_inverse(s):
|
def expand_str_inverse(str_):
|
||||||
parts_len = [len(part) for part in s.split("\t")]
|
result = []
|
||||||
result = list(range(parts_len[0]))
|
for index, char in enumerate(str_):
|
||||||
cursor = parts_len[0]
|
run_length = 8 - len(result) % 8 if char == "\t" else cwcwidth.wcwidth(char)
|
||||||
for part_len in parts_len[1:]:
|
result.extend([index] * run_length)
|
||||||
result.extend([cursor] * (8 - (len(result) % 8)))
|
|
||||||
cursor += 1
|
|
||||||
result.extend(range(cursor, cursor + part_len))
|
|
||||||
cursor += part_len
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -190,12 +194,17 @@ class Editor:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cursor_x(self):
|
def cursor_x(self):
|
||||||
line_length = len(self.text_widget.actual_text[self.cursor_y])
|
try:
|
||||||
return min(self._cursor_x, line_length)
|
return expand_str_inverse(self.text_widget[self.cursor_y])[self._cursor_x]
|
||||||
|
except IndexError:
|
||||||
|
return len(self.text_widget.actual_text[self.cursor_y])
|
||||||
|
|
||||||
@cursor_x.setter
|
@cursor_x.setter
|
||||||
def cursor_x(self, x):
|
def cursor_x(self, x):
|
||||||
self._cursor_x = x
|
try:
|
||||||
|
self._cursor_x = len(expand_str(self.text_widget[self.cursor_y][:x]))
|
||||||
|
except IndexError:
|
||||||
|
self._cursor_x = x
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cursor_y(self):
|
def cursor_y(self):
|
||||||
|
|
@ -229,8 +238,8 @@ class Editor:
|
||||||
result[self.cursor_y] = highlight_str(result[self.cursor_y], termstr.Color.white, 0.8)
|
result[self.cursor_y] = highlight_str(result[self.cursor_y], termstr.Color.white, 0.8)
|
||||||
else:
|
else:
|
||||||
(start_x, start_y), (end_x, end_y) = self.get_selection_interval()
|
(start_x, start_y), (end_x, end_y) = self.get_selection_interval()
|
||||||
screen_start_x = len(self.text_widget[start_y][:start_x].expandtabs())
|
screen_start_x = len(expand_str(self.text_widget[start_y][:start_x]))
|
||||||
screen_end_x = len(self.text_widget[end_y][:end_x].expandtabs())
|
screen_end_x = len(expand_str(self.text_widget[end_y][:end_x]))
|
||||||
if start_y == end_y:
|
if start_y == end_y:
|
||||||
result[start_y] = highlight_part(result[start_y], screen_start_x, screen_end_x)
|
result[start_y] = highlight_part(result[start_y], screen_start_x, screen_end_x)
|
||||||
else:
|
else:
|
||||||
|
|
@ -242,10 +251,12 @@ class Editor:
|
||||||
if self.cursor_x >= len(result[0]):
|
if self.cursor_x >= len(result[0]):
|
||||||
result = fill3.appearance_resize(result, (self.cursor_x+1, len(result)))
|
result = fill3.appearance_resize(result, (self.cursor_x+1, len(result)))
|
||||||
cursor_line = result[self.cursor_y]
|
cursor_line = result[self.cursor_y]
|
||||||
screen_x = len(self.text_widget[self.cursor_y][:self.cursor_x].expandtabs())
|
screen_x = len(expand_str(self.text_widget[self.cursor_y][:self.cursor_x]))
|
||||||
|
screen_x_after = (screen_x + 1 if self._current_character() in ["\t", "\n"]
|
||||||
|
else len(expand_str(self.text_widget[self.cursor_y][:self.cursor_x+1])))
|
||||||
result[self.cursor_y] = (cursor_line[:screen_x] +
|
result[self.cursor_y] = (cursor_line[:screen_x] +
|
||||||
termstr.TermStr(cursor_line[screen_x]).invert() +
|
termstr.TermStr(cursor_line[screen_x:screen_x_after]).invert() +
|
||||||
cursor_line[screen_x+1:])
|
cursor_line[screen_x_after:])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def set_text(self, text):
|
def set_text(self, text):
|
||||||
|
|
@ -259,7 +270,7 @@ class Editor:
|
||||||
self.view_widget.portal.is_scroll_limited = True
|
self.view_widget.portal.is_scroll_limited = True
|
||||||
if not self.is_left_aligned:
|
if not self.is_left_aligned:
|
||||||
self.view_widget.portal.is_left_aligned = False
|
self.view_widget.portal.is_left_aligned = False
|
||||||
self.cursor_x, self.cursor_y = 0, 0
|
self._cursor_x, self._cursor_y = 0, 0
|
||||||
self.original_text = self.text_widget.actual_text.copy()
|
self.original_text = self.text_widget.actual_text.copy()
|
||||||
|
|
||||||
def load(self, path):
|
def load(self, path):
|
||||||
|
|
@ -482,7 +493,7 @@ class Editor:
|
||||||
new_line = (self.text_widget[start_y][:start_x] + "# " +
|
new_line = (self.text_widget[start_y][:start_x] + "# " +
|
||||||
self.text_widget[start_y][start_x:])
|
self.text_widget[start_y][start_x:])
|
||||||
self.text_widget[start_y] = new_line
|
self.text_widget[start_y] = new_line
|
||||||
self.cursor_x = len(new_line)
|
self._cursor_x = len(new_line)
|
||||||
start_y += 1
|
start_y += 1
|
||||||
if end_x != 0:
|
if end_x != 0:
|
||||||
end_y += 1
|
end_y += 1
|
||||||
|
|
@ -599,7 +610,7 @@ class Editor:
|
||||||
new_y = self.cursor_y - height // 2
|
new_y = self.cursor_y - height // 2
|
||||||
else:
|
else:
|
||||||
new_y = view_y
|
new_y = view_y
|
||||||
screen_x = len(self.text_widget[self.cursor_y][:self.cursor_x].expandtabs())
|
screen_x = len(expand_str(self.text_widget[self.cursor_y][:self.cursor_x]))
|
||||||
if screen_x >= view_x + width or screen_x < view_x:
|
if screen_x >= view_x + width or screen_x < view_x:
|
||||||
new_x = screen_x - width // 2
|
new_x = screen_x - width // 2
|
||||||
else:
|
else:
|
||||||
|
|
@ -633,10 +644,7 @@ class Editor:
|
||||||
def on_mouse_press(self, x, y):
|
def on_mouse_press(self, x, y):
|
||||||
view_x, view_y = self.view_widget.position
|
view_x, view_y = self.view_widget.position
|
||||||
self.cursor_y = min(y + view_y - 1, len(self.text_widget) - 1)
|
self.cursor_y = min(y + view_y - 1, len(self.text_widget) - 1)
|
||||||
try:
|
self._cursor_x = x + view_x
|
||||||
self.cursor_x = expandtabs_inverse(self.text_widget[self.cursor_y])[x + view_x]
|
|
||||||
except IndexError:
|
|
||||||
self.cursor_x = expandtabs_inverse(self.text_widget[self.cursor_y])[-1]
|
|
||||||
self.last_mouse_position = (x, y)
|
self.last_mouse_position = (x, y)
|
||||||
|
|
||||||
def on_mouse_drag(self, x, y):
|
def on_mouse_drag(self, x, y):
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -18,4 +18,4 @@ setup(name="diff-edit",
|
||||||
entry_points={"console_scripts": ["diff-edit=diff_edit:main"]},
|
entry_points={"console_scripts": ["diff-edit=diff_edit:main"]},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"pygments==2.10.0", "docopt==0.6.2",
|
"pygments==2.10.0", "docopt==0.6.2",
|
||||||
"fill3 @ git+https://github.com/ahamilton/eris@v2022.02.13#subdirectory=fill3"])
|
"fill3 @ git+https://github.com/ahamilton/eris@v2022.02.23#subdirectory=fill3"])
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import termstr
|
||||||
|
|
||||||
import diff_edit.editor as editor
|
import diff_edit.editor as editor
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -50,18 +52,21 @@ class TextWidgetTestCase(unittest.TestCase):
|
||||||
def test_tabs(self):
|
def test_tabs(self):
|
||||||
text = editor.Text("a\tb\naa\tb")
|
text = editor.Text("a\tb\naa\tb")
|
||||||
self.assertEqual(text.get_text(), "a\tb\naa\tb")
|
self.assertEqual(text.get_text(), "a\tb\naa\tb")
|
||||||
self.assertEqual(text.appearance(), ["a b", "aa b"])
|
self.assertEqual(text.appearance(),
|
||||||
|
[termstr.TermStr("a b"), termstr.TermStr("aa b")])
|
||||||
text = editor.Text("a\tb\tc")
|
text = editor.Text("a\tb\tc")
|
||||||
self.assertEqual(text.appearance(), ["a b c"])
|
self.assertEqual(text.appearance(), [termstr.TermStr("a b c")])
|
||||||
|
|
||||||
def test_expandtabs_inverse(self):
|
def test_expand_str_inverse(self):
|
||||||
self.assertEqual(editor.expandtabs_inverse(""), [])
|
self.assertEqual(editor.expand_str_inverse(""), [])
|
||||||
self.assertEqual(editor.expandtabs_inverse("a"), [0])
|
self.assertEqual(editor.expand_str_inverse("a"), [0])
|
||||||
self.assertEqual(editor.expandtabs_inverse("a\tb"), [0, 1, 1, 1, 1, 1, 1, 1, 2])
|
self.assertEqual(editor.expand_str_inverse("a\tb"), [0, 1, 1, 1, 1, 1, 1, 1, 2])
|
||||||
self.assertEqual(editor.expandtabs_inverse("aaaaaaaaaa\t"),
|
self.assertEqual(editor.expand_str_inverse("aaaaaaaaaa\t"),
|
||||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 10, 10])
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 10, 10])
|
||||||
self.assertEqual(editor.expandtabs_inverse("a\tb\tc"),
|
self.assertEqual(editor.expand_str_inverse("a\tb\tc"),
|
||||||
[0, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 3, 3, 3, 3, 3, 4])
|
[0, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 3, 3, 3, 3, 3, 4])
|
||||||
|
self.assertEqual(editor.expand_str_inverse("♓"), [0, 0])
|
||||||
|
self.assertEqual(editor.expand_str_inverse("♓\tb"), [0, 0, 1, 1, 1, 1, 1, 1, 2])
|
||||||
|
|
||||||
|
|
||||||
class EditorTestCase(unittest.TestCase):
|
class EditorTestCase(unittest.TestCase):
|
||||||
|
|
@ -77,7 +82,7 @@ class EditorTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def _set_editor(self, text, cursor_position):
|
def _set_editor(self, text, cursor_position):
|
||||||
self.editor.set_text(text)
|
self.editor.set_text(text)
|
||||||
self.editor.cursor_x, self.editor.cursor_y = cursor_position
|
self.editor._cursor_x, self.editor._cursor_y = cursor_position
|
||||||
|
|
||||||
def _assert_change(self, method, expected_text, expected_cursor_position):
|
def _assert_change(self, method, expected_text, expected_cursor_position):
|
||||||
method()
|
method()
|
||||||
|
|
@ -183,6 +188,11 @@ class EditorTestCase(unittest.TestCase):
|
||||||
self._assert_change(self.editor.cursor_down, text, (2, 1))
|
self._assert_change(self.editor.cursor_down, text, (2, 1))
|
||||||
self._assert_change(self.editor.jump_to_start_of_line, text, (0, 1))
|
self._assert_change(self.editor.jump_to_start_of_line, text, (0, 1))
|
||||||
self._assert_change(self.editor.jump_to_end_of_line, text, (2, 1))
|
self._assert_change(self.editor.jump_to_end_of_line, text, (2, 1))
|
||||||
|
text = ("♓\n"
|
||||||
|
"bc")
|
||||||
|
self._set_editor(text, (0, 0))
|
||||||
|
self._assert_change(self.editor.cursor_right, text, (1, 0))
|
||||||
|
self._assert_change(self.editor.cursor_down, text, (2, 1))
|
||||||
|
|
||||||
def test_jumping_words(self):
|
def test_jumping_words(self):
|
||||||
text = ("ab .dj\n"
|
text = ("ab .dj\n"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue