- 新增 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)
165 lines
4.5 KiB
Python
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
|