From 7936f6b3a7aa13771955fc377b3c9383b2bda4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Sun, 10 May 2020 16:33:58 +0200 Subject: [PATCH] Vim: use Levenshtein line-wise and hunk-wise --- patch2vimedit/vim_session.py | 148 +++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 15 deletions(-) diff --git a/patch2vimedit/vim_session.py b/patch2vimedit/vim_session.py index 207e967..d10a855 100644 --- a/patch2vimedit/vim_session.py +++ b/patch2vimedit/vim_session.py @@ -1,4 +1,4 @@ -import sys +from hunk_changes import InlineLevenshtein, HunkLevenshtein class LineMovement: @@ -30,6 +30,8 @@ class LineMovement: class VimSession: """ A Vim session instrumented through tmux """ + debug = False + def __init__(self, tmux_session, file_path=None): self.tmux_session = tmux_session self.file_path = file_path @@ -39,10 +41,65 @@ class VimSession: self.tmux_session.type_keys(" {}".format(self.file_path)) self.tmux_session.type_keys("enter") - self.tmux_session.send_keys(":set paste", "enter") + self.mode = "command" + + self.log_open = False + + def log(self, msg): + if not self.debug: + return + + self.set_mode("command") + if not self.log_open: + self.tmux_session.send_keys(":new", "enter", "C-w", "j") + self.log_open = True + + self.tmux_session.send_keys("C-w", "k") + self.tmux_session.send_keys("o", msg.rstrip(), "escape") + self.tmux_session.send_keys("C-w", "j") + + def set_mode(self, new_mode, dry_run=False, ofs_balance=True): + """ Sets Vim to mode `new_mode`. Returns the resulting cursor movement, in + column offset. If `dry_run`, do not actually change the mode, just compute the + offset. If `ofs_balance`, balance the induced offset with appropriate cursor + movement. """ + + if new_mode == self.mode: + return 0 + + if dry_run and ofs_balance: + return 0 + + ofs = 0 + + if self.mode != "command": + ofs -= 1 + + if not dry_run: + self.tmux_session.type_keys("escape") + self.mode = "command" + + if ofs_balance and ofs: # implies `not dry_run` + if ofs < 0: + self.tmux_session.type_keys("{}l".format(-ofs)) + else: + self.tmux_session.type_keys("{}h".format(ofs)) + ofs = 0 + + if new_mode == "insert": + if not dry_run: + self.tmux_session.type_keys("i") + self.mode = "insert" + elif new_mode == "replace": + if not dry_run: + self.tmux_session.type_keys("R") + self.mode = "replace" + + return ofs def edit_file(self, file_path): - self.tmux_session.type_keys("escape", ":e ", file_path, "enter") + self.set_mode("command") + self.tmux_session.type_keys(":e ", file_path, "enter") self.file_path = file_path def apply_patchset(self, patchset): @@ -53,48 +110,109 @@ class VimSession: source = patch.source.decode("utf8") target = patch.target.decode("utf8") if source != target: - self.tmux_session.type_keys( - "escape", ":!mv ", source, " ", target, "enter", - ) + self.set_mode("command") + self.tmux_session.type_keys(":!mv ", source, " ", target, "enter") if self.file_path != target: self.edit_file(target) for hunk in patch: self.apply_hunk(hunk) - self.tmux_session.type_keys("escape", ":w", "enter") + self.set_mode("command") + self.tmux_session.type_keys(":w", "enter") + + @staticmethod + def tabify(text): + """ Substitute groups of four spaces by a tabulation, as much as possible. """ + return text.replace(" ", "\t") def write_line(self, line): """ Write a line to the vim buffer, assuming everything is set up for it and it must be insterted above. """ + line = line.rstrip() + if line.startswith(" "): lead_spaces = 0 while lead_spaces < len(line) and line[lead_spaces] == " ": lead_spaces += 1 line = line.strip() + self.set_mode("command") self.tmux_session.type_keys( "O", "escape", "{}a ".format(lead_spaces), "escape", "A", line, "escape" ) else: self.tmux_session.type_keys("O", line, "escape") + def subst_line(self, pre, post): + """ Substitute the current line of the vim buffer, assuming everything is set + up for it """ + + line_levenshtein = InlineLevenshtein(pre, post).compute() + ops = line_levenshtein["ops"] + + edit_pos = 1 + rel_pos = 0 + + for op, (_, span), values, _ in ops: + if op == "L": + edit_pos += span + rel_pos += span + else: + if rel_pos > 0: + self.set_mode("command") + self.tmux_session.type_keys("{}|".format(edit_pos)) + rel_pos = 0 + if op == "I": + self.set_mode("insert") + self.tmux_session.type_keys(self.tabify(values)) + edit_pos += span + elif op == "D": + self.set_mode("command") + self.tmux_session.type_keys("x") + elif op == "S": + self.set_mode("replace") + self.tmux_session.type_keys(self.tabify(values[1])) + edit_pos += span + + self.set_mode("command") + def apply_hunk(self, hunk): - # So far, very naive. + pre_lines = [] + post_lines = [] + for b_line in hunk.text: + u_line = b_line.decode("utf8") + op = u_line[0] + line = u_line[1:] + + if op in ["+", " "]: + post_lines.append(line) + if op in ["-", " "]: + pre_lines.append(line) + hunk_levenshtein = HunkLevenshtein(pre_lines, post_lines).compute() + line_ops = hunk_levenshtein["ops"] + line_mvt = LineMovement(absolute=hunk.starttgt) - for b_line in hunk.text: - line = b_line.decode("utf8") - if line[0] == " ": + for op, positions, values, cost in line_ops: + if op == "L": + self.log("LEAVE {} -- L{}".format(values[0].strip(), line_mvt.absolute)) line_mvt += 1 else: line_mvt.do(self.tmux_session) - if line[0] == "-": + if op == "I": + self.log("INSERT {}".format(values.strip())) + self.write_line(values) + line_mvt += 1 + elif op == "D": + self.log("DELETE {}".format(values.strip())) self.tmux_session.type_keys("dd") - elif line[0] == "+": - self.write_line(line.strip()[1:]) + elif op == "S": + self.log("SUBST {}/{}".format(values[0].strip(), values[1].strip())) + self.subst_line(values[0].rstrip(), values[1].rstrip()) line_mvt += 1 def quit(self): - self.tmux_session.type_keys("escape", ":q", "enter") + self.set_mode("command") + self.tmux_session.type_keys(":qa!", "enter")