feat: 添加 Ctrl+R 反向历史搜索

- Ctrl+R 进入搜索模式,输入关键词实时过滤历史记录
- 输入区显示 bash 风格搜索提示: (reverse-i-search)\query': result
- 再按 Ctrl+R 搜索更旧的匹配项
- Enter 选定结果放入输入框,Esc 退出搜索
- 快捷键栏显示搜索模式操作提示

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
abel533 1 month ago
parent 6c5580ccd7
commit 012d5dfae6
  1. 94
      src/main/java/com/claudecode/tui/ClaudeCodeComponent.java

@ -111,6 +111,12 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
private volatile List<String> commandSuggestions = List.of(); // 当前匹配的命令名列表
private volatile int tabCompletionIndex = -1; // 当前 Tab 循环索引,-1 表示未开始
/** Ctrl+R 反向历史搜索状态 */
private volatile boolean historySearchMode = false; // 是否在搜索模式
private volatile String historySearchQuery = ""; // 搜索关键词
private volatile String historySearchResult = ""; // 当前匹配结果
private volatile int historySearchIndex = -1; // 匹配到的历史记录索引
/** 首次用户输入回调(用于 conversation summary) */
private Consumer<String> onFirstUserInput;
@ -185,6 +191,12 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
// 权限选择模式:光标在选中选项的 ❯ 位置
int permCursorRow = h - 2 - (snapPermOptions.size() - snapPermSelected);
setCursorPosition(Math.max(0, permCursorRow), 3);
} else if (historySearchMode) {
// 搜索模式:光标在搜索词 █ 的位置
// "(reverse-i-search)`" = 20 chars, then query, then "█"
int cursorRow = Math.max(0, h - 3);
int cursorCol = 1 + 20 + StringWidth.width(historySearchQuery);
setCursorPosition(cursorRow, cursorCol);
} else {
// 正常模式:光标隐藏在块光标 █ 的位置
int cursorRow = Math.max(0, h - 3);
@ -511,6 +523,20 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
return renderPermissionSelectArea(s, w);
}
// Ctrl+R 历史搜索模式
if (historySearchMode) {
String query = historySearchQuery;
String result = historySearchResult;
Text searchPrompt = Text.of(
Text.of("(reverse-i-search)`").color(Color.BRIGHT_CYAN),
Text.of(query).color(Color.BRIGHT_YELLOW),
Text.of("█").color(Color.BRIGHT_WHITE),
Text.of("': ").color(Color.BRIGHT_CYAN),
Text.of(result.isEmpty() ? "" : result).color(Color.WHITE)
);
return Box.of(searchPrompt).paddingX(1);
}
Text prompt = Text.of("❯ ").color(Color.BRIGHT_GREEN).bold();
Text content;
@ -621,6 +647,8 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
Renderable leftText;
if (ctrlCPending) {
leftText = Text.of("Press Ctrl-C again to exit").color(Color.BRIGHT_YELLOW);
} else if (historySearchMode) {
leftText = Text.of("Ctrl+R next · Enter select · Esc cancel").dimmed();
} else if (agentRunning.get()) {
leftText = Text.of("esc to interrupt").dimmed();
} else {
@ -725,6 +753,22 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
return;
}
// Ctrl+R: 进入历史搜索模式
if (key.ctrl() && "r".equals(input) && !agentRunning.get()) {
historySearchMode = true;
historySearchQuery = "";
historySearchResult = "";
historySearchIndex = -1;
setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
return;
}
// 历史搜索模式的输入处理
if (historySearchMode) {
handleHistorySearchInput(input, key, s);
return;
}
// AI 运行中时允许滚动和 Escape 中断
if (agentRunning.get()) {
if (key.escape()) {
@ -946,6 +990,56 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
else if (key.pageDown()) scroll(s, -10);
}
/** Ctrl+R 反向历史搜索输入处理 */
private void handleHistorySearchInput(String input, Key key, TuiState s) {
if (key.escape() || (key.ctrl() && "c".equals(input))) {
// Esc/Ctrl+C: 退出搜索,恢复原输入
historySearchMode = false;
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, false, ""));
} else if (key.return_()) {
// Enter: 选定搜索结果,放入输入框
String result = historySearchResult;
historySearchMode = false;
setState(new TuiState(result, s.messages, s.scrollOffset, false, ""));
} else if (key.ctrl() && "r".equals(input)) {
// 再次 Ctrl+R: 搜索下一个匹配(更旧的)
searchHistoryBackward(historySearchQuery, historySearchIndex - 1);
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, false, ""));
} else if (key.backspace()) {
// 退格: 缩短搜索词
if (!historySearchQuery.isEmpty()) {
historySearchQuery = historySearchQuery.substring(0, historySearchQuery.length() - 1);
searchHistoryBackward(historySearchQuery, inputHistory.size() - 1);
}
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, false, ""));
} else if (!input.isEmpty() && isPrintableInput(input, key)) {
// 输入字符: 追加到搜索词并搜索
historySearchQuery += input;
searchHistoryBackward(historySearchQuery, inputHistory.size() - 1);
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, false, ""));
}
}
/** 从指定位置向前搜索历史记录 */
private void searchHistoryBackward(String query, int startIdx) {
if (query.isEmpty()) {
historySearchResult = "";
historySearchIndex = -1;
return;
}
String lowerQuery = query.toLowerCase();
for (int i = Math.min(startIdx, inputHistory.size() - 1); i >= 0; i--) {
if (inputHistory.get(i).toLowerCase().contains(lowerQuery)) {
historySearchResult = inputHistory.get(i);
historySearchIndex = i;
return;
}
}
// 没找到 — 保留之前的结果(或清空)
historySearchResult = "";
historySearchIndex = -1;
}
/** Tab 自动补全处理 */
private void handleTabCompletion(TuiState s) {
if (!s.inputText.startsWith("/")) return;

Loading…
Cancel
Save