Files
voidraft/frontend/src/views/editor/extensions/textHighlight/textHighlightExtension.ts

213 lines
5.8 KiB
TypeScript

import { EditorState, StateEffect, StateField, Facet } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView } from "@codemirror/view";
// 高亮配置接口
export interface TextHighlightConfig {
backgroundColor?: string;
opacity?: number;
}
// 默认配置
const DEFAULT_CONFIG: Required<TextHighlightConfig> = {
backgroundColor: '#FFD700', // 金黄色
opacity: 0.3
};
// 定义添加和移除高亮的状态效果
const addHighlight = StateEffect.define<{from: number, to: number}>({
map: ({from, to}, change) => ({
from: change.mapPos(from),
to: change.mapPos(to)
})
});
const removeHighlight = StateEffect.define<{from: number, to: number}>({
map: ({from, to}, change) => ({
from: change.mapPos(from),
to: change.mapPos(to)
})
});
// 配置facet
const highlightConfigFacet = Facet.define<TextHighlightConfig, Required<TextHighlightConfig>>({
combine: (configs) => {
let result = { ...DEFAULT_CONFIG };
for (const config of configs) {
if (config.backgroundColor !== undefined) {
result.backgroundColor = config.backgroundColor;
}
if (config.opacity !== undefined) {
result.opacity = config.opacity;
}
}
return result;
}
});
// 创建高亮装饰
function createHighlightMark(config: Required<TextHighlightConfig>): Decoration {
const { backgroundColor, opacity } = config;
const rgbaColor = hexToRgba(backgroundColor, opacity);
return Decoration.mark({
attributes: {
style: `background-color: ${rgbaColor}; border-radius: 2px;`
}
});
}
// 将十六进制颜色转换为RGBA
function hexToRgba(hex: string, opacity: number): string {
// 移除 # 符号
hex = hex.replace('#', '');
// 处理短格式 (如 #FFF -> #FFFFFF)
if (hex.length === 3) {
hex = hex.split('').map(char => char + char).join('');
}
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
// 存储高亮范围的状态字段 - 支持撤销
const highlightState = StateField.define<DecorationSet>({
create() {
return Decoration.none;
},
update(decorations, tr) {
// 映射现有装饰以适应文档变化
decorations = decorations.map(tr.changes);
// 处理效果
for (const effect of tr.effects) {
if (effect.is(addHighlight)) {
const { from, to } = effect.value;
const config = tr.state.facet(highlightConfigFacet);
const highlightMark = createHighlightMark(config);
decorations = decorations.update({
add: [highlightMark.range(from, to)]
});
}
else if (effect.is(removeHighlight)) {
const { from, to } = effect.value;
decorations = decorations.update({
filter: (rangeFrom, rangeTo) => {
// 移除与指定范围重叠的装饰
return !(rangeFrom < to && rangeTo > from);
}
});
}
}
return decorations;
},
provide: field => EditorView.decorations.from(field)
});
// 查找与给定范围重叠的所有高亮
function findHighlightsInRange(state: EditorState, from: number, to: number): Array<{from: number, to: number}> {
const highlights: Array<{from: number, to: number}> = [];
state.field(highlightState).between(from, to, (rangeFrom, rangeTo) => {
if (rangeFrom < to && rangeTo > from) {
highlights.push({ from: rangeFrom, to: rangeTo });
}
});
return highlights;
}
// 查找指定位置包含的高亮
function findHighlightsAt(state: EditorState, pos: number): Array<{from: number, to: number}> {
const highlights: Array<{from: number, to: number}> = [];
state.field(highlightState).between(pos, pos, (from, to) => {
highlights.push({ from, to });
});
return highlights;
}
// 添加高亮范围
function addHighlightRange(view: EditorView, from: number, to: number): boolean {
if (from === to) return false; // 不高亮空选择
// 检查是否已经完全高亮
const overlappingHighlights = findHighlightsInRange(view.state, from, to);
const isFullyHighlighted = overlappingHighlights.some(range =>
range.from <= from && range.to >= to
);
if (isFullyHighlighted) return false;
view.dispatch({
effects: addHighlight.of({from, to})
});
return true;
}
// 移除高亮范围
function removeHighlightRange(view: EditorView, from: number, to: number): boolean {
const highlights = findHighlightsInRange(view.state, from, to);
if (highlights.length === 0) return false;
view.dispatch({
effects: removeHighlight.of({from, to})
});
return true;
}
// 切换高亮状态
function toggleHighlight(view: EditorView): boolean {
const selection = view.state.selection.main;
// 如果有选择文本
if (!selection.empty) {
const {from, to} = selection;
// 检查选择范围内是否已经有高亮
const highlights = findHighlightsInRange(view.state, from, to);
if (highlights.length > 0) {
// 如果已有高亮,则移除
return removeHighlightRange(view, from, to);
} else {
// 如果没有高亮,则添加
return addHighlightRange(view, from, to);
}
}
// 如果是光标
else {
const pos = selection.from;
const highlightsAtCursor = findHighlightsAt(view.state, pos);
if (highlightsAtCursor.length > 0) {
// 移除光标位置的高亮
const highlight = highlightsAtCursor[0];
return removeHighlightRange(view, highlight.from, highlight.to);
}
}
return false;
}
// 导出文本高亮切换命令,供快捷键系统使用
export const textHighlightToggleCommand = toggleHighlight;
// 创建文本高亮扩展
export function createTextHighlighter(config: TextHighlightConfig = {}) {
return [
highlightConfigFacet.of(config),
highlightState
];
}