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>
This commit is contained in:
@@ -111,6 +111,12 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
private volatile List<String> commandSuggestions = List.of(); // 当前匹配的命令名列表
|
private volatile List<String> commandSuggestions = List.of(); // 当前匹配的命令名列表
|
||||||
private volatile int tabCompletionIndex = -1; // 当前 Tab 循环索引,-1 表示未开始
|
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) */
|
/** 首次用户输入回调(用于 conversation summary) */
|
||||||
private Consumer<String> onFirstUserInput;
|
private Consumer<String> onFirstUserInput;
|
||||||
|
|
||||||
@@ -185,6 +191,12 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
// 权限选择模式:光标在选中选项的 ❯ 位置
|
// 权限选择模式:光标在选中选项的 ❯ 位置
|
||||||
int permCursorRow = h - 2 - (snapPermOptions.size() - snapPermSelected);
|
int permCursorRow = h - 2 - (snapPermOptions.size() - snapPermSelected);
|
||||||
setCursorPosition(Math.max(0, permCursorRow), 3);
|
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 {
|
} else {
|
||||||
// 正常模式:光标隐藏在块光标 █ 的位置
|
// 正常模式:光标隐藏在块光标 █ 的位置
|
||||||
int cursorRow = Math.max(0, h - 3);
|
int cursorRow = Math.max(0, h - 3);
|
||||||
@@ -511,6 +523,20 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
return renderPermissionSelectArea(s, w);
|
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 prompt = Text.of("❯ ").color(Color.BRIGHT_GREEN).bold();
|
||||||
Text content;
|
Text content;
|
||||||
|
|
||||||
@@ -621,6 +647,8 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
Renderable leftText;
|
Renderable leftText;
|
||||||
if (ctrlCPending) {
|
if (ctrlCPending) {
|
||||||
leftText = Text.of("Press Ctrl-C again to exit").color(Color.BRIGHT_YELLOW);
|
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()) {
|
} else if (agentRunning.get()) {
|
||||||
leftText = Text.of("esc to interrupt").dimmed();
|
leftText = Text.of("esc to interrupt").dimmed();
|
||||||
} else {
|
} else {
|
||||||
@@ -725,6 +753,22 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
return;
|
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 中断
|
// AI 运行中时允许滚动和 Escape 中断
|
||||||
if (agentRunning.get()) {
|
if (agentRunning.get()) {
|
||||||
if (key.escape()) {
|
if (key.escape()) {
|
||||||
@@ -946,6 +990,56 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
else if (key.pageDown()) scroll(s, -10);
|
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 自动补全处理 */
|
/** Tab 自动补全处理 */
|
||||||
private void handleTabCompletion(TuiState s) {
|
private void handleTabCompletion(TuiState s) {
|
||||||
if (!s.inputText.startsWith("/")) return;
|
if (!s.inputText.startsWith("/")) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user