Files
voidraft/frontend/src/views/editor/extensions/codeblock/copyPaste.ts
2025-10-05 00:58:27 +08:00

189 lines
4.6 KiB
TypeScript

/**
* 代码块复制粘贴扩展
* 防止复制分隔符标记,自动替换为换行符
*/
import { EditorState, EditorSelection } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { Command } from "@codemirror/view";
import { LANGUAGES } from "./lang-parser/languages";
/**
* 构建块分隔符正则表达式
*/
const languageTokensMatcher = LANGUAGES.map(lang => lang.token).join("|");
const blockSeparatorRegex = new RegExp(`\\n∞∞∞(${languageTokensMatcher})(-a)?\\n`, "g");
/**
* 获取被复制的范围和内容
*/
function copiedRange(state: EditorState, forCut: boolean = false) {
const content: string[] = [];
const ranges: any[] = [];
for (const range of state.selection.ranges) {
if (!range.empty) {
content.push(state.sliceDoc(range.from, range.to));
ranges.push(range);
}
}
if (ranges.length === 0) {
// 如果所有范围都是空的,我们想要复制每个选择的整行(唯一的)
const copiedLines: number[] = [];
for (const range of state.selection.ranges) {
if (range.empty) {
const line = state.doc.lineAt(range.head);
const lineContent = state.sliceDoc(line.from, line.to);
if (!copiedLines.includes(line.from)) {
content.push(lineContent);
// 对于剪切操作,需要包含整行范围(包括换行符)
if (forCut) {
const lineEnd = line.to < state.doc.length ? line.to + 1 : line.to;
ranges.push({ from: line.from, to: lineEnd });
} else {
ranges.push(range);
}
copiedLines.push(line.from);
}
}
}
}
return {
text: content.join(state.lineBreak),
ranges
};
}
/**
* 设置浏览器复制和剪切事件处理器,将块分隔符替换为换行符
*/
export const codeBlockCopyCut = EditorView.domEventHandlers({
copy(event, view) {
let { text } = copiedRange(view.state);
// 将块分隔符替换为双换行符
text = text.replaceAll(blockSeparatorRegex, "\n\n");
const data = event.clipboardData;
if (data) {
event.preventDefault();
data.clearData();
data.setData("text/plain", text);
}
},
cut(event, view) {
let { text, ranges } = copiedRange(view.state, true);
// 将块分隔符替换为双换行符
text = text.replaceAll(blockSeparatorRegex, "\n\n");
const data = event.clipboardData;
if (data) {
event.preventDefault();
data.clearData();
data.setData("text/plain", text);
}
if (!view.state.readOnly) {
view.dispatch({
changes: ranges,
scrollIntoView: true,
userEvent: "delete.cut"
});
}
}
});
/**
* 复制和剪切的通用函数
*/
const copyCut = (view: EditorView, cut: boolean): boolean => {
let { text, ranges } = copiedRange(view.state, cut);
// 将块分隔符替换为双换行符
text = text.replaceAll(blockSeparatorRegex, "\n\n");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text);
}
if (cut && !view.state.readOnly) {
view.dispatch({
changes: ranges,
scrollIntoView: true,
userEvent: "delete.cut"
});
}
return true;
};
/**
* 粘贴函数
*/
function doPaste(view: EditorView, input: string) {
const { state } = view;
const text = state.toText(input);
const byLine = text.lines === state.selection.ranges.length;
let changes: any;
if (byLine) {
let i = 1;
changes = state.changeByRange(range => {
const line = text.line(i++);
return {
changes: { from: range.from, to: range.to, insert: line.text },
range: EditorSelection.cursor(range.from + line.length)
};
});
} else {
changes = state.replaceSelection(text);
}
view.dispatch(changes, {
userEvent: "input.paste",
scrollIntoView: true
});
}
/**
* 复制命令
*/
export const copyCommand: Command = (view) => {
return copyCut(view, false);
};
/**
* 剪切命令
*/
export const cutCommand: Command = (view) => {
return copyCut(view, true);
};
/**
* 粘贴命令
*/
export const pasteCommand: Command = (view) => {
if (navigator.clipboard && navigator.clipboard.readText) {
navigator.clipboard.readText()
.then(text => {
doPaste(view, text);
})
.catch(err => {
console.error('Failed to read from clipboard:', err);
});
} else {
console.warn('The clipboard API is not available, please use your browser\'s native paste feature');
}
return true;
};
/**
* 获取复制粘贴扩展
*/
export function getCopyPasteExtensions() {
return [
codeBlockCopyCut,
];
}