WIP: autocompletion

This commit is contained in:
Aldo Cortesi 2017-12-15 07:20:07 +13:00 committed by Aldo Cortesi
parent 0cd4a77268
commit 4d358c49fb
5 changed files with 72 additions and 23 deletions

View File

@ -123,6 +123,11 @@ class Command:
return ret return ret
class ParseResult(typing.NamedTuple):
value: str
type: type
class CommandManager: class CommandManager:
def __init__(self, master): def __init__(self, master):
self.master = master self.master = master
@ -138,11 +143,11 @@ class CommandManager:
def add(self, path: str, func: typing.Callable): def add(self, path: str, func: typing.Callable):
self.commands[path] = Command(self, path, func) self.commands[path] = Command(self, path, func)
def parse_partial(self, cmdstr: str) -> typing.Sequence[typing.Tuple[str, type]]: def parse_partial(self, cmdstr: str) -> typing.Sequence[ParseResult]:
""" """
Parse a possibly partial command. Return a sequence of (part, type) tuples. Parse a possibly partial command. Return a sequence of (part, type) tuples.
""" """
parts: typing.List[typing.Tuple[str, type]] = [] parts: typing.List[ParseResult] = []
buf = io.StringIO(cmdstr) buf = io.StringIO(cmdstr)
# mypy mis-identifies shlex.shlex as abstract # mypy mis-identifies shlex.shlex as abstract
lex = shlex.shlex(buf) # type: ignore lex = shlex.shlex(buf) # type: ignore
@ -151,7 +156,7 @@ class CommandManager:
try: try:
t = lex.get_token() t = lex.get_token()
except ValueError: except ValueError:
parts.append((remainder, str)) parts.append(ParseResult(value = remainder, type = str))
break break
if not t: if not t:
break break
@ -159,7 +164,9 @@ class CommandManager:
# First value is a special case: it has to be a command # First value is a special case: it has to be a command
if not parts: if not parts:
typ = Cmd typ = Cmd
parts.append((t, typ)) parts.append(ParseResult(value = t, type = typ))
if not parts:
return [ParseResult(value = "", type = Cmd)]
return parts return parts
def call_args(self, path, args): def call_args(self, path, args):

View File

@ -1,9 +1,19 @@
import urwid import urwid
from urwid.text_layout import calc_coords from urwid.text_layout import calc_coords
import typing
import mitmproxy.master
import mitmproxy.command
class CompletionState:
def __init__(self, parts: typing.Sequence[mitmproxy.command.ParseResult]) -> None:
self.parts = parts
class CommandBuffer(): class CommandBuffer():
def __init__(self, start: str = "") -> None: def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None:
self.master = master
self.buf = start self.buf = start
# This is the logical cursor position - the display cursor is one # This is the logical cursor position - the display cursor is one
# character further on. Cursor is always within the range [0:len(buffer)]. # character further on. Cursor is always within the range [0:len(buffer)].
@ -31,6 +41,12 @@ class CommandBuffer():
def right(self) -> None: def right(self) -> None:
self.cursor = self.cursor + 1 self.cursor = self.cursor + 1
def cycle_completion(self) -> None:
parts = self.master.commands.parse_partial(self.buf[:self.cursor])
if parts[-1][1] == str:
return
raise ValueError
def backspace(self) -> None: def backspace(self) -> None:
if self.cursor == 0: if self.cursor == 0:
return return
@ -48,8 +64,9 @@ class CommandBuffer():
class CommandEdit(urwid.WidgetWrap): class CommandEdit(urwid.WidgetWrap):
leader = ": " leader = ": "
def __init__(self, text) -> None: def __init__(self, master: mitmproxy.master.Master, text: str) -> None:
self.cbuf = CommandBuffer(text) self.master = master
self.cbuf = CommandBuffer(master, text)
self._w = urwid.Text(self.leader) self._w = urwid.Text(self.leader)
self.update() self.update()
@ -60,6 +77,8 @@ class CommandEdit(urwid.WidgetWrap):
self.cbuf.left() self.cbuf.left()
elif key == "right": elif key == "right":
self.cbuf.right() self.cbuf.right()
elif key == "tab":
self.cbuf.cycle_completion()
elif len(key) == 1: elif len(key) == 1:
self.cbuf.insert(key) self.cbuf.insert(key)
self.update() self.update()

View File

@ -67,7 +67,7 @@ class ActionBar(urwid.WidgetWrap):
def sig_prompt_command(self, sender, partial=""): def sig_prompt_command(self, sender, partial=""):
signals.focus.send(self, section="footer") signals.focus.send(self, section="footer")
self._w = commander.CommandEdit(partial) self._w = commander.CommandEdit(self.master, partial)
self.prompting = commandeditor.CommandExecutor(self.master) self.prompting = commandeditor.CommandExecutor(self.master)
def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()):

View File

@ -66,8 +66,22 @@ class TestCommand:
def test_parse_partial(self): def test_parse_partial(self):
tests = [ tests = [
["foo bar", [("foo", command.Cmd), ("bar", str)]], [
["foo 'bar", [("foo", command.Cmd), ("'bar", str)]], "foo bar",
[
command.ParseResult(value = "foo", type = command.Cmd),
command.ParseResult(value = "bar", type = str)
],
],
[
"foo 'bar",
[
command.ParseResult(value = "foo", type = command.Cmd),
command.ParseResult(value = "'bar", type = str)
]
],
["a", [command.ParseResult(value = "a", type = command.Cmd)]],
["", [command.ParseResult(value = "", type = command.Cmd)]],
] ]
with taddons.context() as tctx: with taddons.context() as tctx:
cm = command.CommandManager(tctx.master) cm = command.CommandManager(tctx.master)

View File

@ -1,5 +1,5 @@
from mitmproxy.tools.console.commander import commander from mitmproxy.tools.console.commander import commander
from mitmproxy.test import taddons
class TestCommandBuffer: class TestCommandBuffer:
@ -13,8 +13,9 @@ class TestCommandBuffer:
[("123", 2), ("13", 1)], [("123", 2), ("13", 1)],
[("123", 0), ("123", 0)], [("123", 0), ("123", 0)],
] ]
with taddons.context() as tctx:
for start, output in tests: for start, output in tests:
cb = commander.CommandBuffer() cb = commander.CommandBuffer(tctx.master)
cb.buf, cb.cursor = start[0], start[1] cb.buf, cb.cursor = start[0], start[1]
cb.backspace() cb.backspace()
assert cb.buf == output[0] assert cb.buf == output[0]
@ -26,9 +27,17 @@ class TestCommandBuffer:
[("a", 0), ("xa", 1)], [("a", 0), ("xa", 1)],
[("xa", 2), ("xax", 3)], [("xa", 2), ("xax", 3)],
] ]
with taddons.context() as tctx:
for start, output in tests: for start, output in tests:
cb = commander.CommandBuffer() cb = commander.CommandBuffer(tctx.master)
cb.buf, cb.cursor = start[0], start[1] cb.buf, cb.cursor = start[0], start[1]
cb.insert("x") cb.insert("x")
assert cb.buf == output[0] assert cb.buf == output[0]
assert cb.cursor == output[1] assert cb.cursor == output[1]
def test_cycle_completion(self):
with taddons.context() as tctx:
cb = commander.CommandBuffer(tctx.master)
cb.buf = "foo bar"
cb.cursor = len(cb.buf)
cb.cycle_completion()