Improve code block function

This commit is contained in:
2025-06-18 21:18:49 +08:00
parent cce9cf7e92
commit 9204315c7b
19 changed files with 1618 additions and 136 deletions

View File

@@ -3,7 +3,7 @@
*/
import { ViewPlugin, EditorView, Decoration, WidgetType, layer, RectangleMarker } from "@codemirror/view";
import { StateField, RangeSetBuilder } from "@codemirror/state";
import { StateField, RangeSetBuilder, EditorState } from "@codemirror/state";
import { blockState } from "./state";
/**
@@ -179,16 +179,65 @@ const blockLayer = layer({
/**
* 防止第一个块被删除
* 使用 changeFilter 来保护第一个块分隔符不被删除
*/
const preventFirstBlockFromBeingDeleted = EditorView.updateListener.of((update: any) => {
// 暂时简化实现,后续可以完善
const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any) => {
const protect: number[] = [];
// 获取块状态
const blocks = tr.startState.field(blockState);
if (blocks && blocks.length > 0) {
const firstBlock = blocks[0];
// 保护第一个块的分隔符区域
if (firstBlock && firstBlock.delimiter) {
protect.push(firstBlock.delimiter.from, firstBlock.delimiter.to);
}
}
// 如果是搜索替换操作,保护所有块分隔符
if (tr.annotations.some((a: any) => a.value === "input.replace" || a.value === "input.replace.all")) {
blocks.forEach((block: any) => {
if (block.delimiter) {
protect.push(block.delimiter.from, block.delimiter.to);
}
});
}
// 返回保护范围数组,如果没有需要保护的范围则返回 false
return protect.length > 0 ? protect : false;
});
/**
* 防止选择在第一个块之前
* 使用 transactionFilter 来确保选择不会在第一个块之前
*/
const preventSelectionBeforeFirstBlock = EditorView.updateListener.of((update: any) => {
// 暂时简化实现,后续可以完善
const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr: any) => {
// 获取块状态
const blocks = tr.startState.field(blockState);
if (!blocks || blocks.length === 0) {
return tr;
}
const firstBlock = blocks[0];
if (!firstBlock || !firstBlock.delimiter) {
return tr;
}
const firstBlockDelimiterSize = firstBlock.delimiter.to;
// 检查选择范围,如果在第一个块之前,则调整到第一个块的内容开始位置
if (tr.selection) {
tr.selection.ranges.forEach((range: any) => {
if (range && range.from < firstBlockDelimiterSize) {
range.from = firstBlockDelimiterSize;
}
if (range && range.to < firstBlockDelimiterSize) {
range.to = firstBlockDelimiterSize;
}
});
}
return tr;
});
/**

View File

@@ -0,0 +1,134 @@
/**
* 删除行功能
* 处理代码块边界
*/
import { EditorSelection, SelectionRange } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { getNoteBlockFromPos } from "./state";
interface LineBlock {
from: number;
to: number;
ranges: SelectionRange[];
}
/**
* 更新选择范围
*/
function updateSel(sel: EditorSelection, by: (range: SelectionRange) => SelectionRange): EditorSelection {
return EditorSelection.create(sel.ranges.map(by), sel.mainIndex);
}
/**
* 获取选中的行块
*/
function selectedLineBlocks(state: any): LineBlock[] {
let blocks: LineBlock[] = [];
let upto = -1;
for (let range of state.selection.ranges) {
let startLine = state.doc.lineAt(range.from);
let endLine = state.doc.lineAt(range.to);
if (!range.empty && range.to == endLine.from) {
endLine = state.doc.lineAt(range.to - 1);
}
if (upto >= startLine.number) {
let prev = blocks[blocks.length - 1];
prev.to = endLine.to;
prev.ranges.push(range);
} else {
blocks.push({
from: startLine.from,
to: endLine.to,
ranges: [range]
});
}
upto = endLine.number + 1;
}
return blocks;
}
/**
* 删除行命令
*/
export const deleteLine = (view: EditorView): boolean => {
if (view.state.readOnly) {
return false;
}
const { state } = view;
const selectedLines = selectedLineBlocks(state);
const changes = state.changes(selectedLines.map(({ from, to }) => {
const block = getNoteBlockFromPos(state, from);
// 如果不是删除整个代码块,需要调整删除范围
if (block && (from !== block.content.from || to !== block.content.to)) {
if (from > 0) {
from--;
} else if (to < state.doc.length) {
to++;
}
}
return { from, to };
}));
const selection = updateSel(
state.selection,
range => view.moveVertically(range, true)
).map(changes);
view.dispatch({
changes,
selection,
scrollIntoView: true,
userEvent: "delete.line"
});
return true;
};
/**
* 删除行命令函数,用于键盘映射
*/
export const deleteLineCommand = ({ state, dispatch }: { state: any; dispatch: any }) => {
if (state.readOnly) {
return false;
}
const selectedLines = selectedLineBlocks(state);
const changes = state.changes(selectedLines.map(({ from, to }: LineBlock) => {
const block = getNoteBlockFromPos(state, from);
// 如果不是删除整个代码块,需要调整删除范围
if (block && (from !== block.content.from || to !== block.content.to)) {
if (from > 0) {
from--;
} else if (to < state.doc.length) {
to++;
}
}
return { from, to };
}));
const selection = updateSel(
state.selection,
range => EditorSelection.cursor(range.from)
).map(changes);
dispatch(state.update({
changes,
selection,
scrollIntoView: true,
userEvent: "delete.line"
}));
return true;
};

View File

@@ -1,9 +1,19 @@
/**
* CodeBlock 扩展主入口
*
* 配置说明:
* - showBackground: 控制是否显示代码块的背景色区分
* - enableAutoDetection: 控制是否启用内容的语言自动检测功能
* - defaultLanguage: 新建代码块时使用的默认语言(也是自动检测的回退语言)
* - defaultAutoDetect: 新建代码块时是否默认添加-a标记启用自动检测
*
* 注意defaultLanguage 和 defaultAutoDetect 是配合使用的:
* - 如果 defaultAutoDetect=true新建块会是 ∞∞∞javascript-a会根据内容自动检测语言
* - 如果 defaultAutoDetect=false新建块会是 ∞∞∞javascript固定使用指定语言
*/
import {Extension} from '@codemirror/state';
import {EditorView, keymap} from '@codemirror/view';
import {keymap} from '@codemirror/view';
// 导入核心模块
import {blockState} from './state';
@@ -11,6 +21,11 @@ import {getBlockDecorationExtensions} from './decorations';
import * as commands from './commands';
import {selectAll, getBlockSelectExtensions} from './selectAll';
import {getCopyPasteExtensions, getCopyPasteKeymap} from './copyPaste';
import {deleteLineCommand} from './deleteLine';
import {moveLineUp, moveLineDown} from './moveLines';
import {transposeChars} from './transposeChars';
import {getCodeBlockLanguageExtension} from './lang-parser';
import {createLanguageDetection} from './language-detection';
import {EditorOptions, SupportedLanguage} from './types';
import {lineNumbers} from '@codemirror/view';
import './styles.css'
@@ -19,16 +34,17 @@ import './styles.css'
* 代码块扩展配置选项
*/
export interface CodeBlockOptions {
// 视觉选项
/** 是否显示块背景色 */
showBackground?: boolean;
// 功能选项
/** 是否启用语言自动检测功能 */
enableAutoDetection?: boolean;
/** 新建块时的默认语言 */
defaultLanguage?: SupportedLanguage;
// 编辑器选项
defaultBlockToken?: string;
defaultBlockAutoDetect?: boolean;
/** 新建块时是否默认启用自动检测(添加-a标记 */
defaultAutoDetect?: boolean;
}
/**
@@ -75,8 +91,6 @@ const blockLineNumbers = lineNumbers({
}
});
/**
* 创建代码块扩展
*/
@@ -85,13 +99,13 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
showBackground = true,
enableAutoDetection = true,
defaultLanguage = 'text',
defaultBlockToken = 'text',
defaultBlockAutoDetect = false,
defaultAutoDetect = true,
} = options;
// 将简化的配置转换为内部使用的EditorOptions
const editorOptions: EditorOptions = {
defaultBlockToken,
defaultBlockAutoDetect,
defaultBlockToken: defaultLanguage,
defaultBlockAutoDetect: defaultAutoDetect,
};
const extensions: Extension[] = [
@@ -101,6 +115,16 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
// 块内行号
blockLineNumbers,
// 语言解析支持
...getCodeBlockLanguageExtension(),
// 语言自动检测(如果启用)
...(enableAutoDetection ? [createLanguageDetection({
defaultLanguage: defaultLanguage,
confidenceThreshold: 0.3,
minContentLength: 8,
})] : []),
// 视觉装饰系统
...getBlockDecorationExtensions({
showBackground
@@ -112,19 +136,6 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
// 复制粘贴功能
...getCopyPasteExtensions(),
// 主题样式
EditorView.theme({
'&': {
fontSize: '14px'
},
'.cm-content': {
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", consolas, monospace'
},
'.cm-focused': {
outline: 'none'
}
}),
// 键盘映射
keymap.of([
// 复制粘贴命令
@@ -191,6 +202,28 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
run: commands.moveCurrentBlockDown,
preventDefault: true
},
// 行编辑命令
{
key: 'Mod-Shift-k', // 删除行
run: deleteLineCommand,
preventDefault: true
},
{
key: 'Alt-ArrowUp', // 向上移动行
run: moveLineUp,
preventDefault: true
},
{
key: 'Alt-ArrowDown', // 向下移动行
run: moveLineDown,
preventDefault: true
},
{
key: 'Ctrl-t', // 字符转置
run: transposeChars,
preventDefault: true
},
])
];
@@ -241,6 +274,44 @@ export {
getCopyPasteKeymap
} from './copyPaste';
// 删除行功能
export {
deleteLine,
deleteLineCommand
} from './deleteLine';
// 移动行功能
export {
moveLineUp,
moveLineDown
} from './moveLines';
// 字符转置功能
export {
transposeChars
} from './transposeChars';
// 语言解析器
export {
getCodeBlockLanguageExtension,
getLanguage,
getLanguageTokens,
languageMapping,
LanguageInfo,
LANGUAGES as PARSER_LANGUAGES
} from './lang-parser';
// 语言检测
export {
createLanguageDetection,
detectLanguage,
detectLanguages,
detectLanguageHeuristic,
getSupportedDetectionLanguages,
levenshteinDistance,
type LanguageDetectionResult
} from './language-detection';
// 行号相关
export { getBlockLineFromPos, blockLineNumbers };

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env node
/**
* 解析器构建脚本
* 使用 lezer-generator 从语法文件生成解析器
* 使用node build-parser.js
*/
import { execSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
console.log('🚀 start building parser...');
try {
// 检查语法文件是否存在
const grammarFile = path.join(__dirname, 'codeblock.grammar');
if (!fs.existsSync(grammarFile)) {
throw new Error('grammarFile codeblock.grammar not found');
}
console.log('📄 grammar file:', grammarFile);
// 运行 lezer-generator
console.log('⚙️ building parser...');
execSync('npx lezer-generator codeblock.grammar -o parser.js', {
cwd: __dirname,
stdio: 'inherit'
});
// 检查生成的文件
const parserFile = path.join(__dirname, 'parser.js');
const termsFile = path.join(__dirname, 'parser.terms.js');
if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) {
console.log('✅ parser file successfully generated');
console.log('📦 parser files:');
console.log(' - parser.js');
console.log(' - parser.terms.js');
} else {
throw new Error('failed to generate parser');
}
console.log('🎉 build success');
} catch (error) {
console.error('❌ build failed:', error.message);
process.exit(1);
}

View File

@@ -0,0 +1,65 @@
/**
* 代码块语言支持
* 提供多语言代码块支持
*/
import { parser } from "./parser.js";
import { configureNesting } from "./nested-parser";
import {
LRLanguage,
LanguageSupport,
foldNodeProp,
} from "@codemirror/language";
import { styleTags, tags as t } from "@lezer/highlight";
import { json } from "@codemirror/lang-json";
/**
* 折叠节点函数
*/
function foldNode(node: any) {
return { from: node.from, to: node.to - 1 };
}
/**
* 代码块语言定义
*/
export const CodeBlockLanguage = LRLanguage.define({
parser: parser.configure({
props: [
styleTags({
BlockDelimiter: t.tagName,
}),
foldNodeProp.add({
BlockContent(node: any) {
return { from: node.from, to: node.to - 1 };
},
}),
],
wrap: configureNesting(),
}),
languageData: {
commentTokens: { line: ";" }
}
});
/**
* 创建代码块语言支持
*/
export function codeBlockLang() {
let wrap = configureNesting();
let lang = CodeBlockLanguage.configure({ dialect: "", wrap: wrap });
return [
new LanguageSupport(lang, [json().support]),
];
}
/**
* 获取代码块语言扩展
*/
export function getCodeBlockLanguageExtension() {
return codeBlockLang();
}

View File

@@ -0,0 +1,23 @@
@external tokens blockContent from "./external-tokens.js" {
BlockContent
}
@top Document { Block* }
Block {
BlockDelimiter BlockContent
}
BlockDelimiter {
"\n∞∞∞" BlockLanguage Auto? "\n"
}
BlockLanguage {
"text" | "math" | "json" | "python" | "html" | "sql" | "markdown" |
"java" | "php" | "css" | "xml" | "cpp" | "rust" | "ruby" | "shell" |
"yaml" | "go" | "javascript" | "typescript"
}
@tokens {
Auto { "-a" }
}

View File

@@ -0,0 +1,51 @@
/**
* 外部标记器
* 用于识别代码块内容的边界
*/
import { ExternalTokenizer } from "@lezer/lr";
import { BlockContent } from "./parser.terms.js";
import { LANGUAGES } from "./languages";
const EOF = -1;
const FIRST_TOKEN_CHAR = "\n".charCodeAt(0);
const SECOND_TOKEN_CHAR = "∞".charCodeAt(0);
// 创建语言标记匹配器
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|");
const tokenRegEx = new RegExp(`^\\n∞∞∞(${languageTokensMatcher})(-a)?\\n`, "g");
/**
* 代码块内容标记器
* 识别 ∞∞∞ 分隔符之间的内容
*/
export const blockContent = new ExternalTokenizer((input) => {
let current = input.peek(0);
let next = input.peek(1);
if (current === EOF) {
return;
}
while (true) {
// 除非前两个字符是换行符和"∞"字符,否则我们没有代码块内容标记
// 所以我们不需要检查标记的其余部分
if (current === FIRST_TOKEN_CHAR && next === SECOND_TOKEN_CHAR) {
let potentialLang = "";
for (let i = 0; i < 18; i++) {
potentialLang += String.fromCharCode(input.peek(i));
}
if (potentialLang.match(tokenRegEx)) {
input.acceptToken(BlockContent);
return;
}
}
if (next === EOF) {
input.acceptToken(BlockContent, 1);
return;
}
current = input.advance(1);
next = input.peek(1);
}
});

View File

@@ -0,0 +1,38 @@
/**
* 代码块语言解析器入口
* 导出所有语言解析相关的功能
*/
// 主要语言支持
export {
CodeBlockLanguage,
codeBlockLang,
getCodeBlockLanguageExtension
} from './codeblock-lang';
// 语言映射和信息
export {
LanguageInfo,
LANGUAGES,
languageMapping,
getLanguage,
getLanguageTokens
} from './languages';
// 嵌套解析器
export {
configureNesting
} from './nested-parser';
// 解析器术语
export * from './parser.terms.js';
// 外部标记器
export {
blockContent
} from './external-tokens';
// 解析器
export {
parser
} from './parser.js';

View File

@@ -0,0 +1,82 @@
/**
* 语言映射和解析器配置
*/
import { jsonLanguage } from "@codemirror/lang-json";
import { pythonLanguage } from "@codemirror/lang-python";
import { javascriptLanguage, typescriptLanguage } from "@codemirror/lang-javascript";
import { htmlLanguage } from "@codemirror/lang-html";
import { StandardSQL } from "@codemirror/lang-sql";
import { markdownLanguage } from "@codemirror/lang-markdown";
import { javaLanguage } from "@codemirror/lang-java";
import { phpLanguage } from "@codemirror/lang-php";
import { cssLanguage } from "@codemirror/lang-css";
import { cppLanguage } from "@codemirror/lang-cpp";
import { xmlLanguage } from "@codemirror/lang-xml";
import { rustLanguage } from "@codemirror/lang-rust";
import { StreamLanguage } from "@codemirror/language";
import { ruby } from "@codemirror/legacy-modes/mode/ruby";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { go } from "@codemirror/legacy-modes/mode/go";
import { yamlLanguage } from "@codemirror/lang-yaml";
import { SupportedLanguage } from '../types';
/**
* 语言信息类
*/
export class LanguageInfo {
constructor(
public token: SupportedLanguage,
public name: string,
public parser: any,
public guesslang?: string | null
) {}
}
/**
* 支持的语言列表
*/
export const LANGUAGES: LanguageInfo[] = [
new LanguageInfo("text", "Plain Text", null),
new LanguageInfo("json", "JSON", jsonLanguage.parser, "json"),
new LanguageInfo("python", "Python", pythonLanguage.parser, "py"),
new LanguageInfo("javascript", "JavaScript", javascriptLanguage.parser, "js"),
new LanguageInfo("typescript", "TypeScript", typescriptLanguage.parser, "ts"),
new LanguageInfo("html", "HTML", htmlLanguage.parser, "html"),
new LanguageInfo("css", "CSS", cssLanguage.parser, "css"),
new LanguageInfo("sql", "SQL", StandardSQL.language.parser, "sql"),
new LanguageInfo("markdown", "Markdown", markdownLanguage.parser, "md"),
new LanguageInfo("java", "Java", javaLanguage.parser, "java"),
new LanguageInfo("php", "PHP", phpLanguage.configure({top:"Program"}).parser, "php"),
new LanguageInfo("xml", "XML", xmlLanguage.parser, "xml"),
new LanguageInfo("cpp", "C++", cppLanguage.parser, "cpp"),
new LanguageInfo("c", "C", cppLanguage.parser, "c"),
new LanguageInfo("rust", "Rust", rustLanguage.parser, "rs"),
new LanguageInfo("ruby", "Ruby", StreamLanguage.define(ruby).parser, "rb"),
new LanguageInfo("shell", "Shell", StreamLanguage.define(shell).parser, "sh"),
new LanguageInfo("yaml", "YAML", yamlLanguage.parser, "yaml"),
new LanguageInfo("go", "Go", StreamLanguage.define(go).parser, "go"),
];
/**
* 语言映射表
*/
export const languageMapping = Object.fromEntries(
LANGUAGES.map(l => [l.token, l.parser])
);
/**
* 根据 token 获取语言信息
*/
export function getLanguage(token: SupportedLanguage): LanguageInfo | undefined {
return LANGUAGES.find(lang => lang.token === token);
}
/**
* 获取所有语言的 token 列表
*/
export function getLanguageTokens(): SupportedLanguage[] {
return LANGUAGES.map(lang => lang.token);
}

View File

@@ -0,0 +1,44 @@
/**
* 嵌套解析器配置
* 为不同语言的代码块提供语法高亮支持
*/
import { parseMixed } from "@lezer/common";
import { BlockContent, BlockLanguage } from "./parser.terms.js";
import { languageMapping } from "./languages";
/**
* 配置嵌套解析器
* 根据代码块的语言标记选择相应的解析器
*/
export function configureNesting() {
return parseMixed((node, input) => {
let id = node.type.id;
if (id === BlockContent) {
// 获取父节点中的语言标记
let blockLang = node.node.parent?.firstChild?.getChildren(BlockLanguage)[0];
let langName = blockLang ? input.read(blockLang.from, blockLang.to) : null;
// 如果 BlockContent 为空,不返回解析器
// 这可以避免 StreamLanguage 解析器在大缓冲区时出错
if (node.node.from === node.node.to) {
return null;
}
// 处理自动检测标记
if (langName && langName.endsWith('-a')) {
langName = langName.slice(0, -2); // 移除 '-a' 后缀
}
// 查找对应的语言解析器
if (langName && langName in languageMapping && languageMapping[langName] !== null) {
return {
parser: languageMapping[langName],
};
}
}
return null;
});
}

View File

@@ -0,0 +1,17 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {blockContent} from "./external-tokens.js"
export const parser = LRParser.deserialize({
version: 14,
states: "!jQQOQOOOVOQO'#C`O!dOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO!iOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO!qOSO1G.fOOOP7+$Q7+$Q",
stateData: "!v~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTO~OPVO~OUYOmXO~OmZO~O",
goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
maxTerm: 29,
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: ",k~R]YZz}!O!e#V#W!p#Z#[#a#[#]#l#^#_$T#a#b%x#d#e'X#f#g([#g#h)R#h#i*O#l#m+q#m#n,SR!PPmQ%&x%&y!SP!VP%&x%&y!YP!]P%&x%&y!`P!eOXP~!hP#T#U!k~!pOU~~!sQ#d#e!y#g#h#U~!|P#d#e#P~#UOe~~#XP#g#h#[~#aOc~~#dP#c#d#g~#lOj~~#oP#h#i#r~#uP#a#b#x~#{P#`#a$O~$TO^~~$WQ#T#U$^#g#h%g~$aP#j#k$d~$gP#T#U$j~$oPa~#g#h$r~$uP#V#W$x~${P#f#g%O~%RP#]#^%U~%XP#d#e%[~%_P#h#i%b~%gOk~~%jP#c#d%m~%pP#b#c%s~%xO[~~%{P#T#U&O~&RQ#f#g&X#h#i&|~&[P#_#`&_~&bP#W#X&e~&hP#c#d&k~&nP#k#l&q~&tP#b#c&w~&|O`~~'PP#[#]'S~'XOZ~~'[Q#[#]'b#m#n'm~'eP#d#e'h~'mOb~~'pP#h#i's~'vP#[#]'y~'|P#c#d(P~(SP#b#c(V~([O]~~(_P#i#j(b~(eQ#U#V(k#g#h(v~(nP#m#n(q~(vOg~~(yP#h#i(|~)ROf~~)UQ#[#])[#e#f)s~)_P#X#Y)b~)eP#`#a)h~)kP#`#a)n~)sOh~~)vP#`#a)y~*OO_~~*RQ#X#Y*X#m#n*j~*[P#l#m*_~*bP#h#i*e~*jOY~~*mP#d#e*p~*sP#X#Y*v~*yP#g#h*|~+PP#V#W+S~+VP#f#g+Y~+]P#]#^+`~+cP#d#e+f~+iP#h#i+l~+qOl~~+tP#a#b+w~+zP#`#a+}~,SOd~~,VP#T#U,Y~,]P#a#b,`~,cP#`#a,f~,kOi~",
tokenizers: [blockContent, 0, 1],
topRules: {"Document":[0,2]},
tokenPrec: 0
})

View File

@@ -0,0 +1,8 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
BlockContent = 1,
Document = 2,
Block = 3,
BlockDelimiter = 4,
BlockLanguage = 5,
Auto = 6

View File

@@ -0,0 +1,259 @@
/**
* 自动语言检测
* 基于内容变化自动检测和更新代码块语言
*/
import { EditorState, Annotation } from '@codemirror/state';
import { EditorView, ViewPlugin } from '@codemirror/view';
import { blockState, getActiveNoteBlock } from '../state';
import { levenshteinDistance } from './levenshtein';
import { detectLanguageHeuristic, LanguageDetectionResult } from './heuristics';
import { LANGUAGES } from '../lang-parser/languages';
import { SupportedLanguage, Block } from '../types';
/**
* 语言检测配置
*/
interface LanguageDetectionConfig {
/** 最小内容长度阈值 */
minContentLength: number;
/** 变化阈值比例 */
changeThreshold: number;
/** 检测置信度阈值 */
confidenceThreshold: number;
/** 空闲检测延迟 (ms) */
idleDelay: number;
/** 默认语言 */
defaultLanguage: SupportedLanguage;
}
/**
* 默认配置
*/
const DEFAULT_CONFIG: LanguageDetectionConfig = {
minContentLength: 8,
changeThreshold: 0.1,
confidenceThreshold: 0.3,
idleDelay: 1000,
defaultLanguage: 'text',
};
/**
* 语言标记映射
* 将检测结果映射到我们的语言标记
*/
const DETECTION_TO_TOKEN = Object.fromEntries(
LANGUAGES.map(l => [l.token, l.token])
);
/**
* 兼容性函数
*/
function requestIdleCallbackCompat(cb: () => void): number {
if (typeof window !== 'undefined' && window.requestIdleCallback) {
return window.requestIdleCallback(cb);
} else {
return setTimeout(cb, 0) as any;
}
}
function cancelIdleCallbackCompat(id: number): void {
if (typeof window !== 'undefined' && window.cancelIdleCallback) {
window.cancelIdleCallback(id);
} else {
clearTimeout(id);
}
}
/**
* 语言更改注解
*/
const languageChangeAnnotation = Annotation.define<boolean>();
/**
* 语言更改命令
* 简化版本,直接更新块的语言标记
*/
function changeLanguageTo(
state: EditorState,
dispatch: (tr: any) => void,
block: Block,
newLanguage: SupportedLanguage,
isAuto: boolean
): void {
// 构建新的分隔符文本
const autoSuffix = isAuto ? '-a' : '';
const newDelimiter = `\n∞∞∞${newLanguage}${autoSuffix}\n`;
// 创建事务来替换分隔符
const transaction = state.update({
changes: {
from: block.delimiter.from,
to: block.delimiter.to,
insert: newDelimiter,
},
annotations: [
languageChangeAnnotation.of(true)
]
});
dispatch(transaction);
}
/**
* 创建语言检测插件
*/
export function createLanguageDetection(
config: Partial<LanguageDetectionConfig> = {}
): ViewPlugin<any> {
const finalConfig = { ...DEFAULT_CONFIG, ...config };
const previousBlockContent: Record<number, string> = {};
let idleCallbackId: number | null = null;
return ViewPlugin.fromClass(
class LanguageDetectionPlugin {
constructor(public view: EditorView) {}
update(update: any) {
if (update.docChanged) {
// 取消之前的检测
if (idleCallbackId !== null) {
cancelIdleCallbackCompat(idleCallbackId);
idleCallbackId = null;
}
// 安排新的检测
idleCallbackId = requestIdleCallbackCompat(() => {
idleCallbackId = null;
this.performLanguageDetection(update);
});
}
}
private performLanguageDetection(update: any) {
const range = update.state.selection.asSingle().ranges[0];
const blocks = update.state.field(blockState);
let block: Block | null = null;
let blockIndex: number | null = null;
// 找到当前块
for (let i = 0; i < blocks.length; i++) {
if (blocks[i].content.from <= range.from && blocks[i].content.to >= range.from) {
block = blocks[i];
blockIndex = i;
break;
}
}
if (block === null || blockIndex === null) {
return;
}
// 如果不是自动检测模式,清除缓存并返回
if (!block.language.auto) {
delete previousBlockContent[blockIndex];
return;
}
const content = update.state.doc.sliceString(block.content.from, block.content.to);
// 如果内容为空,重置为默认语言
if (content === "") {
if (block.language.name !== finalConfig.defaultLanguage) {
changeLanguageTo(
update.state,
this.view.dispatch,
block,
finalConfig.defaultLanguage,
true
);
}
delete previousBlockContent[blockIndex];
return;
}
// 内容太短,跳过检测
if (content.length <= finalConfig.minContentLength) {
return;
}
// 检查内容是否有显著变化
const threshold = content.length * finalConfig.changeThreshold;
const previousContent = previousBlockContent[blockIndex];
if (!previousContent) {
// 执行语言检测
this.detectAndUpdateLanguage(content, block, blockIndex, update.state);
previousBlockContent[blockIndex] = content;
} else {
const distance = levenshteinDistance(previousContent, content);
if (distance >= threshold) {
// 执行语言检测
this.detectAndUpdateLanguage(content, block, blockIndex, update.state);
previousBlockContent[blockIndex] = content;
}
}
}
private detectAndUpdateLanguage(
content: string,
block: any,
blockIndex: number,
state: EditorState
) {
// 使用启发式检测
const detectionResult = detectLanguageHeuristic(content);
// 检查置信度和语言变化
if (detectionResult.confidence >= finalConfig.confidenceThreshold &&
detectionResult.language !== block.language.name &&
DETECTION_TO_TOKEN[detectionResult.language]) {
// 验证内容未显著变化
const currentContent = state.doc.sliceString(block.content.from, block.content.to);
const threshold = currentContent.length * finalConfig.changeThreshold;
const contentDistance = levenshteinDistance(content, currentContent);
if (contentDistance <= threshold) {
// 内容未显著变化,安全更新语言
changeLanguageTo(
state,
this.view.dispatch,
block,
detectionResult.language,
true
);
}
}
}
destroy() {
if (idleCallbackId !== null) {
cancelIdleCallbackCompat(idleCallbackId);
}
}
}
);
}
/**
* 手动检测语言
*/
export function detectLanguage(content: string): LanguageDetectionResult {
return detectLanguageHeuristic(content);
}
/**
* 批量检测多个内容块的语言
*/
export function detectLanguages(contents: string[]): LanguageDetectionResult[] {
return contents.map(content => detectLanguageHeuristic(content));
}

View File

@@ -0,0 +1,269 @@
/**
* 基于启发式规则的语言检测
* 用于快速识别常见的编程语言模式
*/
import { SupportedLanguage } from '../types';
/**
* 语言检测结果
*/
export interface LanguageDetectionResult {
language: SupportedLanguage;
confidence: number;
}
/**
* 语言模式定义
*/
interface LanguagePattern {
patterns: RegExp[];
weight: number;
}
/**
* 语言检测规则映射
*/
const LANGUAGE_PATTERNS: Record<string, LanguagePattern> = {
javascript: {
patterns: [
/\b(function|const|let|var|class|extends|import|export|async|await)\b/g,
/\b(console\.log|document\.|window\.)\b/g,
/=>\s*[{(]/g,
/\b(require|module\.exports)\b/g,
],
weight: 1.0,
},
typescript: {
patterns: [
/\b(interface|type|enum|namespace|implements|declare)\b/g,
/:\s*(string|number|boolean|object|any)\b/g,
/<[A-Z][a-zA-Z0-9<>,\s]*>/g,
/\b(public|private|protected|readonly)\b/g,
],
weight: 1.2,
},
python: {
patterns: [
/\b(def|class|import|from|if __name__|print|len|range)\b/g,
/^\s*#.*$/gm,
/\b(True|False|None)\b/g,
/:\s*$/gm,
],
weight: 1.0,
},
java: {
patterns: [
/\b(public|private|protected|static|final|class|interface)\b/g,
/\b(System\.out\.println|String|int|void)\b/g,
/import\s+[a-zA-Z0-9_.]+;/g,
/\b(extends|implements)\b/g,
],
weight: 1.0,
},
html: {
patterns: [
/<\/?[a-zA-Z][^>]*>/g,
/<!DOCTYPE\s+html>/gi,
/<(div|span|p|h[1-6]|body|head|html)\b/g,
/\s(class|id|src|href)=/g,
],
weight: 1.5,
},
css: {
patterns: [
/[.#][a-zA-Z][\w-]*\s*{/g,
/\b(color|background|margin|padding|font-size):\s*[^;]+;/g,
/@(media|keyframes|import)\b/g,
/\{[^}]*\}/g,
],
weight: 1.3,
},
json: {
patterns: [
/^\s*[{\[][\s\S]*[}\]]\s*$/,
/"[^"]*":\s*(".*"|[\d.]+|true|false|null)/g,
/,\s*$/gm,
],
weight: 2.0,
},
sql: {
patterns: [
/\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\b/gi,
/\b(JOIN|LEFT|RIGHT|INNER|OUTER|ON|GROUP BY|ORDER BY)\b/gi,
/;\s*$/gm,
/\b(TABLE|DATABASE|INDEX)\b/gi,
],
weight: 1.4,
},
shell: {
patterns: [
/^#!/g,
/\b(echo|cd|ls|grep|awk|sed|cat|chmod)\b/g,
/\$\{?\w+\}?/g,
/\|\s*\w+/g,
],
weight: 1.2,
},
markdown: {
patterns: [
/^#+\s+/gm,
/\*\*.*?\*\*/g,
/\[.*?\]\(.*?\)/g,
/^```/gm,
],
weight: 1.1,
},
php: {
patterns: [
/<\?php/g,
/\$\w+/g,
/\b(function|class|extends|implements)\b/g,
/echo\s+/g,
],
weight: 1.3,
},
cpp: {
patterns: [
/#include\s*<.*>/g,
/\b(int|char|float|double|void|class|struct)\b/g,
/std::/g,
/cout\s*<<|cin\s*>>/g,
],
weight: 1.1,
},
rust: {
patterns: [
/\bfn\s+\w+/g,
/\b(let|mut|struct|enum|impl|trait)\b/g,
/println!\(/g,
/::\w+/g,
],
weight: 1.2,
},
go: {
patterns: [
/\bfunc\s+\w+/g,
/\b(var|const|type|package|import)\b/g,
/fmt\.\w+/g,
/:=\s*/g,
],
weight: 1.1,
},
ruby: {
patterns: [
/\b(def|class|module|end)\b/g,
/\b(puts|print|require)\b/g,
/@\w+/g,
/\|\w+\|/g,
],
weight: 1.0,
},
yaml: {
patterns: [
/^\s*\w+:\s*.*$/gm, // key: value 模式
/^\s*-\s+\w+/gm, // 列表项
/^---\s*$/gm, // 文档分隔符
/^\s*\w+:\s*\|/gm, // 多行字符串
/^\s*\w+:\s*>/gm, // 折叠字符串
/^\s*#.*$/gm, // 注释
/:\s*\[.*\]/g, // 内联数组
/:\s*\{.*\}/g, // 内联对象
],
weight: 1.5,
},
xml: {
patterns: [
/<\?xml/g,
/<\/\w+>/g,
/<\w+[^>]*\/>/g,
/\s\w+="[^"]*"/g,
],
weight: 1.3,
},
};
/**
* JSON 特殊检测
* 使用更严格的规则检测 JSON
*/
function detectJSON(content: string): LanguageDetectionResult | null {
const trimmed = content.trim();
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
JSON.parse(trimmed);
return {
language: 'json',
confidence: 1.0,
};
} catch (e) {
// JSON 解析失败,继续其他检测
}
}
return null;
}
/**
* 计算文本与语言模式的匹配分数
*/
function calculateScore(content: string, pattern: LanguagePattern): number {
let score = 0;
const contentLength = Math.max(content.length, 1);
for (const regex of pattern.patterns) {
const matches = content.match(regex);
if (matches) {
score += matches.length;
}
}
// 根据内容长度和权重标准化分数
return (score * pattern.weight) / (contentLength / 100);
}
/**
* 基于启发式规则检测语言
*/
export function detectLanguageHeuristic(content: string): LanguageDetectionResult {
if (!content.trim()) {
return { language: 'text', confidence: 1.0 };
}
// 首先尝试 JSON 特殊检测
const jsonResult = detectJSON(content);
if (jsonResult) {
return jsonResult;
}
const scores: Record<string, number> = {};
// 计算每种语言的匹配分数
for (const [language, pattern] of Object.entries(LANGUAGE_PATTERNS)) {
scores[language] = calculateScore(content, pattern);
}
// 找到最高分的语言
const sortedScores = Object.entries(scores)
.sort(([, a], [, b]) => b - a)
.filter(([, score]) => score > 0);
if (sortedScores.length > 0) {
const [bestLanguage, bestScore] = sortedScores[0];
return {
language: bestLanguage as SupportedLanguage,
confidence: Math.min(bestScore, 1.0),
};
}
return { language: 'text', confidence: 1.0 };
}
/**
* 获取所有支持的检测语言
*/
export function getSupportedDetectionLanguages(): SupportedLanguage[] {
return Object.keys(LANGUAGE_PATTERNS) as SupportedLanguage[];
}

View File

@@ -0,0 +1,26 @@
/**
* 语言检测模块入口
* 导出所有语言检测相关的功能
*/
// 主要检测功能
export {
createLanguageDetection,
detectLanguage,
detectLanguages
} from './autodetect';
// 启发式检测
export {
detectLanguageHeuristic,
getSupportedDetectionLanguages,
type LanguageDetectionResult
} from './heuristics';
// 工具函数
export {
levenshteinDistance
} from './levenshtein';
// 重新导出类型
export type { SupportedLanguage } from '../types';

View File

@@ -0,0 +1,113 @@
/**
* Levenshtein 距离算法
* 用于计算两个字符串之间的编辑距离
*/
/**
* 内部最小值计算函数
*/
function _min(d0: number, d1: number, d2: number, bx: number, ay: number): number {
return d0 < d1 || d2 < d1
? d0 > d2
? d2 + 1
: d0 + 1
: bx === ay
? d1
: d1 + 1;
}
/**
* 计算两个字符串之间的 Levenshtein 距离
* @param a 第一个字符串
* @param b 第二个字符串
* @returns 编辑距离
*/
export function levenshteinDistance(a: string, b: string): number {
if (a === b) {
return 0;
}
if (a.length > b.length) {
const tmp = a;
a = b;
b = tmp;
}
let la = a.length;
let lb = b.length;
while (la > 0 && (a.charCodeAt(la - 1) === b.charCodeAt(lb - 1))) {
la--;
lb--;
}
let offset = 0;
while (offset < la && (a.charCodeAt(offset) === b.charCodeAt(offset))) {
offset++;
}
la -= offset;
lb -= offset;
if (la === 0 || lb < 3) {
return lb;
}
let x = 0;
let y: number;
let d0: number;
let d1: number;
let d2: number;
let d3: number;
let dd = 0;
let dy: number;
let ay: number;
let bx0: number;
let bx1: number;
let bx2: number;
let bx3: number;
const vector: number[] = [];
for (y = 0; y < la; y++) {
vector.push(y + 1);
vector.push(a.charCodeAt(offset + y));
}
const len = vector.length - 1;
for (; x < lb - 3;) {
bx0 = b.charCodeAt(offset + (d0 = x));
bx1 = b.charCodeAt(offset + (d1 = x + 1));
bx2 = b.charCodeAt(offset + (d2 = x + 2));
bx3 = b.charCodeAt(offset + (d3 = x + 3));
x += 4;
dd = x;
for (y = 0; y < len; y += 2) {
dy = vector[y];
ay = vector[y + 1];
d0 = _min(dy, d0, d1, bx0, ay);
d1 = _min(d0, d1, d2, bx1, ay);
d2 = _min(d1, d2, d3, bx2, ay);
dd = _min(d2, d3, dd, bx3, ay);
vector[y] = dd;
d3 = d2;
d2 = d1;
d1 = d0;
d0 = dy;
}
}
for (; x < lb;) {
bx0 = b.charCodeAt(offset + (d0 = x));
dd = ++x;
for (y = 0; y < len; y += 2) {
dy = vector[y];
vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1]);
d0 = dy;
}
}
return dd;
}

View File

@@ -0,0 +1,160 @@
/**
* 移动行功能
* 处理代码块分隔符
*/
import { EditorSelection, SelectionRange } from "@codemirror/state";
import { blockState } from "./state";
import { SUPPORTED_LANGUAGES } from "./types";
interface LineBlock {
from: number;
to: number;
ranges: SelectionRange[];
}
// 创建语言标记的正则表达式
const languageTokensMatcher = SUPPORTED_LANGUAGES.join("|");
const tokenRegEx = new RegExp(`^∞∞∞(${languageTokensMatcher})(-a)?$`, "g");
/**
* 获取选中的行块
*/
function selectedLineBlocks(state: any): LineBlock[] {
let blocks: LineBlock[] = [];
let upto = -1;
for (let range of state.selection.ranges) {
let startLine = state.doc.lineAt(range.from);
let endLine = state.doc.lineAt(range.to);
if (!range.empty && range.to == endLine.from) {
endLine = state.doc.lineAt(range.to - 1);
}
if (upto >= startLine.number) {
let prev = blocks[blocks.length - 1];
prev.to = endLine.to;
prev.ranges.push(range);
} else {
blocks.push({
from: startLine.from,
to: endLine.to,
ranges: [range]
});
}
upto = endLine.number + 1;
}
return blocks;
}
/**
* 移动行的核心逻辑
*/
function moveLine(state: any, dispatch: any, forward: boolean): boolean {
if (state.readOnly) {
return false;
}
let changes: any[] = [];
let ranges: SelectionRange[] = [];
for (let block of selectedLineBlocks(state)) {
if (forward ? block.to == state.doc.length : block.from == 0) {
continue;
}
let nextLine = state.doc.lineAt(forward ? block.to + 1 : block.from - 1);
let previousLine;
if (!forward ? block.to == state.doc.length : block.from == 0) {
previousLine = null;
} else {
previousLine = state.doc.lineAt(forward ? block.from - 1 : block.to + 1);
}
// 如果整个选择是一个被分隔符包围的块,我们需要在分隔符之间添加额外的换行符
// 以避免创建两个只有单个换行符的分隔符,这会导致语法解析器无法解析
let nextLineIsSeparator = nextLine.text.match(tokenRegEx);
let blockSurroundedBySeparators = previousLine !== null &&
previousLine.text.match(tokenRegEx) && nextLineIsSeparator;
let size = nextLine.length + 1;
if (forward) {
if (blockSurroundedBySeparators) {
size += 1;
changes.push(
{ from: block.to, to: nextLine.to },
{ from: block.from, insert: state.lineBreak + nextLine.text + state.lineBreak }
);
} else {
changes.push(
{ from: block.to, to: nextLine.to },
{ from: block.from, insert: nextLine.text + state.lineBreak }
);
}
for (let r of block.ranges) {
ranges.push(EditorSelection.range(
Math.min(state.doc.length, r.anchor + size),
Math.min(state.doc.length, r.head + size)
));
}
} else {
if (blockSurroundedBySeparators || (previousLine === null && nextLineIsSeparator)) {
changes.push(
{ from: nextLine.from, to: block.from },
{ from: block.to, insert: state.lineBreak + nextLine.text + state.lineBreak }
);
for (let r of block.ranges) {
ranges.push(EditorSelection.range(r.anchor - size, r.head - size));
}
} else {
changes.push(
{ from: nextLine.from, to: block.from },
{ from: block.to, insert: state.lineBreak + nextLine.text }
);
for (let r of block.ranges) {
ranges.push(EditorSelection.range(r.anchor - size, r.head - size));
}
}
}
}
if (!changes.length) {
return false;
}
dispatch(state.update({
changes,
scrollIntoView: true,
selection: EditorSelection.create(ranges, state.selection.mainIndex),
userEvent: "move.line"
}));
return true;
}
/**
* 向上移动行
*/
export const moveLineUp = ({ state, dispatch }: { state: any; dispatch: any }): boolean => {
// 防止移动行到第一个块分隔符之前
if (state.selection.ranges.some((range: SelectionRange) => {
let startLine = state.doc.lineAt(range.from);
return startLine.from <= state.field(blockState)[0].content.from;
})) {
return true;
}
return moveLine(state, dispatch, false);
};
/**
* 向下移动行
*/
export const moveLineDown = ({ state, dispatch }: { state: any; dispatch: any }): boolean => {
return moveLine(state, dispatch, true);
};

View File

@@ -4,6 +4,7 @@
import { EditorState } from '@codemirror/state';
import { syntaxTree, syntaxTreeAvailable } from '@codemirror/language';
import { Block as BlockNode, BlockDelimiter, BlockContent, BlockLanguage, Document } from './lang-parser/parser.terms.js';
import {
CodeBlock,
SupportedLanguage,
@@ -17,121 +18,81 @@ import {
Block
} from './types';
/**
* 语言检测工具
*/
class LanguageDetector {
// 语言关键字映射
private static readonly LANGUAGE_PATTERNS: Record<string, RegExp[]> = {
javascript: [
/\b(function|const|let|var|class|extends|import|export|async|await)\b/,
/\b(console\.log|document\.|window\.)\b/,
/=>\s*[{(]/
],
typescript: [
/\b(interface|type|enum|namespace|implements|declare)\b/,
/:\s*(string|number|boolean|object|any)\b/,
/<[A-Z][a-zA-Z0-9<>,\s]*>/
],
python: [
/\b(def|class|import|from|if __name__|print|len|range)\b/,
/^\s*#.*$/m,
/\b(True|False|None)\b/
],
java: [
/\b(public|private|protected|static|final|class|interface)\b/,
/\b(System\.out\.println|String|int|void)\b/,
/import\s+[a-zA-Z0-9_.]+;/
],
html: [
/<\/?[a-zA-Z][^>]*>/,
/<!DOCTYPE\s+html>/i,
/<(div|span|p|h[1-6]|body|head|html)\b/
],
css: [
/[.#][a-zA-Z][\w-]*\s*{/,
/\b(color|background|margin|padding|font-size):\s*[^;]+;/,
/@(media|keyframes|import)\b/
],
json: [
/^\s*[{\[][\s\S]*[}\]]\s*$/,
/"[^"]*":\s*(".*"|[\d.]+|true|false|null)/,
/,\s*$/m
],
sql: [
/\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\b/i,
/\b(JOIN|LEFT|RIGHT|INNER|OUTER|ON|GROUP BY|ORDER BY)\b/i,
/;\s*$/m
],
shell: [
/^#!/,
/\b(echo|cd|ls|grep|awk|sed|cat|chmod)\b/,
/\$\{?\w+\}?/
],
markdown: [
/^#+\s+/m,
/\*\*.*?\*\*/,
/\[.*?\]\(.*?\)/,
/^```/m
]
};
/**
* 检测文本的编程语言
*/
static detectLanguage(text: string): LanguageDetectionResult {
if (!text.trim()) {
return { language: 'text', confidence: 1.0 };
}
const scores: Record<string, number> = {};
// 对每种语言计算匹配分数
for (const [language, patterns] of Object.entries(this.LANGUAGE_PATTERNS)) {
let score = 0;
const textLower = text.toLowerCase();
for (const pattern of patterns) {
const matches = text.match(pattern);
if (matches) {
score += matches.length;
}
}
// 根据文本长度标准化分数
scores[language] = score / Math.max(text.length / 100, 1);
}
// 找到最高分的语言
const bestMatch = Object.entries(scores)
.sort(([, a], [, b]) => b - a)[0];
if (bestMatch && bestMatch[1] > 0) {
return {
language: bestMatch[0] as SupportedLanguage,
confidence: Math.min(bestMatch[1], 1.0)
};
}
return { language: 'text', confidence: 1.0 };
}
}
/**
* 从语法树解析代码块
*/
export function getBlocksFromSyntaxTree(state: EditorState): CodeBlock[] | null {
export function getBlocksFromSyntaxTree(state: EditorState): Block[] | null {
if (!syntaxTreeAvailable(state)) {
return null;
}
const tree = syntaxTree(state);
const blocks: CodeBlock[] = [];
const blocks: Block[] = [];
const doc = state.doc;
// TODO: 如果使用自定义 Lezer 语法,在这里实现语法树解析
// 目前先返回 null使用字符串解析作为后备
return null;
// 遍历语法树中的所有块
tree.iterate({
enter(node) {
if (node.type.id === BlockNode) {
// 查找块的分隔符和内容
let delimiter: { from: number; to: number } | null = null;
let content: { from: number; to: number } | null = null;
let language = 'text';
let auto = false;
// 遍历块的子节点
const blockNode = node.node;
blockNode.firstChild?.cursor().iterate(child => {
if (child.type.id === BlockDelimiter) {
delimiter = { from: child.from, to: child.to };
// 解析整个分隔符文本来获取语言和自动检测标记
const delimiterText = doc.sliceString(child.from, child.to);
console.log('🔍 [解析器] 分隔符文本:', delimiterText);
// 使用正则表达式解析分隔符
const match = delimiterText.match(/∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/);
if (match) {
language = match[1] || 'text';
auto = match[2] === '-a';
console.log(`🔍 [解析器] 解析结果: 语言=${language}, 自动=${auto}`);
} else {
// 回退到逐个解析子节点
child.node.firstChild?.cursor().iterate(langChild => {
if (langChild.type.id === BlockLanguage) {
const langText = doc.sliceString(langChild.from, langChild.to);
language = langText || 'text';
}
// 检查是否有自动检测标记
if (doc.sliceString(langChild.from, langChild.to) === '-a') {
auto = true;
}
});
}
} else if (child.type.id === BlockContent) {
content = { from: child.from, to: child.to };
}
});
if (delimiter && content) {
blocks.push({
language: {
name: language as SupportedLanguage,
auto: auto,
},
content: content,
delimiter: delimiter,
range: {
from: node.from,
to: node.to,
},
});
}
}
}
});
return blocks.length > 0 ? blocks : null;
}
// 跟踪第一个分隔符的大小
@@ -308,6 +269,13 @@ export function getBlocksFromString(state: EditorState): Block[] {
* 获取文档中的所有块
*/
export function getBlocks(state: EditorState): Block[] {
// 优先使用语法树解析
const syntaxTreeBlocks = getBlocksFromSyntaxTree(state);
if (syntaxTreeBlocks) {
return syntaxTreeBlocks;
}
// 如果语法树不可用,回退到字符串解析
return getBlocksFromString(state);
}

View File

@@ -0,0 +1,53 @@
/**
* 字符转置功能
* 交换光标前后的字符
*/
import { EditorSelection, findClusterBreak } from "@codemirror/state";
import { getNoteBlockFromPos } from "./state";
/**
* 交换光标前后的字符
*/
export const transposeChars = ({ state, dispatch }: { state: any; dispatch: any }): boolean => {
if (state.readOnly) {
return false;
}
let changes = state.changeByRange((range: any) => {
// 防止在代码块的开始或结束位置进行字符转置,因为这会破坏块语法
const block = getNoteBlockFromPos(state, range.from);
if (block && (range.from === block.content.from || range.from === block.content.to)) {
return { range };
}
if (!range.empty || range.from == 0 || range.from == state.doc.length) {
return { range };
}
let pos = range.from;
let line = state.doc.lineAt(pos);
let from = pos == line.from ? pos - 1 : findClusterBreak(line.text, pos - line.from, false) + line.from;
let to = pos == line.to ? pos + 1 : findClusterBreak(line.text, pos - line.from, true) + line.from;
return {
changes: {
from,
to,
insert: state.doc.slice(pos, to).append(state.doc.slice(from, pos))
},
range: EditorSelection.cursor(to)
};
});
if (changes.changes.empty) {
return false;
}
dispatch(state.update(changes, {
scrollIntoView: true,
userEvent: "move.character"
}));
return true;
};