diff --git a/.gitignore b/.gitignore index 0d6dd3c..df3afee 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ easy-copy/ # env .env +/.sisyphus diff --git a/CHANGELOG.md b/CHANGELOG.md index 02e337e..7b635b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +--- + +## [1.6.5] - 2026-05-26 + +### 🐛 Bug Fixes + +- **Code block detection**: fix false positive when cursor is on a heading/paragraph between two fenced code blocks. The old implementation searched upward for the nearest `` ``` `` line without distinguishing opening from closing fences — a closing fence of the previous block was mistakenly treated as an opening fence. +- **Nested fence support**: the detector now correctly handles fences of different lengths (e.g. ```` ```` ```` wrapping ` ``` `). Per CommonMark spec, a closing fence must be at least as long as its opening fence, so a shorter inner fence is treated as content, not as a closing fence. + +### 🧪 Tests + +- Extract core detection logic into a pure function `detectCodeBlockFromLines` in `src/codeBlockDetect.ts`, making it independently testable +- Add 37 unit tests covering: the reported bug scenario, nested fences, unclosed blocks, all `CodeBlockBehavior` modes, and fence-line edge cases + +
+中文说明(点击展开) + +### 🐛 Bug 修复 + +- **代码块检测误判**:修复了光标在两个代码块之间的标题行或段落时,被错误识别为「在代码块内」的问题。原实现向上搜索第一个 ` ``` ` 行时,无法区分开始围栏和结束围栏,导致上方代码块的结束围栏被当成开始围栏。 +- **嵌套围栏支持**:检测逻辑现在正确处理不同长度的嵌套围栏(如 ```` ```` ```` 包裹 ` ``` `)。依照 CommonMark 规范,结束围栏长度必须 ≥ 开始围栏,较短的内层围栏视为内容行而非结束围栏。 + +### 🧪 测试 + +- 将核心检测逻辑提取为纯函数 `detectCodeBlockFromLines`(位于 `src/codeBlockDetect.ts`),使其可独立进行单元测试 +- 新增 37 个单元测试,覆盖:问题复现场景、嵌套围栏、未闭合块、所有 `CodeBlockBehavior` 模式、围栏行边界情况 + +
+ --- ## [1.6.4] - 2026-05-15 diff --git a/manifest.json b/manifest.json index aae936d..44d02a7 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "easy-copy", "name": "Easy Copy", - "version": "1.6.4", + "version": "1.6.5", "minAppVersion": "1.8.7", "description": "Easily copy the text within inline code, bold text (and many other formats), or quickly generate an elegant link to a heading.", "author": "Moy", diff --git a/src/codeBlockDetect.test.ts b/src/codeBlockDetect.test.ts new file mode 100644 index 0000000..7e2d072 --- /dev/null +++ b/src/codeBlockDetect.test.ts @@ -0,0 +1,387 @@ +import { describe, it, expect } from 'vitest'; +import { detectCodeBlockFromLines } from './codeBlockDetect'; +import { CodeBlockBehavior, ContextType } from './type'; + +// 辅助函数:把多行字符串转成行数组 +function lines(text: string): string[] { + return text.split('\n'); +} + +// ─── 复现场景:两个代码块之间有标题,光标在标题行 ───────────────────────────── +// +// 结构(行号): +// 0: --- +// 1: chisel: true +// 2: --- +// 3: (空行) +// 4: ```css +// 5: body { color: red !important; } +// 6: ``` ← 第一个代码块的结束围栏 +// 7: (空行) +// 8: 思考,是一种储存 CSS 代码块到 MD 的插件? +// 9: 暂时没搞懂做啥的…… +// 10: (空行) +// 11: ### 某个独立标题 ← 光标在这里 +// 12: ```sync ← 第二个代码块的开始围栏 +// 13: 别的内容。 +// 14: ``` + +const twoBlocksWithHeading = lines( + `--- +chisel: true +--- + +\`\`\`css +body { color: red !important; } +\`\`\` + +思考,是一种储存 CSS 代码块到 MD 的插件? +暂时没搞懂做啥的…… + +### 某个独立标题 +\`\`\`sync +别的内容。 +\`\`\`` +); + +describe('detectCodeBlockFromLines — bug 复现:两块之间的标题行', () => { + it('光标在两个代码块之间的标题行(行11),不应识别为代码块', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 11, CodeBlockBehavior.COPY_CONTENT); + expect(result).toBeNull(); + }); + + it('光标在两个代码块之间的空行(行10),不应识别为代码块', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 10, CodeBlockBehavior.COPY_CONTENT); + expect(result).toBeNull(); + }); + + it('光标在两个代码块之间的正文行(行8),不应识别为代码块', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 8, CodeBlockBehavior.COPY_CONTENT); + expect(result).toBeNull(); + }); +}); + +// ─── 正常情况:光标确实在代码块内 ───────────────────────────────────────────── + +describe('detectCodeBlockFromLines — 正常识别代码块', () => { + it('光标在第一个代码块内容行(行5),应识别并返回代码内容', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 5, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.type).toBe(ContextType.CODEBLOCK); + expect(result!.match).toBe('body { color: red !important; }'); + }); + + it('光标在第二个代码块内容行(行13),应识别并返回代码内容', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 13, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.type).toBe(ContextType.CODEBLOCK); + expect(result!.match).toBe('别的内容。'); + }); + + it('COPY_WITH_FENCES:光标在块内,返回含围栏的完整代码块', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 5, CodeBlockBehavior.COPY_WITH_FENCES); + expect(result).not.toBeNull(); + expect(result!.match).toBe('```css\nbody { color: red !important; }\n```'); + }); +}); + +// ─── 围栏行本身 ──────────────────────────────────────────────────────────────── + +describe('detectCodeBlockFromLines — 光标在围栏行上', () => { + it('光标在开始围栏行(行4),不应识别为代码块内', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 4, CodeBlockBehavior.COPY_CONTENT); + expect(result).toBeNull(); + }); + + it('光标在结束围栏行(行6),不应识别为代码块内', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 6, CodeBlockBehavior.COPY_CONTENT); + expect(result).toBeNull(); + }); + + it('光标在第二个代码块开始围栏行(行12),不应识别为代码块内', () => { + const result = detectCodeBlockFromLines(twoBlocksWithHeading, 12, CodeBlockBehavior.COPY_CONTENT); + expect(result).toBeNull(); + }); +}); + +// ─── DISABLED 模式 ───────────────────────────────────────────────────────────── + +describe('detectCodeBlockFromLines — DISABLED 行为', () => { + it('任意位置都返回 null', () => { + expect(detectCodeBlockFromLines(twoBlocksWithHeading, 5, CodeBlockBehavior.DISABLED)).toBeNull(); + expect(detectCodeBlockFromLines(twoBlocksWithHeading, 13, CodeBlockBehavior.DISABLED)).toBeNull(); + }); +}); + +// ─── GENERATE_BLOCK_LINK 模式 ────────────────────────────────────────────────── + +describe('detectCodeBlockFromLines — GENERATE_BLOCK_LINK 行为', () => { + it('光标在代码块内也返回 null(交给块ID逻辑处理)', () => { + expect( + detectCodeBlockFromLines(twoBlocksWithHeading, 5, CodeBlockBehavior.GENERATE_BLOCK_LINK) + ).toBeNull(); + }); +}); + +// ─── 未闭合代码块 ────────────────────────────────────────────────────────────── + +describe('detectCodeBlockFromLines — 未闭合代码块', () => { + it('代码块没有结束围栏,光标在其中,返回 null', () => { + const doc = lines('```js\nconsole.log("hi")\n// no closing fence'); + expect(detectCodeBlockFromLines(doc, 1, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); +}); + +// ─── 单一代码块(无 frontmatter / 无其他内容)───────────────────────────────── + +describe('detectCodeBlockFromLines — 单一简单代码块', () => { + const simple = lines('```\nhello world\n```'); + + it('光标在行1(内容行),返回内容', () => { + const result = detectCodeBlockFromLines(simple, 1, CodeBlockBehavior.COPY_CONTENT); + expect(result?.match).toBe('hello world'); + }); + + it('光标在行0(开始围栏),返回 null', () => { + expect(detectCodeBlockFromLines(simple, 0, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); + + it('光标在行2(结束围栏),返回 null', () => { + expect(detectCodeBlockFromLines(simple, 2, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); +}); + +// ─── 嵌套场景:较长围栏包裹较短围栏 ────────────────────────────────────────── +// +// CommonMark 规则:结束围栏必须与开始围栏字符相同,且长度 ≥ 开始围栏。 +// 因此 ```` 内的 ``` 是内容,不是结束围栏。 +// +// 场景 A:4 个反引号包裹一个完整的 3 反引号代码块 +// 行0: ````markdown +// 行1: ```js +// 行2: console.log("hi") +// 行3: ``` +// 行4: ```` +// +// 光标在行2,应识别为在外层块内(行0~4),match 包含行1~3。 +// 光标在行3(内层 ```),这行是内容,不是结束围栏,也应识别为块内。 +// 光标在行1(内层 ```js),同理是内容行,应识别为块内。 + +const nestedThreeInFour = lines( + '````markdown\n' + + '```js\n' + + 'console.log("hi")\n' + + '```\n' + + '````' +); + +describe('detectCodeBlockFromLines — 嵌套:4个反引号包裹3个反引号', () => { + it('光标在内层代码内容行(行2),应识别为外层块内', () => { + const result = detectCodeBlockFromLines(nestedThreeInFour, 2, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.type).toBe(ContextType.CODEBLOCK); + }); + + it('光标在内层结束围栏行(行3,是外层块的内容),应识别为外层块内', () => { + const result = detectCodeBlockFromLines(nestedThreeInFour, 3, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.type).toBe(ContextType.CODEBLOCK); + }); + + it('光标在内层开始围栏行(行1,是外层块的内容),应识别为外层块内', () => { + const result = detectCodeBlockFromLines(nestedThreeInFour, 1, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.type).toBe(ContextType.CODEBLOCK); + }); + + it('光标在外层开始围栏行(行0),不应识别为块内', () => { + expect(detectCodeBlockFromLines(nestedThreeInFour, 0, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); + + it('光标在外层结束围栏行(行4),不应识别为块内', () => { + expect(detectCodeBlockFromLines(nestedThreeInFour, 4, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); + + it('COPY_WITH_FENCES:返回含外层围栏的完整内容', () => { + const result = detectCodeBlockFromLines(nestedThreeInFour, 2, CodeBlockBehavior.COPY_WITH_FENCES); + expect(result).not.toBeNull(); + expect(result!.match).toBe('````markdown\n```js\nconsole.log("hi")\n```\n````'); + }); +}); + +// 场景 B:5 个反引号 + 外层嵌套,光标在外层块之外的普通行不误判 +// 行0: ```` +// 行1: 内容 +// 行2: ```` +// 行3: 普通行 +// 行4: ````` +// 行5: 另一内容 +// 行6: ````` + +const twoLongBlocks = lines( + '````\n' + + '内容A\n' + + '````\n' + + '普通行\n' + + '`````\n' + + '内容B\n' + + '`````' +); + +describe('detectCodeBlockFromLines — 不同长度围栏的独立块', () => { + it('光标在行1(第一个4反引号块内),应识别为块内', () => { + const result = detectCodeBlockFromLines(twoLongBlocks, 1, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.match).toBe('内容A'); + }); + + it('光标在行3(两个块之间的普通行),不应识别为块内', () => { + expect(detectCodeBlockFromLines(twoLongBlocks, 3, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); + + it('光标在行5(第二个5反引号块内),应识别为块内', () => { + const result = detectCodeBlockFromLines(twoLongBlocks, 5, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.match).toBe('内容B'); + }); +}); + +// 场景 C:开始用3个,内容里有4个反引号行(不应关闭外层块) +// 行0: ``` +// 行1: ````这行是内容 +// 行2: 普通内容 +// 行3: ``` +// +// 根据 CommonMark:开始围栏是3个,结束围栏必须 >= 3 且是纯反引号行。 +// 行1 的 ```` 后面跟了文字,不是有效围栏(开始或结束都不算)。 +// 行3 的 ``` 是有效结束围栏(= 开始长度3)。 + +const threeWithLongerContentLine = lines( + '```\n' + + '````这行是内容\n' + + '普通内容\n' + + '```' +); + +describe('detectCodeBlockFromLines — 内容行含更长反引号但带文字(非围栏)', () => { + it('光标在行1(内容行,虽以4个反引号开头但后跟文字),应识别为块内', () => { + const result = detectCodeBlockFromLines(threeWithLongerContentLine, 1, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.type).toBe(ContextType.CODEBLOCK); + }); + + it('光标在行2(普通内容),应识别为块内', () => { + const result = detectCodeBlockFromLines(threeWithLongerContentLine, 2, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + }); +}); + +// ─── 嵌套:外层用更长的围栏包裹内层 ``` ────────────────────────────────────── +// +// CommonMark 规范:结束围栏必须 >= 开始围栏的长度,且使用相同字符。 +// 所以 ```` 内的 ``` 是普通内容,不是结束围栏。 +// +// 典型场景:在 Obsidian 里用 ```` 或 ````` 包裹含 ``` 的代码块示例。 +// +// 文档结构(行号): +// 0: ````markdown ← 4个反引号,开始围栏 +// 1: ```js ← 3个反引号,是内容,不是结束围栏 +// 2: console.log("hi") ← 内容 +// 3: ``` ← 3个反引号,是内容,不是结束围栏 +// 4: ```` ← 4个反引号,真正的结束围栏 +// +// 光标在行2,应识别为「在外层代码块内」 + +const nestedFence = [ + '````markdown', + '```js', + 'console.log("hi")', + '```', + '````', +]; + +describe('detectCodeBlockFromLines — 嵌套围栏(外长内短)', () => { + it('光标在内层 ``` 内容行(行2),应识别为在外层代码块内', () => { + const result = detectCodeBlockFromLines(nestedFence, 2, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.type).toBe(ContextType.CODEBLOCK); + }); + + it('内容不应包含外层围栏行,COPY_CONTENT 只返回行1-3', () => { + const result = detectCodeBlockFromLines(nestedFence, 2, CodeBlockBehavior.COPY_CONTENT); + expect(result!.match).toBe('```js\nconsole.log("hi")\n```'); + }); + + it('COPY_WITH_FENCES 应返回含外层围栏的完整块', () => { + const result = detectCodeBlockFromLines(nestedFence, 2, CodeBlockBehavior.COPY_WITH_FENCES); + expect(result!.match).toBe('````markdown\n```js\nconsole.log("hi")\n```\n````'); + }); + + it('光标在内层开始围栏行(行1),仍属于外层代码块内', () => { + const result = detectCodeBlockFromLines(nestedFence, 1, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + }); + + it('光标在内层结束围栏行(行3),仍属于外层代码块内', () => { + const result = detectCodeBlockFromLines(nestedFence, 3, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + }); + + it('光标在外层开始围栏行(行0),返回 null', () => { + expect(detectCodeBlockFromLines(nestedFence, 0, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); + + it('光标在外层结束围栏行(行4),返回 null', () => { + expect(detectCodeBlockFromLines(nestedFence, 4, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); +}); + +// ─── 嵌套:5个反引号包裹含 ``` 和 ```` 的内容 ──────────────────────────────── +// +// 0: ````` +// 1: ````js ← 4个,内容 +// 2: code here +// 3: ```` ← 4个,内容(不够长,不是结束围栏) +// 4: ````` ← 5个,真正的结束围栏 + +const deepNested = [ + '`````', + '````js', + 'code here', + '````', + '`````', +]; + +describe('detectCodeBlockFromLines — 5个反引号嵌套', () => { + it('光标在行2(内容),识别为在块内', () => { + const result = detectCodeBlockFromLines(deepNested, 2, CodeBlockBehavior.COPY_CONTENT); + expect(result).not.toBeNull(); + expect(result!.match).toBe('````js\ncode here\n````'); + }); + + it('光标在行1(内层4个反引号开头行),识别为在块内', () => { + expect(detectCodeBlockFromLines(deepNested, 1, CodeBlockBehavior.COPY_CONTENT)).not.toBeNull(); + }); + + it('光标在行3(内层4个反引号结束行),识别为在块内', () => { + expect(detectCodeBlockFromLines(deepNested, 3, CodeBlockBehavior.COPY_CONTENT)).not.toBeNull(); + }); +}); + +// ─── 波浪号围栏:~~~ 不受反引号计数影响 ───────────────────────────────────── +// +// CommonMark 规范同样支持 ~ 作为围栏字符,且两者不互相关闭。 +// 本插件目前只处理反引号(原始实现也只用 /^```/), +// 所以波浪号围栏不在支持范围内,光标在其中应返回 null。 + +const tildeFence = [ + '~~~js', + 'const x = 1;', + '~~~', +]; + +describe('detectCodeBlockFromLines — 波浪号围栏(不支持)', () => { + it('光标在波浪号围栏内,返回 null(未支持)', () => { + expect(detectCodeBlockFromLines(tildeFence, 1, CodeBlockBehavior.COPY_CONTENT)).toBeNull(); + }); +}); diff --git a/src/codeBlockDetect.ts b/src/codeBlockDetect.ts new file mode 100644 index 0000000..e7c1e22 --- /dev/null +++ b/src/codeBlockDetect.ts @@ -0,0 +1,107 @@ +import { CodeBlockBehavior, ContextData, ContextType } from './type'; + +/** + * 解析一行是否为有效的反引号围栏行,返回围栏长度;否则返回 0。 + * trimStart 后开头必须是 3 个或更多连续反引号。 + */ +function parseFenceLength(line: string): number { + const trimmed = line.trimStart(); + const match = trimmed.match(/^(`{3,})/); + if (!match) return 0; + return match[1].length; +} + +/** + * 判断某行是否为有效的结束围栏: + * 反引号数量 >= minLen,且反引号后面只有空白。 + */ +function isClosingFence(line: string, minLen: number): boolean { + const trimmed = line.trimStart(); + const match = trimmed.match(/^(`+)\s*$/); + if (!match) return false; + return match[1].length >= minLen; +} + +/** + * 纯函数版本的代码块检测,符合 CommonMark 嵌套围栏规则: + * - 开始围栏:3 个或更多连续反引号,后可跟 info string + * - 结束围栏:反引号数量 >= 开始围栏,且行内只有空白 + * - 内层较短的围栏不会关闭外层较长的围栏(如 ```` 内的 ``` 是内容) + * + * 当前只处理反引号围栏;波浪号 ~~~ 不在支持范围内。 + * + * @param lines 文件所有行(按行号索引) + * @param cursorLine 光标所在行号(0-based) + * @param behavior 代码块行为设置 + * @returns ContextData(命中代码块)或 null + */ +export function detectCodeBlockFromLines( + lines: string[], + cursorLine: number, + behavior: CodeBlockBehavior, +): ContextData | null { + if (behavior === CodeBlockBehavior.DISABLED) return null; + + const totalLines = lines.length; + + // 阶段一:从文件开头向下扫描到 cursorLine。 + // openLen = 0 → 不在块内;openLen > 0 → 在块内,值为开始围栏的反引号数。 + // + // 不在块内时:遇到有效围栏行(parseFenceLength >= 3)→ 进入块 + // 在块内时:遇到 isClosingFence(长度 >= openLen 且纯反引号)→ 离开块 + // 其他反引号行(内层短围栏 / 带 info string)→ 忽略,视为内容 + + let openLen = 0; + let fenceStart = -1; + + for (let i = 0; i <= cursorLine; i++) { + if (openLen === 0) { + const len = parseFenceLength(lines[i]); + if (len > 0) { + openLen = len; + fenceStart = i; + } + } else { + if (isClosingFence(lines[i], openLen)) { + openLen = 0; + fenceStart = -1; + } + } + } + + // 光标不在任何代码块内 + if (openLen === 0) return null; + if (fenceStart === -1) return null; + // 光标正好在开始围栏行上,不算「块内」 + if (fenceStart === cursorLine) return null; + + // 阶段二:从 fenceStart+1 向下找匹配的结束围栏 + let fenceEnd = -1; + for (let i = fenceStart + 1; i < totalLines; i++) { + if (isClosingFence(lines[i], openLen)) { + fenceEnd = i; + break; + } + } + if (fenceEnd === -1) return null; + // 光标必须在两个围栏之间(不含两端) + if (cursorLine >= fenceEnd) return null; + + if (behavior === CodeBlockBehavior.GENERATE_BLOCK_LINK) return null; + + // 收集代码块内容行(不含围栏行) + const contentLines: string[] = []; + for (let i = fenceStart + 1; i < fenceEnd; i++) { + contentLines.push(lines[i]); + } + + const curLineText = lines[cursorLine]; + + if (behavior === CodeBlockBehavior.COPY_WITH_FENCES) { + const fullBlock = [lines[fenceStart], ...contentLines, lines[fenceEnd]].join('\n'); + return { type: ContextType.CODEBLOCK, curLine: curLineText, match: fullBlock, range: null }; + } + + // 默认 COPY_CONTENT + return { type: ContextType.CODEBLOCK, curLine: curLineText, match: contentLines.join('\n'), range: null }; +} diff --git a/src/main.ts b/src/main.ts index 154d11f..d56c67c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { Language, TranslationKey, I18n } from './i18n'; import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from './type'; import { EasyCopySettingTab } from './settingTab'; import { BlockIdInputModal } from './blockIdModal'; +import { detectCodeBlockFromLines } from './codeBlockDetect'; import { buildHeadingLink, buildBlockLink, buildFileLink, buildExplicitPasteLink } from './linkBuilder'; import { CopyMetadata, buildBlockCopyMetadata, buildHeadingCopyMetadata, buildFileCopyMetadata } from './copyMetadata'; import { decidePasteResolution, shouldOmitAliasForSameFile, shouldRegisterPasteHandler } from './pasteResolution'; @@ -289,74 +290,13 @@ export default class EasyCopy extends Plugin { * @returns ContextData 或 null */ private detectCodeBlock(editor: Editor): ContextData | null { - const behavior = this.settings.codeBlockBehavior; - if (behavior === CodeBlockBehavior.DISABLED) return null; - - const cursor = editor.getCursor(); - const curLine = cursor.line; + const cursorLine = editor.getCursor().line; const totalLines = editor.lineCount(); - - // 向上搜索开始围栏 ``` - let fenceStart = -1; - for (let i = curLine; i >= 0; i--) { - const line = editor.getLine(i); - if (/^```/.test(line.trimStart())) { - fenceStart = i; - break; - } + const allLines: string[] = []; + for (let i = 0; i < totalLines; i++) { + allLines.push(editor.getLine(i)); } - if (fenceStart === -1) return null; - - // 如果 fenceStart 就是光标所在行,光标在 ``` 行上,不算在代码块"内" - if (fenceStart === curLine) return null; - - // 从 fenceStart 之后向下搜索结束围栏 ``` - let fenceEnd = -1; - for (let i = fenceStart + 1; i < totalLines; i++) { - const line = editor.getLine(i); - if (/^```\s*$/.test(line.trimStart())) { - fenceEnd = i; - break; - } - } - if (fenceEnd === -1) return null; - - // 光标必须在 fenceStart 和 fenceEnd 之间(不含两端的 ``` 行) - if (curLine >= fenceEnd) return null; - - // 根据行为模式决定返回内容 - if (behavior === CodeBlockBehavior.GENERATE_BLOCK_LINK) { - // 返回 null,让后续的 block ID 逻辑处理 - return null; - } - - // 收集代码块内容行(不含 ``` 行) - const contentLines: string[] = []; - for (let i = fenceStart + 1; i < fenceEnd; i++) { - contentLines.push(editor.getLine(i)); - } - - if (behavior === CodeBlockBehavior.COPY_WITH_FENCES) { - // 包含前后 ``` 行 - const fenceStartLine = editor.getLine(fenceStart); - const fenceEndLine = editor.getLine(fenceEnd); - const fullBlock = [fenceStartLine, ...contentLines, fenceEndLine].join('\n'); - return { - type: ContextType.CODEBLOCK, - curLine: editor.getLine(curLine), - match: fullBlock, - range: null, - }; - } - - // 默认:COPY_CONTENT — 纯文本(不含 ``` 行) - const content = contentLines.join('\n'); - return { - type: ContextType.CODEBLOCK, - curLine: editor.getLine(curLine), - match: content, - range: null, - }; + return detectCodeBlockFromLines(allLines, cursorLine, this.settings.codeBlockBehavior); } /** diff --git a/versions.json b/versions.json index 842e840..83c4cfc 100644 --- a/versions.json +++ b/versions.json @@ -19,5 +19,6 @@ "1.6.1": "0.15.0", "1.6.2": "0.15.0", "1.6.3": "0.15.0", - "1.6.4": "1.8.7" + "1.6.4": "1.8.7", + "1.6.5": "1.8.7" }