fix: correct fenced code block detection and add unit tests

- Fix false positive when cursor is between two code blocks: the old
  upward search couldn't distinguish closing fences from opening ones
- Fix nested fence support: closing fence must be >= opening fence
  length per CommonMark spec; shorter inner fences are now treated
  as content lines
- Extract detection logic to pure function detectCodeBlockFromLines
  in src/codeBlockDetect.ts for testability
- Add 37 unit tests covering the bug scenario, nested fences,
  unclosed blocks, all CodeBlockBehavior modes, and fence-line edges
This commit is contained in:
Moy 2026-05-26 21:17:59 +08:00
parent 096a74c0c3
commit ea87a27a88
7 changed files with 533 additions and 68 deletions

1
.gitignore vendored
View file

@ -30,3 +30,4 @@ easy-copy/
# env
.env
/.sisyphus

View file

@ -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
<details>
<summary>中文说明(点击展开)</summary>
### 🐛 Bug 修复
- **代码块检测误判**:修复了光标在两个代码块之间的标题行或段落时,被错误识别为「在代码块内」的问题。原实现向上搜索第一个 ` ``` ` 行时,无法区分开始围栏和结束围栏,导致上方代码块的结束围栏被当成开始围栏。
- **嵌套围栏支持**:检测逻辑现在正确处理不同长度的嵌套围栏(如 ```` ```` ```` 包裹 ` ``` `)。依照 CommonMark 规范,结束围栏长度必须 ≥ 开始围栏,较短的内层围栏视为内容行而非结束围栏。
### 🧪 测试
- 将核心检测逻辑提取为纯函数 `detectCodeBlockFromLines`(位于 `src/codeBlockDetect.ts`),使其可独立进行单元测试
- 新增 37 个单元测试,覆盖:问题复现场景、嵌套围栏、未闭合块、所有 `CodeBlockBehavior` 模式、围栏行边界情况
</details>
---
## [1.6.4] - 2026-05-15

View file

@ -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",

387
src/codeBlockDetect.test.ts Normal file
View file

@ -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 规则:结束围栏必须与开始围栏字符相同,且长度 ≥ 开始围栏。
// 因此 ```` 内的 ``` 是内容,不是结束围栏。
//
// 场景 A4 个反引号包裹一个完整的 3 反引号代码块
// 行0: ````markdown
// 行1: ```js
// 行2: console.log("hi")
// 行3: ```
// 行4: ````
//
// 光标在行2应识别为在外层块内行0~4match 包含行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````');
});
});
// 场景 B5 个反引号 + 外层嵌套,光标在外层块之外的普通行不误判
// 行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();
});
});

107
src/codeBlockDetect.ts Normal file
View file

@ -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 };
}

View file

@ -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);
}
/**

View file

@ -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"
}