✨ Improve code block function
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
/**
|
||||
|
134
frontend/src/views/editor/extensions/codeblock/deleteLine.ts
Normal file
134
frontend/src/views/editor/extensions/codeblock/deleteLine.ts
Normal 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;
|
||||
};
|
@@ -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 };
|
||||
|
||||
|
@@ -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);
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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" }
|
||||
}
|
@@ -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);
|
||||
}
|
||||
});
|
@@ -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';
|
@@ -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);
|
||||
}
|
@@ -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;
|
||||
});
|
||||
}
|
@@ -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
|
||||
})
|
@@ -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
|
@@ -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));
|
||||
}
|
@@ -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[];
|
||||
}
|
@@ -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';
|
@@ -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;
|
||||
}
|
160
frontend/src/views/editor/extensions/codeblock/moveLines.ts
Normal file
160
frontend/src/views/editor/extensions/codeblock/moveLines.ts
Normal 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);
|
||||
};
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
};
|
Reference in New Issue
Block a user