diff --git a/tests/test_types.py b/tests/test_types.py index f946a8b8..dcea1b0a 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -43,6 +43,18 @@ class TestCorrectedCommand(object): out, _ = capsys.readouterr() assert out[:-1] == printed + def test_run_with_edit(self, capsys, monkeypatch, mocker): + script = "git branch" + edit_tpl = 'editor "{}"' + monkeypatch.setattr( + 'thefuck.types.shell.edit_command', + lambda script: edit_tpl.format(script), + ) + command = CorrectedCommand(script, None, 1000).edit() + command.run(Command(script, '')) + out, _ = capsys.readouterr() + assert out[:-1] == edit_tpl.format(script) + class TestRule(object): def test_from_path(self, mocker): diff --git a/tests/test_ui.py b/tests/test_ui.py index 1e539651..e88d0605 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -16,12 +16,15 @@ def patch_get_key(monkeypatch): return patch -def test_read_actions(patch_get_key): +@pytest.mark.parametrize("shell_can_edit", [True, False]) +def test_read_actions(shell_can_edit, patch_get_key): patch_get_key([ # Enter: '\n', # Enter: '\r', + # Edit: + const.KEY_BACKSPACE, 'd', # Ignored: 'x', 'y', # Up: @@ -30,11 +33,17 @@ def test_read_actions(patch_get_key): const.KEY_DOWN, 'j', # Ctrl+C: const.KEY_CTRL_C, 'q']) - assert (list(islice(ui.read_actions(), 8)) - == [const.ACTION_SELECT, const.ACTION_SELECT, - const.ACTION_PREVIOUS, const.ACTION_PREVIOUS, - const.ACTION_NEXT, const.ACTION_NEXT, - const.ACTION_ABORT, const.ACTION_ABORT]) + expected_actions = [const.ACTION_SELECT, const.ACTION_SELECT, + const.ACTION_PREVIOUS, const.ACTION_PREVIOUS, + const.ACTION_NEXT, const.ACTION_NEXT, + const.ACTION_ABORT, const.ACTION_ABORT] + number_of_items = 8 + if shell_can_edit: + expected_actions.insert(2, const.ACTION_EDIT) + expected_actions.insert(2, const.ACTION_EDIT) + number_of_items = 10 + assert (list(islice(ui.read_actions(shell_can_edit), number_of_items)) + == expected_actions) def test_command_selector(): @@ -106,3 +115,14 @@ class TestSelectCommand(object): u'{mark}\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n' ).format(mark=const.USER_COMMAND_MARK) assert capsys.readouterr() == ('', stderr) + + def test_with_edit(self, capsys, patch_get_key, commands, monkeypatch): + monkeypatch.setattr('thefuck.ui.shell.can_edit', lambda: True) + patch_get_key([const.KEY_BACKSPACE, '\n']) + command = ui.select_command(iter(commands)) + assert command == commands[0] + assert command.should_edit is True + stderr = ( + u'{mark}\x1b[1K\rls [enter/edit/↑/↓/ctrl+c]\n' + ).format(mark=const.USER_COMMAND_MARK) + assert capsys.readouterr() == ('', stderr) diff --git a/thefuck/const.py b/thefuck/const.py index d272f1b2..10fea854 100644 --- a/thefuck/const.py +++ b/thefuck/const.py @@ -14,15 +14,18 @@ KEY_DOWN = _GenConst('↓') KEY_CTRL_C = _GenConst('Ctrl+C') KEY_CTRL_N = _GenConst('Ctrl+N') KEY_CTRL_P = _GenConst('Ctrl+P') +KEY_BACKSPACE = _GenConst('Backspace') KEY_MAPPING = {'\x0e': KEY_CTRL_N, '\x03': KEY_CTRL_C, - '\x10': KEY_CTRL_P} + '\x10': KEY_CTRL_P, + '\x7f': KEY_BACKSPACE} ACTION_SELECT = _GenConst('select') ACTION_ABORT = _GenConst('abort') ACTION_PREVIOUS = _GenConst('previous') ACTION_NEXT = _GenConst('next') +ACTION_EDIT = _GenConst('edit') ALL_ENABLED = _GenConst('All rules enabled') DEFAULT_RULES = [ALL_ENABLED] diff --git a/thefuck/logs.py b/thefuck/logs.py index e064de67..0ba46ba3 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -56,10 +56,19 @@ def show_corrected_command(corrected_command): reset=color(colorama.Style.RESET_ALL))) -def confirm_text(corrected_command): +def edit_part(show_edit): + if show_edit: + return '/{yellow}e{bold}d{normal}it'.format( + yellow=color(colorama.Fore.YELLOW), + bold=color(colorama.Style.BRIGHT), + normal=color(colorama.Style.NORMAL)) + return '' + + +def confirm_text(corrected_command, show_edit): sys.stderr.write( (u'{prefix}{clear}{bold}{script}{reset}{side_effect} ' - u'[{green}enter{reset}/{blue}↑{reset}/{blue}↓{reset}' + u'[{green}enter{reset}{edit}{reset}/{blue}↑{reset}/{blue}↓{reset}' u'/{red}ctrl+c{reset}]').format( prefix=const.USER_COMMAND_MARK, script=corrected_command.script, @@ -69,7 +78,8 @@ def confirm_text(corrected_command): green=color(colorama.Fore.GREEN), red=color(colorama.Fore.RED), reset=color(colorama.Style.RESET_ALL), - blue=color(colorama.Fore.BLUE))) + blue=color(colorama.Fore.BLUE), + edit=edit_part(show_edit))) def debug(msg): diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index 51478192..bf91934e 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -127,3 +127,10 @@ class Fish(Generic): history.write(entry.encode('utf-8')) else: history.write(entry) + + def can_edit(self): + return True + + def edit_command(self, command): + """Return the shell editable command""" + return u'commandline -r "{}"'.format(command) diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index aa81e2ac..2b44a745 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -121,6 +121,13 @@ class Generic(object): """ + def can_edit(self): + return False + + def edit_command(self, command): + """Return the shell editable command""" + return command + def get_builtin_commands(self): """Returns shells builtin commands.""" return ['alias', 'bg', 'bind', 'break', 'builtin', 'case', 'cd', diff --git a/thefuck/types.py b/thefuck/types.py index 8c5770f4..92e64d1a 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -209,6 +209,7 @@ class CorrectedCommand(object): self.script = script self.side_effect = side_effect self.priority = priority + self.should_edit = False def __eq__(self, other): """Ignores `priority` field.""" @@ -232,6 +233,9 @@ class CorrectedCommand(object): of running fuck in case fixed command fails again. """ + if self.should_edit: + self.script = shell.edit_command(self.script) + if settings.repeat: repeat_fuck = '{} --repeat {}--force-command {}'.format( get_alias(), @@ -241,6 +245,10 @@ class CorrectedCommand(object): else: return self.script + def edit(self): + self.should_edit = True + return self + def run(self, old_cmd): """Runs command from rule for passed command. diff --git a/thefuck/ui.py b/thefuck/ui.py index 9c05db33..573fb36e 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -3,21 +3,24 @@ import sys from .conf import settings from .exceptions import NoRuleMatched +from .shells import shell from .system import get_key from .utils import get_alias from . import logs, const -def read_actions(): +def read_actions(can_edit): """Yields actions for pressed keys.""" while True: key = get_key() - # Handle arrows, j/k (qwerty), and n/e (colemak) + # Handle arrows, edit, j/k (qwerty), and n/e (colemak) if key in (const.KEY_UP, const.KEY_CTRL_N, 'k', 'e'): yield const.ACTION_PREVIOUS elif key in (const.KEY_DOWN, const.KEY_CTRL_P, 'j', 'n'): yield const.ACTION_NEXT + elif can_edit and key in (const.KEY_BACKSPACE, 'd'): + yield const.ACTION_EDIT elif key in (const.KEY_CTRL_C, 'q'): yield const.ACTION_ABORT elif key in ('\n', '\r'): @@ -78,18 +81,21 @@ def select_command(corrected_commands): logs.show_corrected_command(selector.value) return selector.value - logs.confirm_text(selector.value) + logs.confirm_text(selector.value, shell.can_edit()) - for action in read_actions(): + for action in read_actions(shell.can_edit()): if action == const.ACTION_SELECT: sys.stderr.write('\n') return selector.value + elif action == const.ACTION_EDIT: + sys.stderr.write('\n') + return selector.value.edit() elif action == const.ACTION_ABORT: logs.failed('\nAborted') return elif action == const.ACTION_PREVIOUS: selector.previous() - logs.confirm_text(selector.value) + logs.confirm_text(selector.value, shell.can_edit()) elif action == const.ACTION_NEXT: selector.next() - logs.confirm_text(selector.value) + logs.confirm_text(selector.value, shell.can_edit())