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:
parent
938a086188
commit
80048c64f8
3 changed files with 105 additions and 55 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue