hunkdiff 内容搜索的空白保留:从 less 式 `/` 查询到 n/N 重复的完整实现剖析
开发工具代码评审CLIAI 应用【免费下载链接】hunkReview-first terminal diff viewer for agentic coders项目地址https://gitcode.com/gh_mirrors/hu/hunk点击查看免费下载hunk 是面向 agent 化开发者的 review-first 终端 diff 查看器其内置扩展提供了一套less风格的内容搜索/打开状态行提示符、n/N在匹配项间前进/后退并渲染 diff 内高亮。本文聚焦一次针对搜索查询空白处理的修复——Preserve surrounding whitespace in content search queries when matching, repeating, and reopening the prompt变更记录从用户视角的输入体验到compileQuery的匹配语义、session.ts的查询状态保持再到search.test.ts/session.test.ts中的回归测试完整还原这一细节修复的实现原理与验收标准。读完本文你将掌握hunkdiff 内容搜索中空白字符首尾空格在匹配、重复搜索、重开提示符三个环节的完整生命周期理解为何trim只在判空时调用而绝不出现在匹配逻辑里以及这一约定如何被单元测试钉死。修复背景一次 patch 级行为变更本次修复记录在 .changeset/search-query-whitespace.mdPreserve surrounding whitespace in content search queries when matching, repeating, and reopening the prompt.它属于hunkdiff: patch级别的行为修正服务于同一批已发布的内容搜索功能/搜索、n/N步进、hunk.review.focusFilter /键位恢复见 .changeset/bundled-content-search.md。在此之前用户输入的查询会被裁剪后再参与匹配修复后首尾空白成为查询的一部分参与三件事matching匹配 foo 必须匹配到行文本中的 foo 而不是被裁剪成foo后误匹配foo或其他位置repeating重复搜索n/N重放当前查询时沿用完整原文含空白保证结果一致reopening the prompt重开提示符再按/时提示符预填的initial文本是上次的完整查询含空白方便用户微调后回车重搜。核心改动点一compileQuery中裁剪判空、原文匹配搜索原语位于 packages/hunk/src/extensions/default/ui/search/search.ts其中compileQuery第 79-125 行是整个匹配语义的枢纽export function compileQuery(query: string, mode: SearchMode): CompiledQuery { if (query.trim().length 0) { return { ok: false, error: empty search }; } const caseSensitive query ! query.toLowerCase(); if (mode regex) { try { const pattern new RegExp(query, caseSensitive ? g : gi); return { ok: true, locate: (line) { const ranges: MatchRange[] []; pattern.lastIndex 0; let found: RegExpExecArray | null; while ((found pattern.exec(line)) ! null) { // Give zero-width matches a visible character and advance past it // so the next match cannot overlap or loop at the same position. const end found.index Math.max(found[0].length, 1); ranges.push([found.index, end]); pattern.lastIndex end; } return ranges; }, }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } } const needle caseSensitive ? query : query.toLowerCase(); return { ok: true, locate: (line) { const text caseSensitive ? line : line.toLowerCase(); const ranges: MatchRange[] []; let index text.indexOf(needle); while (index ! -1) { const end index needle.length; ranges.push([index, end]); index text.indexOf(needle, end); } return ranges; }, }; }关键语义拆解query.trim().length 0只负责空查询判定全空白字符串如 被判定为empty search返回{ ok: false }。这是唯一一次调用trim——它决定的是能不能搜而不是搜什么。needle直接用原始query在 literal 模式下needle caseSensitive ? query : query.toLowerCase()首尾空格原样保留。 foo 的 needle 就是 foo 含空格。大小写智能判定同样发生在原始串上caseSensitive query ! query.toLowerCase()——只要查询中出现任意大写字符整个查询就变为大小写敏感这是less -I、vim、ripgrep 用户熟悉的约定。regex 模式同样使用原始querynew RegExp(query, ...)因此 foo 作为正则会匹配行中带首尾空格的子串零宽匹配如空串的locate会被推进一个字符以避免重叠死循环测试用例见 search.test.ts。匹配边界的回归测试search.test.ts 为两种模式分别钉死了保留首尾空白与智能大小写的语义test.each([literal, regex] as const)( %s preserves surrounding whitespace and smart case, (mode) { const compiled compileQuery( foo , mode); const upper compileQuery( Foo , mode); if (!compiled.ok || !upper.ok) throw new Error(query should compile); expect(compiled.locate(foo)).toEqual([]); expect(compiled.locate(foo )).toEqual([]); expect(compiled.locate( foo)).toEqual([]); expect(compiled.locate( Foo )).toEqual([[0, 5]]); expect(upper.locate( foo )).toEqual([]); expect(upper.locate( Foo )).toEqual([[0, 5]]); }, );这份断言说明了修复后的全部行为 foo 在foo、foo 、 foo上均无匹配——空白是实打实的匹配条件位置也不能错 foo 在 Foo 上有[0, 5]的命中全文小写后 needle 为 foo Foo 含大写触发大小写敏感 foo 不命中、 Foo 命中。同时compileQuery( , literal).ok仍为falsesearch.test.ts确认判空逻辑未被破坏。核心改动点二session.ts中原文即状态匹配语义之外查询状态由 packages/hunk/src/extensions/default/ui/search/session.ts 中的createSearchSession管理。它的search(raw, files, position)方法第 127-141 行直接以用户输入的原始字符串作为会话状态search(raw, files, position) { const compiled compileQuery(raw, options.mode); if (!compiled.ok) { // A bad query never clobbers a working one: the previous search stays // repeatable with n. return { kind: invalid-query, query: raw, error: compiled.error }; } query raw; corpus files; fileOrder buildFileOrder(files); targets findTargets(files, compiled.locate); current null; return move(forward, position); },要点query raw存入会话的是未裁剪的原文。这同时支撑了n/N重复repeat内部走adoptCorpus后用compileQuery(query, ...)重建目标见第 77-93 行和/重开提示符index.ts 用initial: session.query ?? 预填原文坏查询不覆盖好查询invalid-query分支不写query上一次成功的搜索仍可通过n重复totalgetter 返回当前语料上的目标 hunk 数第 124-126 行供状态栏显示[i/n]。会话层的回归测试session.test.ts 专门验证了尾部空白在预填、标记、语料重建三处都被保留test.each([literal, regex] as const)( %s keeps trailing whitespace for prompt prefill, marks, and corpus rebuilds, (mode) { const session createSearchSession({ mode }); const file createTestSearchFile(whitespace, spaces.ts, -0,0 1 \nfoo fooX); const replacement createTestSearchFile(whitespace, spaces.ts, -0,0 1 \nfooX); expect(session.search(foo , [file], nowhere)).toMatchObject({ kind: moved, total: 1 }); expect(session.query).toBe(foo ); expect(session.marksFor(file)).toEqual([ { side: new, line: 1, range: [0, 4], tone: current }, ]); expect(session.repeat(forward, [replacement], nowhere)).toEqual({ kind: no-matches, query: foo , }); expect(session.query).toBe(foo ); expect(session.marksFor(replacement)).toEqual([]); }, );断言逐条对应本次修复的三个环节session.query恒为foo ——会话状态、提示符预填的来源都是原文marksFor生成的标记 range 为[0, 4]——foo 在foo fooX上命中前 5 个字符含空格高亮当前命中语料替换为fooX后repeat返回no-matches且query仍为foo ——重复搜索同样按原文匹配若查询被裁剪成foo这轮repeat就会错误命中fooX从而暴露回归。用户视角的完整行为链路把两个改动点串起来一次带空白的搜索从按下/到n/N步进的完整链路为入口见 index.ts按下/触发search.find命令ctx.prompts.line({ prefix: /, placeholder: search diff, initial: session.query ?? })打开状态行提示符——initial预填上次查询的完整原文含空白输入 foo 并回车query.trim().length 0为假 →compileQuery( foo , literal)成功 →session.search存下原文 foo 并正向步进到第一个目标deliver调用performNavigation做revealLine命中行号存在时精确落地否则退化为selectHunk见 index.ts并请求ctx.highlights.refresh(search.matches)重算标记、写入状态行[i/n] path:line (k in hunk) • wrapped — quoted text格式逻辑见 session.ts按n/N触发session.repeat(forward/backward, ...)adoptCorpus在可见文件列表变更时用compileQuery(query, ...)重建目标列表会话级状态因此不持有 review 本身进程级单例也不受 review 生命周期影响随后move按stepToTarget做less式的严格步进——绝不会停在当前 hunk 上跑到末尾会回绕在提示符里把文本清空后回车query.trim().length 0命中判空分支index.ts会话clear()并清空标记与状态项退出搜索。与相邻变更的关系高亮每处命中、仅第一处为 current同批的 .changeset/search-repeated-matches.md 让collectFileMatchMarks标记一行内的所有非重叠命中但只有当前目标首行首处使用current色调search.ts。本次空白修复与它正交前者解决标记多少处后者解决按什么字符串匹配。查询与文件过滤提示符互不冲突文件过滤保留在 Tab 与菜单中hunk.review.focusFilter /可恢复且其提示符文本在内容重载时保持打开.changeset/filter-prompt-reload.md本次修复只作用于内容搜索提示符的预填不涉及过滤提示符。小结search-query-whitespace是一处小而关键的语义修复它的设计约束可以概括为三条原则trim只用于判空绝不进入匹配全空白查询被拒绝但任何非空查询的首尾空白都是 needle 的一部分状态即原文会话保存rawn/N重复与/重开提示符的预填都基于未裁剪查询保证三个环节行为一致用测试钉死约定search.test.ts与session.test.ts以test.each([literal, regex])双模式覆盖匹配边界、标记 range、语料重建后的重复搜索任何后续裁剪查询的改动都会立刻在测试中暴露。对于需要精确搜索含缩进、尾随空格文本例如 YAML 键值、代码中的对齐块的 diff 审阅场景这一行为意味着所见即所搜输入多少个空格就匹配多少个空格。赞分享开发工具代码评审CLIAI 应用【免费下载链接】hunkReview-first terminal diff viewer for agentic coders项目地址https://gitcode.com/gh_mirrors/hu/hunk点击查看免费下载相关推荐Flink Window Top-N 完整指南窗口内 Top-N 查询的语法、原理与实战Flink Window Top N 完整指南窗口内 Top N 查询的语法、原理与实战 导读 Window Top N窗口 Top N是 Flink S大数据流处理批处理数据工程GameDevMind 数据结构实战四叉树空间索引——从 O(n²) 暴力检测到 O(log n) 空间查询GameDevMind 数据结构实战四叉树空间索引——从 O n² 暴力检测到 O log n 空间查询 导读 本篇文章基于 GameDevMind 仓库中「文档教程知识库游戏开发Fuse.js搜索算法优化从O(n)到O(log n)的效率提升Fuse.js搜索算法优化从O n 到O log n 的效率提升 你是否曾为前端搜索功能的卡顿而烦恼当数据量超过1000条时普通模糊搜索算法的响应时间可能前端搜索引擎上一篇3分钟搞定对象存储权限管控从混乱到有序的ACL实战指南下一篇GitHub_Trending/de/developer-portfolios项目核心组件解析字母索引系统的实现与优化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

ClawHub 发布者搜索缺陷复现与句柄前缀检索修复验证

ClawHub 发布者搜索缺陷复现与句柄前缀检索修复验证

后端前端AI 技能AI 插件搜索引擎 【免费下载链接】clawhub Skill Plugin Registry for OpenClaw 项目地址: https://gitcode.com/gh_mirrors/mo/clawhub 点击查看 免费下载 listPublicPage 是 ClawHub(OpenClaw 的 Skill Plugin Registry)…

2026/9/25 5:35:28 阅读更多 →
Cyrus SASL 2.1.21源码编译与认证服务部署实践

Cyrus SASL 2.1.21源码编译与认证服务部署实践

简介:Cyrus SASL 2.1.21 是一套面向邮件服务场景的开源认证与安全层库,主要服务于 Postfix 等 MTA 的运维人员、邮件系统管理员以及需要对接 SMTP/IMAP/POP3 认证的开发者。它内置 PLAIN、CRAM-MD5、DIGEST-MD5 等多种安全机制,可有效防范中间…

2026/9/25 5:35:28 阅读更多 →
PTA 7-32 交换实数整数部分:C语言浮点数精度与类型转换避坑指南

PTA 7-32 交换实数整数部分:C语言浮点数精度与类型转换避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 5:35:28 阅读更多 →

最新新闻

OFDM频谱感知实战:10节点协作+循环平稳检测+历史谱图可视化

OFDM频谱感知实战:10节点协作+循环平稳检测+历史谱图可视化

简介:本资源是一套面向通信工程专业高年级本科生及无线认知网络研究者的OFDM信号协作频谱感知MATLAB仿真方案,聚焦于解决单节点在阴影与深度衰落场景下检测不可靠的问题,通过融合多节点感知结果提升频谱判断准确性。压缩包共6个文件&#xff…

2026/9/25 9:41:42 阅读更多 →
2026年AI大模型应用盘点:从通用对话到Coding Agent的15家主流工具实测

2026年AI大模型应用盘点:从通用对话到Coding Agent的15家主流工具实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 9:41:42 阅读更多 →
计算机网络简答题与论述题核心考点梳理:从TCP/IP到子网划分

计算机网络简答题与论述题核心考点梳理:从TCP/IP到子网划分

简介:计算机网络课程的简答题与论述题常考内容,集中整理进一份Word文档,面向高校学生、考研备考生及求职面试者备考使用。文档系统梳理了电路交换、分组交换与报文交换的优缺点,分组传输中传输、传播、排队等延迟的影响因素&#…

2026/9/25 9:41:42 阅读更多 →
从TMN框架到E300实战:传输网管入门核心知识梳理

从TMN框架到E300实战:传输网管入门核心知识梳理

简介:《中兴传输网管入门知识》是一份面向通信行业新手与传输网管初学者的入门教程,系统梳理电信管理网(TMN)核心概念及其在SDH传输网络中的落地方式。内容从TMN的引入背景、三大结构(功能结构、信息结构、物理结构&am…

2026/9/25 9:41:42 阅读更多 →
Atlas 300V 24G部署YOLO全流程:昇腾推理卡环境搭建与优化

Atlas 300V 24G部署YOLO全流程:昇腾推理卡环境搭建与优化

1. Atlas 300V 24G到底是一张什么卡如果你也是被"atlas部署yolo"这个词带进来的,那你大概率跟我一样,手头或公司机房里躺着一张Atlas 300V 24G,想赶紧把YOLO跑起来,结果一查资料各种术语铺过来,头都大了。先…

2026/9/25 9:41:42 阅读更多 →
Linux服务器SSH连接与GPU开发环境实操指南

Linux服务器SSH连接与GPU开发环境实操指南

1. 项目概述:这不是“连服务器”,而是重建你和算力之间的信任链 “手把手教你如何连上实验室的服务器”——这句话在研究生新生群里刷屏的频率,几乎和开学季的快递单号一样高。但真正点开教程的人,十有八九卡在第二步&#xff1a…

2026/9/25 9:40:41 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →