
开发工具代码评审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),仅供参考