Optimize editing large files

- Fixed slow editing on large files.
  - Only calculating appearance of lines that are seen.
  - Not resizing all lines when max line length changes.
- Fast changing of syntax highlighting themes.
- Faster startup, since not highlighting all lines.
This commit is contained in:
Andrew Hamilton 2022-03-11 19:20:51 +10:00
parent 938a086188
commit 80048c64f8
3 changed files with 105 additions and 55 deletions

View file

@ -71,7 +71,7 @@ def get_diff(a_text, b_text):
def get_lines(text_editor, start, end):
return tuple(text_editor.text_widget[start:end]), tuple(text_editor.text_widget.text[start:end])
return tuple(text_editor.text_widget[start:end]), tuple(text_editor.text_widget.appearance_interval((start, end)))
def replace_part(a_str, start, end, part):
@ -125,6 +125,20 @@ def draw_connector(columns, color, left_y, right_y):
line[index] = union_box_line("", line[index])
def ranges_overlap(a, b):
return a[1] > b[0] and a[0] < b[1]
def overlay_list(bg_list, fg_list, index):
if index < 0:
bg_len = len(bg_list)
bg_list[:len(fg_list) + index] = fg_list[abs(index):]
bg_list[bg_len:] = []
else:
bg_list[index:index + len(fg_list)] = fg_list[:len(bg_list) - index]
return bg_list
class DiffEditor:
def __init__(self, left_path, right_path):
@ -142,27 +156,31 @@ class DiffEditor:
appearance[index] = highlight_str(appearance[index], (0, 200, 0), 0.6)
def left_highlight_lines(appearance):
appearance = appearance.copy()
view_x, view_y = self.left_view.position
for op, left_start, left_end, right_start, right_end in self.diff:
if op == "replace":
if (op == "replace"
and ranges_overlap((left_start, left_end), (view_y, view_y + len(appearance)))):
left_lines = get_lines(self.left_editor, left_start, left_end)
right_lines = get_lines(self.right_editor, right_start, right_end)
left_appearance, right_appearance = highlight_modification(
left_lines, right_lines, self.show_sub_highlights)
appearance[left_start:left_end] = left_appearance
highlight_lines(appearance, left_start, left_end, op, "delete")
overlay_list(appearance, left_appearance, left_start - view_y)
highlight_lines(appearance, max(left_start, view_y) - view_y,
min(left_end, view_y + len(appearance)) - view_y, op, "delete")
return appearance
def right_highlight_lines(appearance):
appearance = appearance.copy()
view_x, view_y = self.right_view.position
for op, left_start, left_end, right_start, right_end in self.diff:
if op == "replace":
if (op == "replace"
and ranges_overlap((right_start, right_end), (view_y, view_y + len(appearance)))):
left_lines = get_lines(self.left_editor, left_start, left_end)
right_lines = get_lines(self.right_editor, right_start, right_end)
left_appearance, right_appearance = highlight_modification(
left_lines, right_lines, self.show_sub_highlights)
appearance[right_start:right_end] = right_appearance
highlight_lines(appearance, right_start, right_end, op, "insert")
overlay_list(appearance, right_appearance, right_start - view_y)
highlight_lines(appearance, max(right_start, view_y) - view_y,
min(right_end, view_y + len(appearance)) - view_y, op, "insert")
return appearance
left_decor = editor.Decor(self.left_editor.text_widget, left_highlight_lines)

View file

@ -62,7 +62,7 @@ def _syntax_highlight(text, lexer, style):
return fill3.join("\n", text_widget.text)
@functools.lru_cache(maxsize=500)
@functools.lru_cache(maxsize=5000)
def expand_str(str_):
expanded_str = termstr.TermStr(str_)
return str_ if expanded_str.data == str_ else expanded_str
@ -72,19 +72,20 @@ class Text:
def __init__(self, text, padding_char=" "):
self.padding_char = padding_char
self.text, self.actual_text, self.max_line_length = [], [], 0
self.actual_text = []
self.max_line_length = None
lines = [""] if text == "" else text.splitlines()
if text.endswith("\n"):
lines.append("")
self[:] = lines
def __len__(self):
return len(self.text)
return len(self.actual_text)
def __getitem__(self, line_index):
return self.actual_text[line_index]
@functools.lru_cache(maxsize=1000)
@functools.lru_cache(maxsize=5000)
def _convert_line(self, line, max_line_length):
return expand_str(line).ljust(max_line_length)
@ -94,35 +95,34 @@ class Text:
else: # slice
self._replace_lines(key, value)
@functools.cached_property
def max_line_length(self):
return max(len(expand_str(line)) for line in self.actual_text)
def _replace_lines(self, slice_, new_lines):
fixed_lines = [expand_str(line) for line in new_lines]
max_new_lengths = max(len(line) for line in fixed_lines)
if max_new_lengths > self.max_line_length:
padding = self.padding_char * (max_new_lengths - self.max_line_length)
self.text = [line + padding for line in self.text]
self.max_line_length = max_new_lengths
converted_lines = [self._convert_line(line, self.max_line_length) for line in new_lines]
self.text[slice_], self.actual_text[slice_] = converted_lines, new_lines
new_max_line_length = max(len(expand_str(line)) for line in self.actual_text)
if new_max_line_length < self.max_line_length:
clip_width = self.max_line_length - new_max_line_length
self.text = [line[:-clip_width] for line in self.text]
self.max_line_length = new_max_line_length
self.actual_text[slice_] = new_lines
with contextlib.suppress(AttributeError):
del self.max_line_length
def insert(self, index, line):
self._replace_lines(slice(index, index), [line])
def append(self, line):
self.insert(len(self.text), line)
self.insert(len(self.actual_text), line)
def get_text(self):
return "\n".join(self)
def appearance(self):
return self.text
return [self._convert_line(line, self.max_line_length) for line in self.actual_text]
def appearance_for(self, dimensions):
return fill3.appearance_resize(self.appearance(), dimensions)
def appearance_dimensions(self):
return (self.max_line_length, len(self.actual_text))
def appearance_interval(self, interval):
start_y, end_y = interval
return [self._convert_line(line, self.max_line_length)
for line in self.actual_text[start_y:end_y]]
class Code(Text):
@ -130,23 +130,23 @@ class Code(Text):
def __init__(self, text, path, theme=NATIVE_STYLE):
self.lexer = pygments.lexers.get_lexer_for_filename(path, text, stripnl=False)
self.theme = theme
padding_char = _syntax_highlight(" ", self.lexer, theme)
padding_char = None
Text.__init__(self, text, padding_char)
@functools.lru_cache(maxsize=1000)
def _convert_line(self, line, max_line_length):
highlighted_line = (termstr.TermStr(line) if self.theme is None
else _syntax_highlight(line, self.lexer, self.theme))
@functools.lru_cache(maxsize=5000)
def _convert_line_themed(self, line, max_line_length, theme):
if self.padding_char is None:
self.padding_char = (" " if self.theme is None
else _syntax_highlight(" ", self.lexer, self.theme))
highlighted_line = (termstr.TermStr(line) if theme is None
else _syntax_highlight(line, self.lexer, theme))
return highlighted_line.ljust(max_line_length, fillchar=self.padding_char)
def _convert_line(self, line, max_line_length):
return self._convert_line_themed(line, max_line_length, self.theme)
def syntax_highlight_all(self):
if self.theme is None:
self.text = [termstr.TermStr(line.ljust(self.max_line_length))
for line in self.get_text().splitlines()]
else:
self.padding_char = _syntax_highlight(" ", self.lexer, self.theme)
highlighted = _syntax_highlight(self.get_text(), self.lexer, self.theme)
self.text = [line.ljust(self.max_line_length) for line in highlighted.splitlines()]
self.padding_char = None
class Decor:
@ -161,13 +161,19 @@ class Decor:
def appearance(self):
return self.decorator(self.widget.appearance())
def appearance_interval(self, interval):
return self.decorator(self.widget.appearance_interval(interval))
def appearance_dimensions(self):
return self.widget.appearance_dimensions()
def highlight_part(line, start, end):
return (line[:start] + highlight_str(line[start:end], termstr.Color.white, transparency=0.7) +
line[end:])
@functools.lru_cache(maxsize=500)
@functools.lru_cache(maxsize=5000)
def expandtabs(text):
result = []
for line in text.splitlines(keepends=True):
@ -184,7 +190,7 @@ def expandtabs(text):
return "".join(result)
@functools.lru_cache(maxsize=500)
@functools.lru_cache(maxsize=5000)
def expand_str_inverse(str_):
result = []
for index, char in enumerate(str_):
@ -252,32 +258,39 @@ class Editor:
return (start_x, start_y), (end_x, end_y)
def add_highlights(self, appearance):
result = appearance.copy()
view_x, view_y = self.view_widget.position
result = appearance
if not self.is_editing:
return result
if self.mark is None:
result[self.cursor_y] = highlight_str(result[self.cursor_y], termstr.Color.white, 0.8)
result[self.cursor_y - view_y] = highlight_str(result[self.cursor_y - view_y],
termstr.Color.white, 0.8)
else:
(start_x, start_y), (end_x, end_y) = self.get_selection_interval()
screen_start_x = len(expand_str(self.text_widget[start_y][:start_x]))
screen_end_x = len(expand_str(self.text_widget[end_y][:end_x]))
start_y -= view_y
end_y -= view_y
if start_y == end_y:
result[start_y] = highlight_part(result[start_y], screen_start_x, screen_end_x)
else:
result[start_y] = highlight_part(result[start_y], screen_start_x, len(result[start_y]))
if 0 <= start_y < len(result):
result[start_y] = highlight_part(result[start_y], screen_start_x, len(result[start_y]))
view_x, view_y = self.view_widget.position
for line_num in range(max(start_y+1, view_y), min(end_y, view_y + self.last_height)):
result[line_num] = highlight_part(result[line_num], 0, len(result[line_num]))
result[end_y] = highlight_part(result[end_y], 0, screen_end_x)
for line_num in range(max(start_y+1, 0), min(end_y, self.last_height)):
if 0 <= line_num < len(result):
result[line_num] = highlight_part(result[line_num], 0, len(result[line_num]))
if 0 <= end_y < len(result):
result[end_y] = highlight_part(result[end_y], 0, screen_end_x)
if self.cursor_x >= len(result[0]):
result = fill3.appearance_resize(result, (self.cursor_x+1, len(result)))
cursor_line = result[self.cursor_y]
cursor_line = result[self.cursor_y - view_y]
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] +
termstr.TermStr(cursor_line[screen_x:screen_x_after]).invert() +
cursor_line[screen_x_after:])
result[self.cursor_y - view_y] = (cursor_line[:screen_x] +
termstr.TermStr(cursor_line[screen_x:screen_x_after]).invert() +
cursor_line[screen_x_after:])
return result
def set_text(self, text):
@ -342,7 +355,7 @@ class Editor:
def page_down(self):
new_y = self.cursor_y + self.last_height // 2
self.cursor_x, self.cursor_y = 0, min(len(self.text_widget.text) - 1, new_y)
self.cursor_x, self.cursor_y = 0, min(len(self.text_widget.actual_text) - 1, new_y)
def jump_to_start_of_line(self):
self.cursor_x = 0