Files
xiaoli-cli/tests/test_vim_keys.py
T
shuiyu67 66de8eea13 feat: TUI 新增 Vim 风格键位(模态输入 + 聊天导航),版本升 8.0.3
- 新增 xcli_core/vim_keys.py:纯逻辑 Vim 状态机(NORMAL/INSERT),h/j/k/l/w/b/0/$/^/gg/G/i/a/o/I/A/O/dd/x/dw/d$/u
- tui.py:VimSendTextArea 子类接入模态编辑;XiaoliTUI 增加 j/k、Ctrl+F/B/D/U 聊天滚动、Ctrl+Space 聚焦切换、i 聚焦插入、/vim 开关、模式提示栏、/help 更新
- 新增 tests/test_vim_keys.py(18 项动作单测全过)
- 版本号 8.0.2 -> 8.0.3(constants/about/README/tui/rich_tui/lsp/mcp)
2026-08-07 23:35:42 +08:00

165 lines
4.5 KiB
Python

"""VimInputState 纯逻辑单测(不依赖 textual)。
用一个极简 FakeBuffer 持有 (text, offset),把 EditResult 应用回去,
逐动作断言文本/光标/模式变化。
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from xcli_core.vim_keys import VimInputState, EditResult
class FakeBuffer:
"""模拟输入框:持有 text 与 cursor offset。"""
def __init__(self, text, offset, mode="INSERT"):
self.st = VimInputState(mode=mode)
self.text = text
self.offset = offset
def press(self, token):
res = self.st.feed(self.text, self.offset, token)
self.text = res.text
self.offset = res.offset
return res
def press_seq(self, *tokens):
return [self.press(t) for t in tokens]
def test_escape_enters_normal_and_steps_left():
b = FakeBuffer("hello", 5, mode="INSERT")
r = b.press("escape")
assert b.st.mode == "NORMAL"
assert b.offset == 4
assert r.message == "-- NORMAL --"
def test_normal_h_l_clamp():
b = FakeBuffer("hello", 0, mode="NORMAL")
b.press("h") # 行首不动
assert b.offset == 0
b.press_seq("l", "l", "l", "l", "l") # 走到末尾
assert b.offset == 5
b.press("l") # 末尾不动
assert b.offset == 5
def test_j_k_move_lines():
b = FakeBuffer("aa\nbb\ncc", 1, mode="NORMAL") # 第0行 col1
b.press("j") # 第1行 col1
assert b.offset == 4 # "aa\n"=3 +1
b.press("k") # 回第0行 col1
assert b.offset == 1
def test_0_and_dollar():
b = FakeBuffer("hello world", 5, mode="NORMAL")
b.press("0")
assert b.offset == 0
b.press("$")
assert b.offset == 11
def test_i_enters_insert():
b = FakeBuffer("abc", 1, mode="NORMAL")
r = b.press("i")
assert b.st.mode == "INSERT"
assert r.action is None
def test_a_insert_after():
b = FakeBuffer("abc", 1, mode="NORMAL")
r = b.press("a")
assert b.st.mode == "INSERT"
assert b.offset == 2
def test_o_opens_new_line():
b = FakeBuffer("line1\nline2", 2, mode="NORMAL") # 第0行
r = b.press("o")
assert b.st.mode == "INSERT"
assert b.text == "line1\n\nline2"
assert b.offset == 6 # "line1\n"=6
def test_O_opens_above():
b = FakeBuffer("line1\nline2", 7, mode="NORMAL") # 第1行 'l' of line2
r = b.press("O")
assert b.st.mode == "INSERT"
assert b.text == "line1\n\nline2"
assert b.offset == 6 # 新空行起始
def test_dd_deletes_line():
b = FakeBuffer("aaa\nbbb\nccc", 4, mode="NORMAL") # 第1行
b.press_seq("d", "d")
assert b.text == "aaa\nccc"
assert b.offset == 4 # 回到第1行行首(aaa\n 之后)
def test_dw_deletes_word():
b = FakeBuffer("foo bar baz", 0, mode="NORMAL")
b.press_seq("d", "w")
assert b.text == "bar baz"
assert b.offset == 0
def test_d_dollar_deletes_to_eol():
b = FakeBuffer("abc def", 2, mode="NORMAL") # 第0行 col2 ('c')
b.press_seq("d", "$")
assert b.text == "ab"
assert b.offset == 2
def test_x_deletes_char():
b = FakeBuffer("abc", 1, mode="NORMAL") # 'b'
b.press("x")
assert b.text == "ac"
assert b.offset == 1
def test_w_b_word_motion():
b = FakeBuffer("foo bar baz", 0, mode="NORMAL")
b.press("w")
assert b.offset == 4 # 下一词 "bar" 词首
b.press("b")
assert b.offset == 0
def test_gg_and_G():
b = FakeBuffer("a\nb\nc", 4, mode="NORMAL")
b.press_seq("g", "g")
assert b.offset == 0
b.press("G")
assert b.offset == 4 # 末行同列
def test_u_sets_undo_action():
b = FakeBuffer("hello", 2, mode="NORMAL")
r = b.press("u")
assert r.action == "undo"
def test_pending_cleared_on_invalid_combo():
b = FakeBuffer("abc", 1, mode="NORMAL")
b.press("d") # 进入待定
assert b.st.pending == "d"
b.press("x") # 非法组合 -> 清待定, 当普通键
assert b.st.pending is None
assert b.st.mode == "NORMAL"
def test_unmapped_key_ignored_in_normal():
b = FakeBuffer("abc", 1, mode="NORMAL")
r = b.press("z") # 未映射
assert r.handled is True
assert b.text == "abc"
assert b.offset == 1
def test_insert_typing_delegated():
b = FakeBuffer("abc", 1, mode="INSERT")
r = b.press("h") # 非 Esc:交给输入框默认处理
assert r.handled is False