🚨 Format code
This commit is contained in:
@@ -19,7 +19,7 @@ declare global {
|
||||
let menuElement: HTMLElement | null = null;
|
||||
let clickOutsideHandler: ((e: MouseEvent) => void) | null = null;
|
||||
// 子菜单缓存池
|
||||
let submenuPool: Map<string, HTMLElement> = new Map();
|
||||
const submenuPool: Map<string, HTMLElement> = new Map();
|
||||
|
||||
/**
|
||||
* 获取或创建菜单DOM元素
|
||||
|
||||
@@ -92,14 +92,6 @@ function formatKeyBinding(keyBinding: string): string {
|
||||
.replace(/-/g, " + ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从命令注册表获取命令处理程序和翻译键
|
||||
* @param command 命令ID
|
||||
* @returns 命令处理程序和翻译键
|
||||
*/
|
||||
function getCommandInfo(command: KeyBindingCommand): { handler: (view: EditorView) => boolean, descriptionKey: string } | undefined {
|
||||
return commandRegistry[command];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建编辑菜单项
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import { EditorView, Decoration } from "@codemirror/view"
|
||||
import { WidgetType } from "@codemirror/view"
|
||||
import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view"
|
||||
import { Extension, Compartment, StateEffect } from "@codemirror/state"
|
||||
import { EditorView, Decoration } from "@codemirror/view";
|
||||
import { WidgetType } from "@codemirror/view";
|
||||
import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view";
|
||||
import { Extension, StateEffect } from "@codemirror/state";
|
||||
|
||||
// 创建字体变化效果
|
||||
const fontChangeEffect = StateEffect.define<void>()
|
||||
const fontChangeEffect = StateEffect.define<void>();
|
||||
|
||||
/**
|
||||
* 复选框小部件类
|
||||
*/
|
||||
class CheckboxWidget extends WidgetType {
|
||||
constructor(readonly checked: boolean) {
|
||||
super()
|
||||
super();
|
||||
}
|
||||
|
||||
eq(other: CheckboxWidget) {
|
||||
return other.checked == this.checked
|
||||
return other.checked == this.checked;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
let wrap = document.createElement("span")
|
||||
wrap.setAttribute("aria-hidden", "true")
|
||||
wrap.className = "cm-checkbox-toggle"
|
||||
const wrap = document.createElement("span");
|
||||
wrap.setAttribute("aria-hidden", "true");
|
||||
wrap.className = "cm-checkbox-toggle";
|
||||
|
||||
let box = document.createElement("input")
|
||||
box.type = "checkbox"
|
||||
box.checked = this.checked
|
||||
box.tabIndex = -1
|
||||
box.style.margin = "0"
|
||||
box.style.padding = "0"
|
||||
box.style.cursor = "pointer"
|
||||
box.style.position = "relative"
|
||||
box.style.top = "0.1em"
|
||||
box.style.marginRight = "0.5em"
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.checked = this.checked;
|
||||
box.tabIndex = -1;
|
||||
box.style.margin = "0";
|
||||
box.style.padding = "0";
|
||||
box.style.cursor = "pointer";
|
||||
box.style.position = "relative";
|
||||
box.style.top = "0.1em";
|
||||
box.style.marginRight = "0.5em";
|
||||
// 设置相对单位,让复选框跟随字体大小变化
|
||||
box.style.width = "1em"
|
||||
box.style.height = "1em"
|
||||
box.style.width = "1em";
|
||||
box.style.height = "1em";
|
||||
|
||||
wrap.appendChild(box)
|
||||
return wrap
|
||||
wrap.appendChild(box);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,82 +50,82 @@ class CheckboxWidget extends WidgetType {
|
||||
* 查找并创建复选框装饰
|
||||
*/
|
||||
function findCheckboxes(view: EditorView) {
|
||||
let widgets: any = []
|
||||
const doc = view.state.doc
|
||||
const widgets: any = [];
|
||||
const doc = view.state.doc;
|
||||
|
||||
for (let { from, to } of view.visibleRanges) {
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
// 使用正则表达式查找 [x] 或 [ ] 模式
|
||||
const text = doc.sliceString(from, to)
|
||||
const checkboxRegex = /\[([ x])\]/gi
|
||||
let match
|
||||
const text = doc.sliceString(from, to);
|
||||
const checkboxRegex = /\[([ x])\]/gi;
|
||||
let match;
|
||||
|
||||
while ((match = checkboxRegex.exec(text)) !== null) {
|
||||
const matchPos = from + match.index
|
||||
const matchEnd = matchPos + match[0].length
|
||||
const matchPos = from + match.index;
|
||||
const matchEnd = matchPos + match[0].length;
|
||||
|
||||
// 检查前面是否有 "- " 模式
|
||||
const beforeTwoChars = matchPos >= 2 ? doc.sliceString(matchPos - 2, matchPos) : ""
|
||||
const afterChar = matchEnd < doc.length ? doc.sliceString(matchEnd, matchEnd + 1) : ""
|
||||
const beforeTwoChars = matchPos >= 2 ? doc.sliceString(matchPos - 2, matchPos) : "";
|
||||
const afterChar = matchEnd < doc.length ? doc.sliceString(matchEnd, matchEnd + 1) : "";
|
||||
|
||||
// 只有当前面是 "- " 且后面跟空格或行尾时才渲染
|
||||
if (beforeTwoChars === "- " &&
|
||||
(afterChar === "" || afterChar === " " || afterChar === "\t" || afterChar === "\n")) {
|
||||
|
||||
const isChecked = match[1].toLowerCase() === "x"
|
||||
let deco = Decoration.replace({
|
||||
const isChecked = match[1].toLowerCase() === "x";
|
||||
const deco = Decoration.replace({
|
||||
widget: new CheckboxWidget(isChecked),
|
||||
inclusive: false,
|
||||
})
|
||||
});
|
||||
// 替换整个 "- [ ]" 或 "- [x]" 模式,包括前面的 "- "
|
||||
widgets.push(deco.range(matchPos - 2, matchEnd))
|
||||
widgets.push(deco.range(matchPos - 2, matchEnd));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Decoration.set(widgets)
|
||||
return Decoration.set(widgets);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换复选框状态
|
||||
*/
|
||||
function toggleCheckbox(view: EditorView, pos: number) {
|
||||
const doc = view.state.doc
|
||||
const doc = view.state.doc;
|
||||
|
||||
// 查找当前位置附近的复选框模式(需要前面有 "- ")
|
||||
for (let offset = -5; offset <= 0; offset++) {
|
||||
const checkPos = pos + offset
|
||||
const checkPos = pos + offset;
|
||||
if (checkPos >= 2 && checkPos + 3 <= doc.length) {
|
||||
// 检查是否有 "- " 前缀
|
||||
const prefix = doc.sliceString(checkPos - 2, checkPos)
|
||||
const text = doc.sliceString(checkPos, checkPos + 3).toLowerCase()
|
||||
const prefix = doc.sliceString(checkPos - 2, checkPos);
|
||||
const text = doc.sliceString(checkPos, checkPos + 3).toLowerCase();
|
||||
|
||||
if (prefix === "- ") {
|
||||
let change
|
||||
let change;
|
||||
|
||||
if (text === "[x]") {
|
||||
// 替换整个 "- [x]" 为 "- [ ]"
|
||||
change = { from: checkPos - 2, to: checkPos + 3, insert: "- [ ]" }
|
||||
change = { from: checkPos - 2, to: checkPos + 3, insert: "- [ ]" };
|
||||
} else if (text === "[ ]") {
|
||||
// 替换整个 "- [ ]" 为 "- [x]"
|
||||
change = { from: checkPos - 2, to: checkPos + 3, insert: "- [x]" }
|
||||
change = { from: checkPos - 2, to: checkPos + 3, insert: "- [x]" };
|
||||
}
|
||||
|
||||
if (change) {
|
||||
view.dispatch({ changes: change })
|
||||
return true
|
||||
view.dispatch({ changes: change });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建字体变化效果的便捷函数
|
||||
export const triggerFontChange = (view: EditorView) => {
|
||||
view.dispatch({
|
||||
effects: fontChangeEffect.of(undefined)
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建复选框扩展
|
||||
@@ -134,10 +134,10 @@ export function createCheckboxExtension(): Extension {
|
||||
return [
|
||||
// 主要的复选框插件
|
||||
ViewPlugin.fromClass(class {
|
||||
decorations: DecorationSet
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = findCheckboxes(view)
|
||||
this.decorations = findCheckboxes(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
@@ -145,10 +145,10 @@ export function createCheckboxExtension(): Extension {
|
||||
const shouldUpdate = update.docChanged ||
|
||||
update.viewportChanged ||
|
||||
update.geometryChanged ||
|
||||
update.transactions.some(tr => tr.effects.some(e => e.is(fontChangeEffect)))
|
||||
update.transactions.some(tr => tr.effects.some(e => e.is(fontChangeEffect)));
|
||||
|
||||
if (shouldUpdate) {
|
||||
this.decorations = findCheckboxes(update.view)
|
||||
this.decorations = findCheckboxes(update.view);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
@@ -156,10 +156,10 @@ export function createCheckboxExtension(): Extension {
|
||||
|
||||
eventHandlers: {
|
||||
mousedown: (e, view) => {
|
||||
let target = e.target as HTMLElement
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.nodeName == "INPUT" && target.parentElement!.classList.contains("cm-checkbox-toggle")) {
|
||||
const pos = view.posAtDOM(target)
|
||||
return toggleCheckbox(view, pos)
|
||||
const pos = view.posAtDOM(target);
|
||||
return toggleCheckbox(view, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,15 +180,15 @@ export function createCheckboxExtension(): Extension {
|
||||
fontSize: "inherit",
|
||||
}
|
||||
})
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 默认导出
|
||||
export const checkboxExtension = createCheckboxExtension()
|
||||
export const checkboxExtension = createCheckboxExtension();
|
||||
|
||||
// 导出类型和工具函数
|
||||
export {
|
||||
CheckboxWidget,
|
||||
toggleCheckbox,
|
||||
findCheckboxes
|
||||
}
|
||||
};
|
||||
@@ -193,8 +193,8 @@ function setSel(state: any, selection: EditorSelection) {
|
||||
}
|
||||
|
||||
function extendSel(state: any, dispatch: any, how: (range: any) => any) {
|
||||
let selection = updateSel(state.selection, range => {
|
||||
let head = how(range);
|
||||
const selection = updateSel(state.selection, range => {
|
||||
const head = how(range);
|
||||
return EditorSelection.range(range.anchor, head.head, head.goalColumn, head.bidiLevel || undefined);
|
||||
});
|
||||
if (selection.eq(state.selection)) return false;
|
||||
@@ -203,7 +203,7 @@ function extendSel(state: any, dispatch: any, how: (range: any) => any) {
|
||||
}
|
||||
|
||||
function moveSel(state: any, dispatch: any, how: (range: any) => any) {
|
||||
let selection = updateSel(state.selection, how);
|
||||
const selection = updateSel(state.selection, how);
|
||||
if (selection.eq(state.selection)) return false;
|
||||
dispatch(setSel(state, selection));
|
||||
return true;
|
||||
@@ -268,7 +268,7 @@ export function selectPreviousBlock({ state, dispatch }: any) {
|
||||
/**
|
||||
* 删除块
|
||||
*/
|
||||
export const deleteBlock = (options: EditorOptions): Command => ({ state, dispatch }) => {
|
||||
export const deleteBlock = (_options: EditorOptions): Command => ({ state, dispatch }) => {
|
||||
if (state.readOnly) return false;
|
||||
|
||||
const block = getActiveNoteBlock(state);
|
||||
@@ -380,4 +380,4 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
|
||||
*/
|
||||
export const formatCurrentBlock: Command = (view) => {
|
||||
return formatBlockContent(view);
|
||||
}
|
||||
};
|
||||
@@ -18,10 +18,10 @@ const blockSeparatorRegex = new RegExp(`\\n∞∞∞(${languageTokensMatcher})(-
|
||||
* 获取被复制的范围和内容
|
||||
*/
|
||||
function copiedRange(state: EditorState) {
|
||||
let content: string[] = [];
|
||||
let ranges: any[] = [];
|
||||
const content: string[] = [];
|
||||
const ranges: any[] = [];
|
||||
|
||||
for (let range of state.selection.ranges) {
|
||||
for (const range of state.selection.ranges) {
|
||||
if (!range.empty) {
|
||||
content.push(state.sliceDoc(range.from, range.to));
|
||||
ranges.push(range);
|
||||
@@ -31,7 +31,7 @@ function copiedRange(state: EditorState) {
|
||||
if (ranges.length === 0) {
|
||||
// 如果所有范围都是空的,我们想要复制每个选择的整行(唯一的)
|
||||
const copiedLines: number[] = [];
|
||||
for (let range of state.selection.ranges) {
|
||||
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);
|
||||
@@ -55,7 +55,7 @@ function copiedRange(state: EditorState) {
|
||||
*/
|
||||
export const codeBlockCopyCut = EditorView.domEventHandlers({
|
||||
copy(event, view) {
|
||||
let { text, ranges } = copiedRange(view.state);
|
||||
let { text } = copiedRange(view.state);
|
||||
// 将块分隔符替换为双换行符
|
||||
text = text.replaceAll(blockSeparatorRegex, "\n\n");
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class NoteBlockStart extends WidgetType {
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
let wrap = document.createElement("div");
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "code-block-start" + (this.isFirst ? " first" : "");
|
||||
return wrap;
|
||||
}
|
||||
@@ -37,8 +37,8 @@ const noteBlockWidget = () => {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
|
||||
state.field(blockState).forEach((block: any) => {
|
||||
let delimiter = block.delimiter;
|
||||
let deco = Decoration.replace({
|
||||
const delimiter = block.delimiter;
|
||||
const deco = Decoration.replace({
|
||||
widget: new NoteBlockStart(delimiter.from === 0),
|
||||
inclusive: true,
|
||||
block: true,
|
||||
@@ -80,7 +80,7 @@ const noteBlockWidget = () => {
|
||||
* 原子范围,防止在分隔符内编辑
|
||||
*/
|
||||
function atomicRanges(view: EditorView) {
|
||||
let builder = new RangeSetBuilder();
|
||||
const builder = new RangeSetBuilder();
|
||||
view.state.field(blockState).forEach((block: any) => {
|
||||
builder.add(
|
||||
block.delimiter.from,
|
||||
@@ -167,7 +167,7 @@ const blockLayer = layer({
|
||||
return markers;
|
||||
},
|
||||
|
||||
update(update: any, dom: any) {
|
||||
update(update: any, _dom: any) {
|
||||
return update.docChanged || update.viewportChanged;
|
||||
},
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ function updateSel(sel: EditorSelection, by: (range: SelectionRange) => Selectio
|
||||
* 获取选中的行块
|
||||
*/
|
||||
function selectedLineBlocks(state: any): LineBlock[] {
|
||||
let blocks: LineBlock[] = [];
|
||||
const blocks: LineBlock[] = [];
|
||||
let upto = -1;
|
||||
|
||||
for (let range of state.selection.ranges) {
|
||||
let startLine = state.doc.lineAt(range.from);
|
||||
for (const range of state.selection.ranges) {
|
||||
const startLine = state.doc.lineAt(range.from);
|
||||
let endLine = state.doc.lineAt(range.to);
|
||||
|
||||
if (!range.empty && range.to == endLine.from) {
|
||||
@@ -36,7 +36,7 @@ function selectedLineBlocks(state: any): LineBlock[] {
|
||||
}
|
||||
|
||||
if (upto >= startLine.number) {
|
||||
let prev = blocks[blocks.length - 1];
|
||||
const prev = blocks[blocks.length - 1];
|
||||
prev.to = endLine.to;
|
||||
prev.ranges.push(range);
|
||||
} else {
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import { EditorSelection } from "@codemirror/state"
|
||||
import { EditorSelection } from "@codemirror/state";
|
||||
|
||||
import * as prettier from "prettier/standalone"
|
||||
import { getActiveNoteBlock } from "./state"
|
||||
import { getLanguage } from "./lang-parser/languages"
|
||||
import { SupportedLanguage } from "./types"
|
||||
import * as prettier from "prettier/standalone";
|
||||
import { getActiveNoteBlock } from "./state";
|
||||
import { getLanguage } from "./lang-parser/languages";
|
||||
import { SupportedLanguage } from "./types";
|
||||
|
||||
export const formatBlockContent = (view) => {
|
||||
if (!view || view.state.readOnly)
|
||||
return false
|
||||
return false;
|
||||
|
||||
// 获取初始信息,但不缓存state对象
|
||||
const initialState = view.state
|
||||
const block = getActiveNoteBlock(initialState)
|
||||
const initialState = view.state;
|
||||
const block = getActiveNoteBlock(initialState);
|
||||
|
||||
if (!block) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
const blockFrom = block.content.from
|
||||
const blockTo = block.content.to
|
||||
const blockLanguageName = block.language.name as SupportedLanguage
|
||||
const blockFrom = block.content.from;
|
||||
const blockTo = block.content.to;
|
||||
const blockLanguageName = block.language.name as SupportedLanguage;
|
||||
|
||||
const language = getLanguage(blockLanguageName)
|
||||
const language = getLanguage(blockLanguageName);
|
||||
if (!language || !language.prettier) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取初始需要的信息
|
||||
const cursorPos = initialState.selection.asSingle().ranges[0].head
|
||||
const content = initialState.sliceDoc(blockFrom, blockTo)
|
||||
const tabSize = initialState.tabSize
|
||||
const cursorPos = initialState.selection.asSingle().ranges[0].head;
|
||||
const content = initialState.sliceDoc(blockFrom, blockTo);
|
||||
const tabSize = initialState.tabSize;
|
||||
|
||||
// 检查光标是否在块的开始或结束
|
||||
const cursorAtEdge = cursorPos == blockFrom || cursorPos == blockTo
|
||||
const cursorAtEdge = cursorPos == blockFrom || cursorPos == blockTo;
|
||||
|
||||
// 执行异步格式化
|
||||
const performFormat = async () => {
|
||||
let formattedContent
|
||||
let formattedContent;
|
||||
try {
|
||||
// 构建格式化配置
|
||||
const formatOptions = {
|
||||
@@ -44,39 +44,39 @@ export const formatBlockContent = (view) => {
|
||||
plugins: language.prettier!.plugins,
|
||||
tabWidth: tabSize,
|
||||
...language.prettier!.options,
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化代码
|
||||
const formatted = await prettier.format(content, formatOptions)
|
||||
const formatted = await prettier.format(content, formatOptions);
|
||||
|
||||
// 计算新光标位置
|
||||
const cursorOffset = cursorAtEdge
|
||||
? (cursorPos == blockFrom ? 0 : formatted.length)
|
||||
: Math.min(cursorPos - blockFrom, formatted.length)
|
||||
: Math.min(cursorPos - blockFrom, formatted.length);
|
||||
|
||||
formattedContent = {
|
||||
formatted,
|
||||
cursorOffset
|
||||
}
|
||||
} catch (e) {
|
||||
return false
|
||||
};
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 格式化完成后再次获取最新状态
|
||||
const currentState = view.state
|
||||
const currentState = view.state;
|
||||
|
||||
// 重新获取当前块的位置
|
||||
const currentBlock = getActiveNoteBlock(currentState)
|
||||
const currentBlock = getActiveNoteBlock(currentState);
|
||||
|
||||
if (!currentBlock) {
|
||||
console.warn('Block not found after formatting')
|
||||
return false
|
||||
console.warn('Block not found after formatting');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用当前块的实际位置
|
||||
const currentBlockFrom = currentBlock.content.from
|
||||
const currentBlockTo = currentBlock.content.to
|
||||
const currentBlockFrom = currentBlock.content.from;
|
||||
const currentBlockTo = currentBlock.content.to;
|
||||
|
||||
// 基于最新状态创建更新
|
||||
view.dispatch({
|
||||
@@ -88,16 +88,16 @@ export const formatBlockContent = (view) => {
|
||||
selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input"
|
||||
})
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to apply formatting changes:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 执行异步格式化
|
||||
performFormat()
|
||||
return true // 立即返回 true,表示命令已开始执行
|
||||
}
|
||||
performFormat();
|
||||
return true; // 立即返回 true,表示命令已开始执行
|
||||
};
|
||||
@@ -49,8 +49,8 @@ export const CodeBlockLanguage = LRLanguage.define({
|
||||
* 创建代码块语言支持
|
||||
*/
|
||||
export function codeBlockLang() {
|
||||
let wrap = configureNesting();
|
||||
let lang = CodeBlockLanguage.configure({ dialect: "", wrap: wrap });
|
||||
const wrap = configureNesting();
|
||||
const lang = CodeBlockLanguage.configure({ dialect: "", wrap: wrap });
|
||||
|
||||
return [
|
||||
new LanguageSupport(lang, [json().support]),
|
||||
|
||||
@@ -32,17 +32,17 @@ import {dockerFile} from "@codemirror/legacy-modes/mode/dockerfile";
|
||||
import {lua} from "@codemirror/legacy-modes/mode/lua";
|
||||
import {SupportedLanguage} from '../types';
|
||||
|
||||
import typescriptPlugin from "prettier/plugins/typescript"
|
||||
import babelPrettierPlugin from "prettier/plugins/babel"
|
||||
import htmlPrettierPlugin from "prettier/plugins/html"
|
||||
import cssPrettierPlugin from "prettier/plugins/postcss"
|
||||
import markdownPrettierPlugin from "prettier/plugins/markdown"
|
||||
import yamlPrettierPlugin from "prettier/plugins/yaml"
|
||||
import goPrettierPlugin from "@/common/prettier/plugins/go"
|
||||
import sqlPrettierPlugin from "@/common/prettier/plugins/sql"
|
||||
import phpPrettierPlugin from "@/common/prettier/plugins/php"
|
||||
import javaPrettierPlugin from "@/common/prettier/plugins/java"
|
||||
import xmlPrettierPlugin from "@prettier/plugin-xml"
|
||||
import typescriptPlugin from "prettier/plugins/typescript";
|
||||
import babelPrettierPlugin from "prettier/plugins/babel";
|
||||
import htmlPrettierPlugin from "prettier/plugins/html";
|
||||
import cssPrettierPlugin from "prettier/plugins/postcss";
|
||||
import markdownPrettierPlugin from "prettier/plugins/markdown";
|
||||
import yamlPrettierPlugin from "prettier/plugins/yaml";
|
||||
import goPrettierPlugin from "@/common/prettier/plugins/go";
|
||||
import sqlPrettierPlugin from "@/common/prettier/plugins/sql";
|
||||
import phpPrettierPlugin from "@/common/prettier/plugins/php";
|
||||
import javaPrettierPlugin from "@/common/prettier/plugins/java";
|
||||
import xmlPrettierPlugin from "@prettier/plugin-xml";
|
||||
import shellPrettierPlugin from "@/common/prettier/plugins/shell";
|
||||
import dockerfilePrettierPlugin from "@/common/prettier/plugins/docker";
|
||||
import rustPrettierPlugin from "@/common/prettier/plugins/rust";
|
||||
|
||||
@@ -13,11 +13,11 @@ import { languageMapping } from "./languages";
|
||||
*/
|
||||
export function configureNesting() {
|
||||
return parseMixed((node, input) => {
|
||||
let id = node.type.id;
|
||||
const id = node.type.id;
|
||||
|
||||
if (id === BlockContent) {
|
||||
// 获取父节点中的语言标记
|
||||
let blockLang = node.node.parent?.firstChild?.getChildren(BlockLanguage)[0];
|
||||
const blockLang = node.node.parent?.firstChild?.getChildren(BlockLanguage)[0];
|
||||
let langName = blockLang ? input.read(blockLang.from, blockLang.to) : null;
|
||||
|
||||
// 如果 BlockContent 为空,不返回解析器
|
||||
|
||||
@@ -63,13 +63,13 @@ class MathResult extends WidgetType {
|
||||
* 数学装饰函数
|
||||
*/
|
||||
function mathDeco(view: any): any {
|
||||
let mathParsers = new WeakMap();
|
||||
let builder = new RangeSetBuilder();
|
||||
const mathParsers = new WeakMap();
|
||||
const builder = new RangeSetBuilder();
|
||||
|
||||
for (let { from, to } of view.visibleRanges) {
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
for (let pos = from; pos <= to;) {
|
||||
let line = view.state.doc.lineAt(pos);
|
||||
var block = getNoteBlockFromPos(view.state, pos);
|
||||
const line = view.state.doc.lineAt(pos);
|
||||
const block = getNoteBlockFromPos(view.state, pos);
|
||||
|
||||
if (block && block.language.name === "math") {
|
||||
// get math.js parser and cache it for this block
|
||||
@@ -97,7 +97,7 @@ function mathDeco(view: any): any {
|
||||
|
||||
// if we got a result from math.js, add the result decoration
|
||||
if (result !== undefined) {
|
||||
let format = parser?.get("format");
|
||||
const format = parser?.get("format");
|
||||
|
||||
let resultWidget: MathResult | undefined;
|
||||
if (typeof(result) === "string") {
|
||||
|
||||
@@ -21,11 +21,11 @@ const tokenRegEx = new RegExp(`^∞∞∞(${languageTokensMatcher})(-a)?$`, "g")
|
||||
* 获取选中的行块
|
||||
*/
|
||||
function selectedLineBlocks(state: any): LineBlock[] {
|
||||
let blocks: LineBlock[] = [];
|
||||
const blocks: LineBlock[] = [];
|
||||
let upto = -1;
|
||||
|
||||
for (let range of state.selection.ranges) {
|
||||
let startLine = state.doc.lineAt(range.from);
|
||||
for (const range of state.selection.ranges) {
|
||||
const startLine = state.doc.lineAt(range.from);
|
||||
let endLine = state.doc.lineAt(range.to);
|
||||
|
||||
if (!range.empty && range.to == endLine.from) {
|
||||
@@ -33,7 +33,7 @@ function selectedLineBlocks(state: any): LineBlock[] {
|
||||
}
|
||||
|
||||
if (upto >= startLine.number) {
|
||||
let prev = blocks[blocks.length - 1];
|
||||
const prev = blocks[blocks.length - 1];
|
||||
prev.to = endLine.to;
|
||||
prev.ranges.push(range);
|
||||
} else {
|
||||
@@ -57,15 +57,15 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
let changes: any[] = [];
|
||||
let ranges: SelectionRange[] = [];
|
||||
const changes: any[] = [];
|
||||
const ranges: SelectionRange[] = [];
|
||||
|
||||
for (let block of selectedLineBlocks(state)) {
|
||||
for (const 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);
|
||||
const nextLine = state.doc.lineAt(forward ? block.to + 1 : block.from - 1);
|
||||
let previousLine;
|
||||
|
||||
if (!forward ? block.to == state.doc.length : block.from == 0) {
|
||||
@@ -76,8 +76,8 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
|
||||
|
||||
// 如果整个选择是一个被分隔符包围的块,我们需要在分隔符之间添加额外的换行符
|
||||
// 以避免创建两个只有单个换行符的分隔符,这会导致语法解析器无法解析
|
||||
let nextLineIsSeparator = nextLine.text.match(tokenRegEx);
|
||||
let blockSurroundedBySeparators = previousLine !== null &&
|
||||
const nextLineIsSeparator = nextLine.text.match(tokenRegEx);
|
||||
const blockSurroundedBySeparators = previousLine !== null &&
|
||||
previousLine.text.match(tokenRegEx) && nextLineIsSeparator;
|
||||
|
||||
let size = nextLine.length + 1;
|
||||
@@ -96,7 +96,7 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
for (let r of block.ranges) {
|
||||
for (const r of block.ranges) {
|
||||
ranges.push(EditorSelection.range(
|
||||
Math.min(state.doc.length, r.anchor + size),
|
||||
Math.min(state.doc.length, r.head + size)
|
||||
@@ -108,7 +108,7 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
|
||||
{ from: nextLine.from, to: block.from },
|
||||
{ from: block.to, insert: state.lineBreak + nextLine.text + state.lineBreak }
|
||||
);
|
||||
for (let r of block.ranges) {
|
||||
for (const r of block.ranges) {
|
||||
ranges.push(EditorSelection.range(r.anchor - size, r.head - size));
|
||||
}
|
||||
} else {
|
||||
@@ -116,7 +116,7 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
|
||||
{ from: nextLine.from, to: block.from },
|
||||
{ from: block.to, insert: state.lineBreak + nextLine.text }
|
||||
);
|
||||
for (let r of block.ranges) {
|
||||
for (const r of block.ranges) {
|
||||
ranges.push(EditorSelection.range(r.anchor - size, r.head - size));
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
|
||||
export const moveLineUp = ({ state, dispatch }: { state: any; dispatch: any }): boolean => {
|
||||
// 防止移动行到第一个块分隔符之前
|
||||
if (state.selection.ranges.some((range: SelectionRange) => {
|
||||
let startLine = state.doc.lineAt(range.from);
|
||||
const startLine = state.doc.lineAt(range.from);
|
||||
return startLine.from <= state.field(blockState)[0].content.from;
|
||||
})) {
|
||||
return true;
|
||||
|
||||
@@ -27,7 +27,7 @@ export const emptyBlockSelected = StateField.define<number | null>({
|
||||
// 如果选择改变,重置状态
|
||||
return null;
|
||||
} else {
|
||||
for (let e of tr.effects) {
|
||||
for (const e of tr.effects) {
|
||||
if (e.is(setEmptyBlockSelected)) {
|
||||
// 切换状态为 true
|
||||
return e.value;
|
||||
@@ -164,8 +164,8 @@ export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) =>
|
||||
|
||||
// 如果选择在一个块内,确保不超出块边界
|
||||
if (fromBlock) {
|
||||
let newFrom = Math.max(range.from, fromBlock.content.from);
|
||||
let newTo = Math.min(range.to, fromBlock.content.to);
|
||||
const newFrom = Math.max(range.from, fromBlock.content.from);
|
||||
const newTo = Math.min(range.to, fromBlock.content.to);
|
||||
|
||||
if (newFrom !== range.from || newTo !== range.to) {
|
||||
needsCorrection = true;
|
||||
|
||||
@@ -14,7 +14,7 @@ export const transposeChars = ({ state, dispatch }: { state: any; dispatch: any
|
||||
return false;
|
||||
}
|
||||
|
||||
let changes = state.changeByRange((range: any) => {
|
||||
const changes = state.changeByRange((range: any) => {
|
||||
// 防止在代码块的开始或结束位置进行字符转置,因为这会破坏块语法
|
||||
const block = getNoteBlockFromPos(state, range.from);
|
||||
if (block && (range.from === block.content.from || range.from === block.content.to)) {
|
||||
@@ -25,10 +25,10 @@ export const transposeChars = ({ state, dispatch }: { state: any; dispatch: any
|
||||
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;
|
||||
const pos = range.from;
|
||||
const line = state.doc.lineAt(pos);
|
||||
const from = pos == line.from ? pos - 1 : findClusterBreak(line.text, pos - line.from, false) + line.from;
|
||||
const to = pos == line.to ? pos + 1 : findClusterBreak(line.text, pos - line.from, true) + line.from;
|
||||
|
||||
return {
|
||||
changes: {
|
||||
|
||||
@@ -240,7 +240,7 @@ export const colorView = (showPicker: boolean = true) =>
|
||||
const comma = (target.dataset.colorraw || '').indexOf(',') > 4;
|
||||
let converted = target.value;
|
||||
if (data.colorType === ColorType.rgb) {
|
||||
let funName = colorraw?.match(/^(rgba?)/) ? colorraw?.match(/^(rgba?)/)![0] : undefined;
|
||||
const funName = colorraw?.match(/^(rgba?)/) ? colorraw?.match(/^(rgba?)/)![0] : undefined;
|
||||
if (comma) {
|
||||
converted = rgb
|
||||
? `${funName}(${rgb.r}, ${rgb.g}, ${rgb.b}${data.alpha ? ', ' + data.alpha.trim() : ''})`
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import {foldService} from '@codemirror/language';
|
||||
|
||||
export const foldingOnIndent = foldService.of((state, from, to) => {
|
||||
const line = state.doc.lineAt(from) // First line
|
||||
const lines = state.doc.lines // Number of lines in the document
|
||||
const indent = line.text.search(/\S|$/) // Indent level of the first line
|
||||
let foldStart = from // Start of the fold
|
||||
let foldEnd = to // End of the fold
|
||||
const line = state.doc.lineAt(from); // First line
|
||||
const lines = state.doc.lines; // Number of lines in the document
|
||||
const indent = line.text.search(/\S|$/); // Indent level of the first line
|
||||
let foldStart = from; // Start of the fold
|
||||
let foldEnd = to; // End of the fold
|
||||
|
||||
// Check the next line if it is on a deeper indent level
|
||||
// If it is, check the next line and so on
|
||||
// If it is not, go on with the foldEnd
|
||||
let nextLine = line
|
||||
let nextLine = line;
|
||||
while (nextLine.number < lines) {
|
||||
nextLine = state.doc.line(nextLine.number + 1) // Next line
|
||||
const nextIndent = nextLine.text.search(/\S|$/) // Indent level of the next line
|
||||
nextLine = state.doc.line(nextLine.number + 1); // Next line
|
||||
const nextIndent = nextLine.text.search(/\S|$/); // Indent level of the next line
|
||||
|
||||
// If the next line is on a deeper indent level, add it to the fold
|
||||
if (nextIndent > indent) {
|
||||
foldEnd = nextLine.to // Set the fold end to the end of the next line
|
||||
foldEnd = nextLine.to; // Set the fold end to the end of the next line
|
||||
} else {
|
||||
break // If the next line is not on a deeper indent level, stop
|
||||
break; // If the next line is not on a deeper indent level, stop
|
||||
}
|
||||
}
|
||||
|
||||
// If the fold is only one line, don't fold it
|
||||
if (state.doc.lineAt(foldStart).number === state.doc.lineAt(foldEnd).number) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the fold start to the end of the first line
|
||||
// With this, the fold will not include the first line
|
||||
foldStart = line.to
|
||||
foldStart = line.to;
|
||||
|
||||
// Return a fold that covers the entire indent level
|
||||
return {from: foldStart, to: foldEnd}
|
||||
return {from: foldStart, to: foldEnd};
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ViewUpdate,
|
||||
} from '@codemirror/view';
|
||||
import { Extension, Range } from '@codemirror/state';
|
||||
import * as runtime from "@wailsio/runtime"
|
||||
import * as runtime from "@wailsio/runtime";
|
||||
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
|
||||
const defaultRegexp = /\b((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)\b/gi;
|
||||
|
||||
@@ -213,7 +213,7 @@ export const hyperLinkClickHandler = EditorView.domEventHandlers({
|
||||
if (urlElement && urlElement.hasAttribute('data-url')) {
|
||||
const url = urlElement.getAttribute('data-url');
|
||||
if (url) {
|
||||
runtime.Browser.OpenURL(url)
|
||||
runtime.Browser.OpenURL(url);
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export type Options = {
|
||||
const Config = Facet.define<MinimapConfig | null, Required<Options>>({
|
||||
combine: (c) => {
|
||||
const configs: Array<Options> = [];
|
||||
for (let config of c) {
|
||||
for (const config of c) {
|
||||
if (!config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -24,4 +24,4 @@ function drawLineGutter(gutter: Record<Line, Color>, ctx: DrawContext, lineNumbe
|
||||
}
|
||||
|
||||
|
||||
export { GUTTER_WIDTH, drawLineGutter }
|
||||
export { GUTTER_WIDTH, drawLineGutter };
|
||||
@@ -218,7 +218,7 @@ const minimapClass = ViewPlugin.fromClass(
|
||||
/* Small leading buffer */
|
||||
drawContext.offsetX += 2;
|
||||
|
||||
for (let gutter of gutters) {
|
||||
for (const gutter of gutters) {
|
||||
drawLineGutter(gutter, drawContext, i + 1);
|
||||
drawContext.offsetX += GUTTER_WIDTH;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ function computeLinesState(state: EditorState): Lines {
|
||||
|
||||
while (!lineCursor.done) {
|
||||
const lineText = lineCursor.value;
|
||||
let from = textOffset;
|
||||
let to = from + lineText.length;
|
||||
const from = textOffset;
|
||||
const to = from + lineText.length;
|
||||
|
||||
// Iterate through folded ranges until we're at or past the current line
|
||||
while (foldedRangeCursor.value && foldedRangeCursor.to < from) {
|
||||
@@ -34,8 +34,8 @@ function computeLinesState(state: EditorState): Lines {
|
||||
const lineEndsInFold = to > foldFrom && to <= foldTo;
|
||||
|
||||
if (lineStartInFold) {
|
||||
let lastLine = lines.pop() ?? [];
|
||||
let lastRange = lastLine.pop();
|
||||
const lastLine = lines.pop() ?? [];
|
||||
const lastRange = lastLine.pop();
|
||||
|
||||
// If the last range is folded, we extend the folded range
|
||||
if (lastRange && lastRange.folded) {
|
||||
|
||||
@@ -144,7 +144,7 @@ export class SelectionState extends LineBasedState<Array<Selection>> {
|
||||
}
|
||||
|
||||
public drawLine(ctx: DrawContext, lineNumber: number) {
|
||||
let {
|
||||
const {
|
||||
context,
|
||||
lineHeight,
|
||||
charWidth,
|
||||
|
||||
@@ -33,7 +33,7 @@ const removeHighlight = StateEffect.define<{from: number, to: number}>({
|
||||
// 配置facet
|
||||
const highlightConfigFacet = Facet.define<TextHighlightConfig, Required<TextHighlightConfig>>({
|
||||
combine: (configs) => {
|
||||
let result = { ...DEFAULT_CONFIG };
|
||||
const result = { ...DEFAULT_CONFIG };
|
||||
for (const config of configs) {
|
||||
if (config.backgroundColor !== undefined) {
|
||||
result.backgroundColor = config.backgroundColor;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findNext, findPrevious, getSearchQuery, RegExpCursor, replaceAll, replaceNext, SearchCursor, SearchQuery, setSearchQuery } from "@codemirror/search";
|
||||
import { getSearchQuery, RegExpCursor, SearchCursor, SearchQuery, setSearchQuery } from "@codemirror/search";
|
||||
import { CharCategory, EditorState, findClusterBreak, Text } from "@codemirror/state";
|
||||
import { SearchVisibilityEffect } from "./state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
@@ -9,7 +9,7 @@ type Match = { from: number, to: number };
|
||||
export class CustomSearchPanel {
|
||||
dom!: HTMLElement;
|
||||
searchField!: HTMLInputElement;
|
||||
replaceField!: HTMLInputElement
|
||||
replaceField!: HTMLInputElement;
|
||||
matchCountField!: HTMLElement;
|
||||
currentMatch!: number;
|
||||
matches!: Match[];
|
||||
@@ -34,7 +34,7 @@ export class CustomSearchPanel {
|
||||
"close": '<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"/></svg>',
|
||||
"replace": '<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M3.221 3.739l2.261 2.269L7.7 3.784l-.7-.7-1.012 1.007-.008-1.6a.523.523 0 0 1 .5-.526H8V1H6.48A1.482 1.482 0 0 0 5 2.489V4.1L3.927 3.033l-.706.706zm6.67 1.794h.01c.183.311.451.467.806.467.393 0 .706-.168.94-.503.236-.335.353-.78.353-1.333 0-.511-.1-.913-.301-1.207-.201-.295-.488-.442-.86-.442-.405 0-.718.194-.938.581h-.01V1H9v4.919h.89v-.386zm-.015-1.061v-.34c0-.248.058-.448.175-.601a.54.54 0 0 1 .445-.23.49.49 0 0 1 .436.233c.104.154.155.368.155.643 0 .33-.056.587-.169.768a.524.524 0 0 1-.47.27.495.495 0 0 1-.411-.211.853.853 0 0 1-.16-.532zM9 12.769c-.256.154-.625.231-1.108.231-.563 0-1.02-.178-1.369-.533-.349-.355-.523-.813-.523-1.374 0-.648.186-1.158.56-1.53.374-.376.875-.563 1.5-.563.433 0 .746.06.94.179v.998a1.26 1.26 0 0 0-.792-.276c-.325 0-.583.1-.774.298-.19.196-.283.468-.283.816 0 .338.09.603.272.797.182.191.431.287.749.287.282 0 .558-.092.828-.276v.946zM4 7L3 8v6l1 1h7l1-1V8l-1-1H4zm0 1h7v6H4V8z"/></svg>',
|
||||
"replaceAll": '<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.6 2.677c.147-.31.356-.465.626-.465.248 0 .44.118.573.353.134.236.201.557.201.966 0 .443-.078.798-.235 1.067-.156.268-.365.402-.627.402-.237 0-.416-.125-.537-.374h-.008v.31H11V1h.593v1.677h.008zm-.016 1.1a.78.78 0 0 0 .107.426c.071.113.163.169.274.169.136 0 .24-.072.314-.216.075-.145.113-.35.113-.615 0-.22-.035-.39-.104-.514-.067-.124-.164-.187-.29-.187-.12 0-.219.062-.297.185a.886.886 0 0 0-.117.48v.272zM4.12 7.695L2 5.568l.662-.662 1.006 1v-1.51A1.39 1.39 0 0 1 5.055 3H7.4v.905H5.055a.49.49 0 0 0-.468.493l.007 1.5.949-.944.656.656-2.08 2.085zM9.356 4.93H10V3.22C10 2.408 9.685 2 9.056 2c-.135 0-.285.024-.45.073a1.444 1.444 0 0 0-.388.167v.665c.237-.203.487-.304.75-.304.261 0 .392.156.392.469l-.6.103c-.506.086-.76.406-.76.961 0 .263.061.473.183.631A.61.61 0 0 0 8.69 5c.29 0 .509-.16.657-.48h.009v.41zm.004-1.355v.193a.75.75 0 0 1-.12.436.368.368 0 0 1-.313.17.276.276 0 0 1-.22-.095.38.38 0 0 1-.08-.248c0-.222.11-.351.332-.389l.4-.067zM7 12.93h-.644v-.41h-.009c-.148.32-.367.48-.657.48a.61.61 0 0 1-.507-.235c-.122-.158-.183-.368-.183-.63 0-.556.254-.876.76-.962l.6-.103c0-.313-.13-.47-.392-.47-.263 0-.513.102-.75.305v-.665c.095-.063.224-.119.388-.167.165-.049.315-.073.45-.073.63 0 .944.407.944 1.22v1.71zm-.64-1.162v-.193l-.4.068c-.222.037-.333.166-.333.388 0 .1.027.183.08.248a.276.276 0 0 0 .22.095.368.368 0 0 0 .312-.17c.08-.116.12-.26.12-.436zM9.262 13c.321 0 .568-.058.738-.173v-.71a.9.9 0 0 1-.552.207.619.619 0 0 1-.5-.215c-.12-.145-.181-.345-.181-.598 0-.26.063-.464.189-.612a.644.644 0 0 1 .516-.223c.194 0 .37.069.528.207v-.749c-.129-.09-.338-.134-.626-.134-.417 0-.751.14-1.001.422-.249.28-.373.662-.373 1.148 0 .42.116.764.349 1.03.232.267.537.4.913.4zM2 9l1-1h9l1 1v5l-1 1H3l-1-1V9zm1 0v5h9V9H3zm3-2l1-1h7l1 1v5l-1 1V7H6z"/></svg>',
|
||||
}
|
||||
};
|
||||
|
||||
constructor(readonly view: EditorView) {
|
||||
try {
|
||||
@@ -63,7 +63,7 @@ export class CustomSearchPanel {
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(`ERROR: ${err}`)
|
||||
console.warn(`ERROR: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,24 +84,24 @@ export class CustomSearchPanel {
|
||||
}
|
||||
|
||||
private charBefore(str: string, index: number) {
|
||||
return str.slice(findClusterBreak(str, index, false), index)
|
||||
return str.slice(findClusterBreak(str, index, false), index);
|
||||
}
|
||||
|
||||
private charAfter(str: string, index: number) {
|
||||
return str.slice(index, findClusterBreak(str, index))
|
||||
return str.slice(index, findClusterBreak(str, index));
|
||||
}
|
||||
|
||||
private stringWordTest(doc: Text, categorizer: (ch: string) => CharCategory) {
|
||||
return (from: number, to: number, buf: string, bufPos: number) => {
|
||||
if (bufPos > from || bufPos + buf.length < to) {
|
||||
bufPos = Math.max(0, from - 2)
|
||||
buf = doc.sliceString(bufPos, Math.min(doc.length, to + 2))
|
||||
bufPos = Math.max(0, from - 2);
|
||||
buf = doc.sliceString(bufPos, Math.min(doc.length, to + 2));
|
||||
}
|
||||
return (categorizer(this.charBefore(buf, from - bufPos)) != CharCategory.Word ||
|
||||
categorizer(this.charAfter(buf, from - bufPos)) != CharCategory.Word) &&
|
||||
(categorizer(this.charAfter(buf, to - bufPos)) != CharCategory.Word ||
|
||||
categorizer(this.charBefore(buf, to - bufPos)) != CharCategory.Word)
|
||||
}
|
||||
categorizer(this.charBefore(buf, to - bufPos)) != CharCategory.Word);
|
||||
};
|
||||
}
|
||||
|
||||
private regexpWordTest(categorizer: (ch: string) => CharCategory) {
|
||||
@@ -110,7 +110,7 @@ export class CustomSearchPanel {
|
||||
(categorizer(this.charBefore(match.input, match.index)) != CharCategory.Word ||
|
||||
categorizer(this.charAfter(match.input, match.index)) != CharCategory.Word) &&
|
||||
(categorizer(this.charAfter(match.input, match.index + match[0].length)) != CharCategory.Word ||
|
||||
categorizer(this.charBefore(match.input, match.index + match[0].length)) != CharCategory.Word)
|
||||
categorizer(this.charBefore(match.input, match.index + match[0].length)) != CharCategory.Word);
|
||||
}
|
||||
|
||||
|
||||
@@ -123,11 +123,11 @@ export class CustomSearchPanel {
|
||||
*/
|
||||
findMatchesAndSelectClosest(state: EditorState): void {
|
||||
const cursorPos = state.selection.main.head;
|
||||
let query = getSearchQuery(state);
|
||||
const query = getSearchQuery(state);
|
||||
|
||||
if (query.regexp) {
|
||||
try {
|
||||
this.regexCursor = new RegExpCursor(state.doc, query.search)
|
||||
this.regexCursor = new RegExpCursor(state.doc, query.search);
|
||||
this.searchCursor = undefined;
|
||||
} catch (error) {
|
||||
// 如果正则表达式无效,清空匹配结果并显示错误状态
|
||||
@@ -143,10 +143,10 @@ export class CustomSearchPanel {
|
||||
}
|
||||
}
|
||||
else {
|
||||
let cursor = new SearchCursor(state.doc, query.search);
|
||||
const cursor = new SearchCursor(state.doc, query.search);
|
||||
if (cursor !== this.searchCursor) {
|
||||
this.searchCursor = cursor;
|
||||
this.regexCursor = undefined
|
||||
this.regexCursor = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ export class CustomSearchPanel {
|
||||
}
|
||||
else if (this.regexCursor) {
|
||||
try {
|
||||
const matchWord = this.regexpWordTest(state.charCategorizer(state.selection.main.head))
|
||||
const matchWord = this.regexpWordTest(state.charCategorizer(state.selection.main.head));
|
||||
|
||||
while (!this.regexCursor.done) {
|
||||
this.regexCursor.next();
|
||||
@@ -228,13 +228,13 @@ export class CustomSearchPanel {
|
||||
caseSensitive: this.matchCase,
|
||||
regexp: this.useRegex,
|
||||
wholeWord: this.matchWord,
|
||||
})
|
||||
});
|
||||
|
||||
let query = getSearchQuery(this.view.state)
|
||||
const query = getSearchQuery(this.view.state);
|
||||
if (!newQuery.eq(query)) {
|
||||
this.view.dispatch({
|
||||
effects: setSearchQuery.of(newQuery)
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果创建SearchQuery时出错(通常是无效的正则表达式),记录错误但不中断程序
|
||||
@@ -243,7 +243,7 @@ export class CustomSearchPanel {
|
||||
}
|
||||
|
||||
private svgIcon(name: keyof CustomSearchPanel['codicon']): HTMLDivElement {
|
||||
let div = crelt("div", {},
|
||||
const div = crelt("div", {},
|
||||
) as HTMLDivElement;
|
||||
|
||||
div.innerHTML = this.codicon[name];
|
||||
@@ -251,14 +251,14 @@ export class CustomSearchPanel {
|
||||
}
|
||||
|
||||
public toggleReplace() {
|
||||
this.replaceVisibile = !this.replaceVisibile
|
||||
this.replaceVisibile = !this.replaceVisibile;
|
||||
const replaceBar = this.dom.querySelector(".replace-bar") as HTMLElement;
|
||||
const replaceButtons = this.dom.querySelector(".replace-buttons") as HTMLElement;
|
||||
const toggleIcon = this.dom.querySelector(".toggle-replace") as HTMLElement;
|
||||
if (replaceBar && toggleIcon && replaceButtons) {
|
||||
replaceBar.style.display = this.replaceVisibile ? "flex" : "none";
|
||||
replaceButtons.style.display = this.replaceVisibile ? "flex" : "none";
|
||||
toggleIcon.innerHTML = this.svgIcon(this.replaceVisibile ? "downChevron" : "rightChevron").innerHTML
|
||||
toggleIcon.innerHTML = this.svgIcon(this.replaceVisibile ? "downChevron" : "rightChevron").innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export class CustomSearchPanel {
|
||||
this.matchCase = !this.matchCase;
|
||||
const toggleIcon = this.dom.querySelector(".case-sensitive-toggle") as HTMLElement;
|
||||
if (toggleIcon) {
|
||||
toggleIcon.classList.toggle("active")
|
||||
toggleIcon.classList.toggle("active");
|
||||
}
|
||||
this.commit();
|
||||
// 重新搜索以应用新的匹配规则
|
||||
@@ -286,7 +286,7 @@ export class CustomSearchPanel {
|
||||
this.matchWord = !this.matchWord;
|
||||
const toggleIcon = this.dom.querySelector(".whole-word-toggle") as HTMLElement;
|
||||
if (toggleIcon) {
|
||||
toggleIcon.classList.toggle("active")
|
||||
toggleIcon.classList.toggle("active");
|
||||
}
|
||||
this.commit();
|
||||
// 重新搜索以应用新的匹配规则
|
||||
@@ -299,7 +299,7 @@ export class CustomSearchPanel {
|
||||
this.useRegex = !this.useRegex;
|
||||
const toggleIcon = this.dom.querySelector(".regex-toggle") as HTMLElement;
|
||||
if (toggleIcon) {
|
||||
toggleIcon.classList.toggle("active")
|
||||
toggleIcon.classList.toggle("active");
|
||||
}
|
||||
this.commit();
|
||||
// 重新搜索以应用新的匹配规则
|
||||
@@ -341,11 +341,11 @@ export class CustomSearchPanel {
|
||||
}
|
||||
|
||||
public findReplaceMatch() {
|
||||
let query = getSearchQuery(this.view.state)
|
||||
const query = getSearchQuery(this.view.state);
|
||||
if (query.replace) {
|
||||
this.replace()
|
||||
this.replace();
|
||||
} else {
|
||||
this.matchNext()
|
||||
this.matchNext();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ export class CustomSearchPanel {
|
||||
|
||||
private buildUI(): void {
|
||||
|
||||
let query = getSearchQuery(this.view.state)
|
||||
const query = getSearchQuery(this.view.state);
|
||||
|
||||
this.searchField = crelt("input", {
|
||||
value: query?.search ?? "",
|
||||
@@ -420,14 +420,14 @@ export class CustomSearchPanel {
|
||||
}) as HTMLInputElement;
|
||||
|
||||
|
||||
let caseField = this.svgIcon("matchCase");
|
||||
const caseField = this.svgIcon("matchCase");
|
||||
caseField.className = "case-sensitive-toggle";
|
||||
caseField.title = "Match Case (Alt+C)";
|
||||
caseField.addEventListener("click", () => {
|
||||
this.toggleCase();
|
||||
});
|
||||
|
||||
let wordField = this.svgIcon("wholeWord");
|
||||
const wordField = this.svgIcon("wholeWord");
|
||||
wordField.className = "whole-word-toggle";
|
||||
wordField.title = "Match Whole Word (Alt+W)";
|
||||
wordField.addEventListener("click", () => {
|
||||
@@ -435,50 +435,50 @@ export class CustomSearchPanel {
|
||||
});
|
||||
|
||||
|
||||
let reField = this.svgIcon("regex");
|
||||
const reField = this.svgIcon("regex");
|
||||
reField.className = "regex-toggle";
|
||||
reField.title = "Use Regular Expression (Alt+R)";
|
||||
reField.addEventListener("click", () => {
|
||||
this.toggleRegex();
|
||||
});
|
||||
|
||||
let toggleReplaceIcon = this.svgIcon(this.replaceVisibile ? "downChevron" : "rightChevron");
|
||||
const toggleReplaceIcon = this.svgIcon(this.replaceVisibile ? "downChevron" : "rightChevron");
|
||||
toggleReplaceIcon.className = "toggle-replace";
|
||||
toggleReplaceIcon.addEventListener("click", () => {
|
||||
this.toggleReplace();
|
||||
});
|
||||
|
||||
this.matchCountField = crelt("span", { class: "match-count" }, "0 of 0")
|
||||
this.matchCountField = crelt("span", { class: "match-count" }, "0 of 0");
|
||||
|
||||
let prevMatchButton = this.svgIcon("prevMatch");
|
||||
const prevMatchButton = this.svgIcon("prevMatch");
|
||||
prevMatchButton.className = "prev-match";
|
||||
prevMatchButton.title = "Previous Match (Shift+Enter)";
|
||||
prevMatchButton.addEventListener("click", () => {
|
||||
this.matchPrevious();
|
||||
});
|
||||
|
||||
let nextMatchButton = this.svgIcon("nextMatch");
|
||||
const nextMatchButton = this.svgIcon("nextMatch");
|
||||
nextMatchButton.className = "next-match";
|
||||
nextMatchButton.title = "Next Match (Enter)";
|
||||
nextMatchButton.addEventListener("click", () => {
|
||||
this.matchNext();
|
||||
});
|
||||
|
||||
let closeButton = this.svgIcon("close");
|
||||
const closeButton = this.svgIcon("close");
|
||||
closeButton.className = "close";
|
||||
closeButton.title = "Close (Escape)"
|
||||
closeButton.title = "Close (Escape)";
|
||||
closeButton.addEventListener("click", () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
let replaceButton = this.svgIcon("replace");
|
||||
const replaceButton = this.svgIcon("replace");
|
||||
replaceButton.className = "replace-button";
|
||||
replaceButton.title = "Replace (Enter)";
|
||||
replaceButton.addEventListener("click", () => {
|
||||
this.replace();
|
||||
});
|
||||
|
||||
let replaceAllButton = this.svgIcon("replaceAll");
|
||||
const replaceAllButton = this.svgIcon("replaceAll");
|
||||
replaceAllButton.className = "replace-button";
|
||||
replaceAllButton.title = "Replace All (Ctrl+Alt+Enter)";
|
||||
replaceAllButton.addEventListener("click", () => {
|
||||
@@ -534,7 +534,7 @@ export class CustomSearchPanel {
|
||||
this.replaceField
|
||||
);
|
||||
|
||||
replaceBar.style.display = this.replaceVisibile ? "flex" : "none"
|
||||
replaceBar.style.display = this.replaceVisibile ? "flex" : "none";
|
||||
|
||||
const inputSection = crelt("div", { class: "input-section" },
|
||||
searchBar,
|
||||
@@ -545,7 +545,7 @@ export class CustomSearchPanel {
|
||||
prevMatchButton,
|
||||
nextMatchButton,
|
||||
closeButton
|
||||
)
|
||||
);
|
||||
|
||||
const searchButtons = crelt("div", { class: "button-group" },
|
||||
this.matchCountField,
|
||||
@@ -557,9 +557,9 @@ export class CustomSearchPanel {
|
||||
},
|
||||
replaceButton,
|
||||
replaceAllButton
|
||||
)
|
||||
);
|
||||
|
||||
replaceButtons.style.display = this.replaceVisibile ? "flex" : "none"
|
||||
replaceButtons.style.display = this.replaceVisibile ? "flex" : "none";
|
||||
|
||||
const actionSection = crelt("div", { class: "actions-section" },
|
||||
searchButtons,
|
||||
@@ -599,14 +599,14 @@ export class CustomSearchPanel {
|
||||
}
|
||||
|
||||
mount() {
|
||||
this.searchField.select()
|
||||
this.searchField.select();
|
||||
}
|
||||
|
||||
destroy?(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
get pos() { return 80 }
|
||||
get pos() { return 80; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ const isSearchActive = () : boolean => {
|
||||
return document.activeElement.classList.contains('find-input');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isReplaceActive = () : boolean => {
|
||||
if (document.activeElement){
|
||||
return document.activeElement.classList.contains('replace-input');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const selectAllCommand: Command = (view) => {
|
||||
if (isSearchActive() || isReplaceActive()) {
|
||||
@@ -26,7 +26,7 @@ export const selectAllCommand: Command = (view) => {
|
||||
else {
|
||||
view.dispatch({
|
||||
selection: { anchor: 0, head: view.state.doc.length }
|
||||
})
|
||||
});
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -37,7 +37,7 @@ export const deleteCharacterBackwards: Command = (view) => {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
deleteCharBackward(view)
|
||||
deleteCharBackward(view);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -48,7 +48,7 @@ export const deleteCharacterFowards: Command = (view) => {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
deleteCharForward(view)
|
||||
deleteCharForward(view);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -72,7 +72,7 @@ export const showSearchVisibilityCommand: Command = (view) => {
|
||||
|
||||
export const searchMoveCursorLeft: Command = (view) => {
|
||||
if (isSearchActive() || isReplaceActive()) {
|
||||
const input = document.activeElement as HTMLInputElement
|
||||
const input = document.activeElement as HTMLInputElement;
|
||||
const pos = input.selectionStart ?? 0;
|
||||
if (pos > 0) {
|
||||
input.selectionStart = input.selectionEnd = pos - 1;
|
||||
@@ -80,14 +80,14 @@ export const searchMoveCursorLeft: Command = (view) => {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
cursorCharLeft(view)
|
||||
cursorCharLeft(view);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const searchMoveCursorRight: Command = (view) => {
|
||||
if (isSearchActive() || isReplaceActive()) {
|
||||
const input = document.activeElement as HTMLInputElement
|
||||
const input = document.activeElement as HTMLInputElement;
|
||||
const pos = input.selectionStart ?? 0;
|
||||
if (pos < input.value.length) {
|
||||
input.selectionStart = input.selectionEnd = pos + 1;
|
||||
@@ -95,10 +95,10 @@ export const searchMoveCursorRight: Command = (view) => {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
cursorCharRight(view)
|
||||
cursorCharRight(view);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const hideSearchVisibilityCommand: Command = (view) => {
|
||||
view.dispatch({
|
||||
@@ -108,64 +108,64 @@ export const hideSearchVisibilityCommand: Command = (view) => {
|
||||
};
|
||||
|
||||
export const searchToggleCase: Command = (view) => {
|
||||
const plugin = view.plugin(VSCodeSearch)
|
||||
const plugin = view.plugin(VSCodeSearch);
|
||||
|
||||
if (!plugin) return false;
|
||||
|
||||
plugin.toggleCaseInsensitive();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchToggleWholeWord: Command = (view) => {
|
||||
const plugin = view.plugin(VSCodeSearch)
|
||||
const plugin = view.plugin(VSCodeSearch);
|
||||
|
||||
if (!plugin) return false;
|
||||
|
||||
plugin.toggleWholeWord();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchToggleRegex: Command = (view) => {
|
||||
const plugin = view.plugin(VSCodeSearch)
|
||||
const plugin = view.plugin(VSCodeSearch);
|
||||
|
||||
if (!plugin) return false;
|
||||
|
||||
plugin.toggleRegex();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchShowReplace: Command = (view) => {
|
||||
const plugin = view.plugin(VSCodeSearch)
|
||||
const plugin = view.plugin(VSCodeSearch);
|
||||
|
||||
if (!plugin) return false;
|
||||
|
||||
plugin.showReplace();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchFindReplaceMatch: Command = (view) => {
|
||||
const plugin = view.plugin(VSCodeSearch)
|
||||
const plugin = view.plugin(VSCodeSearch);
|
||||
|
||||
if (!plugin) return false;
|
||||
|
||||
plugin.findReplaceMatch();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchFindPrevious: Command = (view) => {
|
||||
const plugin = view.plugin(VSCodeSearch)
|
||||
const plugin = view.plugin(VSCodeSearch);
|
||||
|
||||
if (!plugin) return false;
|
||||
|
||||
plugin.findPrevious();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchReplaceAll: Command = (view) => {
|
||||
const plugin = view.plugin(VSCodeSearch)
|
||||
const plugin = view.plugin(VSCodeSearch);
|
||||
|
||||
if (!plugin) return false;
|
||||
|
||||
plugin.replaceAll();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export { VSCodeSearch, vscodeSearch} from "./plugin";
|
||||
export { searchVisibilityField, SearchVisibilityEffect } from "./state";
|
||||
export { searchBaseTheme } from "./theme"
|
||||
export * from "./commands"
|
||||
export { searchBaseTheme } from "./theme";
|
||||
export * from "./commands";
|
||||
@@ -20,8 +20,8 @@ export class SearchPlugin {
|
||||
}
|
||||
this.prevQuery = currentQuery;
|
||||
|
||||
for (let tr of update.transactions) {
|
||||
for (let e of tr.effects) {
|
||||
for (const tr of update.transactions) {
|
||||
for (const e of tr.effects) {
|
||||
if (e.is(SearchVisibilityEffect)) {
|
||||
this.searchControl.setVisibility(e.value);
|
||||
}
|
||||
@@ -55,19 +55,19 @@ export class SearchPlugin {
|
||||
}
|
||||
|
||||
findNext() {
|
||||
this.searchControl.matchNext()
|
||||
this.searchControl.matchNext();
|
||||
}
|
||||
|
||||
replace() {
|
||||
this.searchControl.replace()
|
||||
this.searchControl.replace();
|
||||
}
|
||||
|
||||
replaceAll() {
|
||||
this.searchControl.replaceAll()
|
||||
this.searchControl.replaceAll();
|
||||
}
|
||||
|
||||
findPrevious() {
|
||||
this.searchControl.matchPrevious()
|
||||
this.searchControl.matchPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,4 +77,4 @@ export const vscodeSearch = [
|
||||
search({}),
|
||||
VSCodeSearch,
|
||||
searchBaseTheme
|
||||
]
|
||||
];
|
||||
@@ -9,7 +9,7 @@ export const searchVisibilityField = StateField.define({
|
||||
return false;
|
||||
},
|
||||
update(value, tr) {
|
||||
for (let e of tr.effects) {
|
||||
for (const e of tr.effects) {
|
||||
if (e.is(SearchVisibilityEffect)) {
|
||||
return e.value;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ const sharedTheme: Theme = {
|
||||
alignItems: "center",
|
||||
gap: "2px",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const lightTheme: Theme = {
|
||||
".find-replace-container": {
|
||||
@@ -254,7 +254,7 @@ const prependThemeSelector = (theme: Theme, selector: string): Theme => {
|
||||
});
|
||||
|
||||
return updatedTheme;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchBaseTheme = EditorView.baseTheme({
|
||||
...sharedTheme,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function simulateBackspace(input: HTMLInputElement, direction: "backward" | "forward" = "backward") {
|
||||
let start = input.selectionStart ?? 0;
|
||||
let end = input.selectionEnd ?? 0;
|
||||
const end = input.selectionEnd ?? 0;
|
||||
|
||||
// Do nothing if at boundaries
|
||||
if (direction === "backward" && start === 0 && end === 0) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {KeyBindingCommand} from '@/../bindings/voidraft/internal/models/models'
|
||||
import {KeyBindingCommand} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {
|
||||
hideSearchVisibilityCommand,
|
||||
searchReplaceAll,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
searchToggleRegex,
|
||||
searchToggleWholeWord,
|
||||
showSearchVisibilityCommand
|
||||
} from '../extensions/vscodeSearch/commands'
|
||||
} from '../extensions/vscodeSearch/commands';
|
||||
import {
|
||||
addNewBlockAfterCurrent,
|
||||
addNewBlockAfterLast,
|
||||
@@ -20,13 +20,13 @@ import {
|
||||
moveCurrentBlockUp,
|
||||
selectNextBlock,
|
||||
selectPreviousBlock
|
||||
} from '../extensions/codeblock/commands'
|
||||
import {selectAll} from '../extensions/codeblock/selectAll'
|
||||
import {deleteLineCommand} from '../extensions/codeblock/deleteLine'
|
||||
import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines'
|
||||
import {transposeChars} from '../extensions/codeblock'
|
||||
import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste'
|
||||
import {textHighlightToggleCommand} from '../extensions/textHighlight/textHighlightExtension'
|
||||
} from '../extensions/codeblock/commands';
|
||||
import {selectAll} from '../extensions/codeblock/selectAll';
|
||||
import {deleteLineCommand} from '../extensions/codeblock/deleteLine';
|
||||
import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines';
|
||||
import {transposeChars} from '../extensions/codeblock';
|
||||
import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste';
|
||||
import {textHighlightToggleCommand} from '../extensions/textHighlight/textHighlightExtension';
|
||||
import {
|
||||
copyLineDown,
|
||||
copyLineUp,
|
||||
@@ -52,15 +52,15 @@ import {
|
||||
toggleComment,
|
||||
undo,
|
||||
undoSelection
|
||||
} from '@codemirror/commands'
|
||||
import {foldAll, foldCode, unfoldAll, unfoldCode} from '@codemirror/language'
|
||||
import i18n from '@/i18n'
|
||||
} from '@codemirror/commands';
|
||||
import {foldAll, foldCode, unfoldAll, unfoldCode} from '@codemirror/language';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
// 默认编辑器选项
|
||||
const defaultEditorOptions = {
|
||||
defaultBlockToken: 'text',
|
||||
defaultBlockAutoDetect: true,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 前端命令注册表
|
||||
@@ -291,7 +291,7 @@ export const commandRegistry = {
|
||||
handler: textHighlightToggleCommand,
|
||||
descriptionKey: 'keybindings.commands.textHighlightToggle'
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 获取命令处理函数
|
||||
@@ -299,8 +299,8 @@ export const commandRegistry = {
|
||||
* @returns 对应的处理函数,如果不存在则返回 undefined
|
||||
*/
|
||||
export const getCommandHandler = (command: KeyBindingCommand) => {
|
||||
return commandRegistry[command]?.handler
|
||||
}
|
||||
return commandRegistry[command]?.handler;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取命令描述
|
||||
@@ -308,9 +308,9 @@ export const getCommandHandler = (command: KeyBindingCommand) => {
|
||||
* @returns 对应的描述,如果不存在则返回 undefined
|
||||
*/
|
||||
export const getCommandDescription = (command: KeyBindingCommand) => {
|
||||
const descriptionKey = commandRegistry[command]?.descriptionKey
|
||||
return descriptionKey ? i18n.global.t(descriptionKey) : undefined
|
||||
}
|
||||
const descriptionKey = commandRegistry[command]?.descriptionKey;
|
||||
return descriptionKey ? i18n.global.t(descriptionKey) : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查命令是否已注册
|
||||
@@ -318,13 +318,13 @@ export const getCommandDescription = (command: KeyBindingCommand) => {
|
||||
* @returns 是否已注册
|
||||
*/
|
||||
export const isCommandRegistered = (command: KeyBindingCommand): boolean => {
|
||||
return command in commandRegistry
|
||||
}
|
||||
return command in commandRegistry;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有已注册的命令
|
||||
* @returns 已注册的命令列表
|
||||
*/
|
||||
export const getRegisteredCommands = (): KeyBindingCommand[] => {
|
||||
return Object.keys(commandRegistry) as KeyBindingCommand[]
|
||||
}
|
||||
return Object.keys(commandRegistry) as KeyBindingCommand[];
|
||||
};
|
||||
@@ -1,46 +1,46 @@
|
||||
import { Extension } from '@codemirror/state'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { KeymapManager } from './keymapManager'
|
||||
import { ExtensionID } from '@/../bindings/voidraft/internal/models/models'
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore';
|
||||
import { useExtensionStore } from '@/stores/extensionStore';
|
||||
import { KeymapManager } from './keymapManager';
|
||||
import { ExtensionID } from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
/**
|
||||
* 异步创建快捷键扩展
|
||||
* 确保快捷键配置和扩展配置已加载
|
||||
*/
|
||||
export const createDynamicKeymapExtension = async (): Promise<Extension> => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const extensionStore = useExtensionStore()
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const extensionStore = useExtensionStore();
|
||||
|
||||
// 确保快捷键配置已加载
|
||||
if (keybindingStore.keyBindings.length === 0) {
|
||||
await keybindingStore.loadKeyBindings()
|
||||
await keybindingStore.loadKeyBindings();
|
||||
}
|
||||
|
||||
// 确保扩展配置已加载
|
||||
if (extensionStore.extensions.length === 0) {
|
||||
await extensionStore.loadExtensions()
|
||||
await extensionStore.loadExtensions();
|
||||
}
|
||||
|
||||
// 获取启用的扩展ID列表
|
||||
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id)
|
||||
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
|
||||
|
||||
return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds)
|
||||
}
|
||||
return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新快捷键映射
|
||||
* @param view 编辑器视图
|
||||
*/
|
||||
export const updateKeymapExtension = (view: any): void => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const extensionStore = useExtensionStore()
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const extensionStore = useExtensionStore();
|
||||
|
||||
// 获取启用的扩展ID列表
|
||||
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id)
|
||||
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
|
||||
|
||||
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds)
|
||||
}
|
||||
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定扩展的快捷键
|
||||
@@ -48,11 +48,11 @@ export const updateKeymapExtension = (view: any): void => {
|
||||
* @returns 该扩展的快捷键列表
|
||||
*/
|
||||
export const getExtensionKeyBindings = (extensionId: ExtensionID) => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
return keybindingStore.getKeyBindingsByExtension(extensionId)
|
||||
}
|
||||
const keybindingStore = useKeybindingStore();
|
||||
return keybindingStore.getKeyBindingsByExtension(extensionId);
|
||||
};
|
||||
|
||||
// 导出相关模块
|
||||
export { KeymapManager } from './keymapManager'
|
||||
export { commandRegistry, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commandRegistry'
|
||||
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'
|
||||
export { KeymapManager } from './keymapManager';
|
||||
export { commandRegistry, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commandRegistry';
|
||||
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';
|
||||
@@ -1,15 +1,15 @@
|
||||
import {keymap} from '@codemirror/view'
|
||||
import {Extension, Compartment} from '@codemirror/state'
|
||||
import {KeyBinding as KeyBindingConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models'
|
||||
import {KeyBinding, KeymapResult} from './types'
|
||||
import {getCommandHandler, isCommandRegistered} from './commandRegistry'
|
||||
import {keymap} from '@codemirror/view';
|
||||
import {Extension, Compartment} from '@codemirror/state';
|
||||
import {KeyBinding as KeyBindingConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {KeyBinding, KeymapResult} from './types';
|
||||
import {getCommandHandler, isCommandRegistered} from './commandRegistry';
|
||||
|
||||
/**
|
||||
* 快捷键管理器
|
||||
* 负责将后端配置转换为CodeMirror快捷键扩展
|
||||
*/
|
||||
export class KeymapManager {
|
||||
private static compartment = new Compartment()
|
||||
private static compartment = new Compartment();
|
||||
|
||||
/**
|
||||
* 将后端快捷键配置转换为CodeMirror快捷键绑定
|
||||
@@ -18,28 +18,28 @@ export class KeymapManager {
|
||||
* @returns 转换结果
|
||||
*/
|
||||
static convertToKeyBindings(keyBindings: KeyBindingConfig[], enabledExtensions?: ExtensionID[]): KeymapResult {
|
||||
const result: KeyBinding[] = []
|
||||
const result: KeyBinding[] = [];
|
||||
|
||||
for (const binding of keyBindings) {
|
||||
// 跳过禁用的快捷键
|
||||
if (!binding.enabled) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果提供了扩展列表,则只处理启用扩展的快捷键
|
||||
if (enabledExtensions && !enabledExtensions.includes(binding.extension)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查命令是否已注册
|
||||
if (!isCommandRegistered(binding.command)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取命令处理函数
|
||||
const handler = getCommandHandler(binding.command)
|
||||
const handler = getCommandHandler(binding.command);
|
||||
if (!handler) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// 转换为CodeMirror快捷键格式
|
||||
@@ -47,12 +47,12 @@ export class KeymapManager {
|
||||
key: binding.key,
|
||||
run: handler,
|
||||
preventDefault: true
|
||||
}
|
||||
};
|
||||
|
||||
result.push(keyBinding)
|
||||
result.push(keyBinding);
|
||||
}
|
||||
|
||||
return {keyBindings: result}
|
||||
return {keyBindings: result};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,9 +63,9 @@ export class KeymapManager {
|
||||
*/
|
||||
static createKeymapExtension(keyBindings: KeyBindingConfig[], enabledExtensions?: ExtensionID[]): Extension {
|
||||
const {keyBindings: cmKeyBindings} =
|
||||
this.convertToKeyBindings(keyBindings, enabledExtensions)
|
||||
this.convertToKeyBindings(keyBindings, enabledExtensions);
|
||||
|
||||
return this.compartment.of(keymap.of(cmKeyBindings))
|
||||
return this.compartment.of(keymap.of(cmKeyBindings));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,11 +76,11 @@ export class KeymapManager {
|
||||
*/
|
||||
static updateKeymap(view: any, keyBindings: KeyBindingConfig[], enabledExtensions: ExtensionID[]): void {
|
||||
const {keyBindings: cmKeyBindings} =
|
||||
this.convertToKeyBindings(keyBindings, enabledExtensions)
|
||||
this.convertToKeyBindings(keyBindings, enabledExtensions);
|
||||
|
||||
view.dispatch({
|
||||
effects: this.compartment.reconfigure(keymap.of(cmKeyBindings))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,16 +89,16 @@ export class KeymapManager {
|
||||
* @returns 按扩展分组的快捷键映射
|
||||
*/
|
||||
static groupByExtension(keyBindings: KeyBindingConfig[]): Map<ExtensionID, KeyBindingConfig[]> {
|
||||
const groups = new Map<ExtensionID, KeyBindingConfig[]>()
|
||||
const groups = new Map<ExtensionID, KeyBindingConfig[]>();
|
||||
|
||||
for (const binding of keyBindings) {
|
||||
if (!groups.has(binding.extension)) {
|
||||
groups.set(binding.extension, [])
|
||||
groups.set(binding.extension, []);
|
||||
}
|
||||
groups.get(binding.extension)!.push(binding)
|
||||
groups.get(binding.extension)!.push(binding);
|
||||
}
|
||||
|
||||
return groups
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,17 +110,17 @@ export class KeymapManager {
|
||||
valid: KeyBindingConfig[]
|
||||
invalid: KeyBindingConfig[]
|
||||
} {
|
||||
const valid: KeyBindingConfig[] = []
|
||||
const invalid: KeyBindingConfig[] = []
|
||||
const valid: KeyBindingConfig[] = [];
|
||||
const invalid: KeyBindingConfig[] = [];
|
||||
|
||||
for (const binding of keyBindings) {
|
||||
if (binding.enabled && binding.key && isCommandRegistered(binding.command)) {
|
||||
valid.push(binding)
|
||||
valid.push(binding);
|
||||
} else {
|
||||
invalid.push(binding)
|
||||
invalid.push(binding);
|
||||
}
|
||||
}
|
||||
|
||||
return {valid, invalid}
|
||||
return {valid, invalid};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Command} from '@codemirror/view'
|
||||
import {Command} from '@codemirror/view';
|
||||
|
||||
/**
|
||||
* CodeMirror快捷键绑定格式
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Compartment, Extension, StateEffect} from '@codemirror/state'
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models'
|
||||
import {Compartment, Extension, StateEffect} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
/**
|
||||
* 扩展工厂接口
|
||||
@@ -56,18 +56,18 @@ interface EditorViewInfo {
|
||||
*/
|
||||
export class ExtensionManager {
|
||||
// 统一的扩展状态存储
|
||||
private extensionStates = new Map<ExtensionID, ExtensionState>()
|
||||
private extensionStates = new Map<ExtensionID, ExtensionState>();
|
||||
|
||||
// 编辑器视图管理
|
||||
private viewsMap = new Map<number, EditorViewInfo>()
|
||||
private activeViewId: number | null = null
|
||||
private viewsMap = new Map<number, EditorViewInfo>();
|
||||
private activeViewId: number | null = null;
|
||||
|
||||
// 注册的扩展工厂
|
||||
private extensionFactories = new Map<ExtensionID, ExtensionFactory>()
|
||||
private extensionFactories = new Map<ExtensionID, ExtensionFactory>();
|
||||
|
||||
// 防抖处理
|
||||
private debounceTimers = new Map<ExtensionID, number>()
|
||||
private debounceDelay = 300 // 默认防抖时间为300毫秒
|
||||
private debounceTimers = new Map<ExtensionID, number>();
|
||||
private debounceDelay = 300; // 默认防抖时间为300毫秒
|
||||
|
||||
/**
|
||||
* 注册扩展工厂
|
||||
@@ -75,12 +75,12 @@ export class ExtensionManager {
|
||||
* @param factory 扩展工厂
|
||||
*/
|
||||
registerExtension(id: ExtensionID, factory: ExtensionFactory): void {
|
||||
this.extensionFactories.set(id, factory)
|
||||
this.extensionFactories.set(id, factory);
|
||||
|
||||
// 创建初始状态
|
||||
if (!this.extensionStates.has(id)) {
|
||||
const compartment = new Compartment()
|
||||
const defaultConfig = factory.getDefaultConfig()
|
||||
const compartment = new Compartment();
|
||||
const defaultConfig = factory.getDefaultConfig();
|
||||
|
||||
this.extensionStates.set(id, {
|
||||
id,
|
||||
@@ -89,7 +89,7 @@ export class ExtensionManager {
|
||||
enabled: false,
|
||||
compartment,
|
||||
extension: [] // 默认为空扩展(禁用状态)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export class ExtensionManager {
|
||||
* 获取所有注册的扩展ID列表
|
||||
*/
|
||||
getRegisteredExtensions(): ExtensionID[] {
|
||||
return Array.from(this.extensionFactories.keys())
|
||||
return Array.from(this.extensionFactories.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +105,7 @@ export class ExtensionManager {
|
||||
* @param id 扩展ID
|
||||
*/
|
||||
isExtensionRegistered(id: ExtensionID): boolean {
|
||||
return this.extensionFactories.has(id)
|
||||
return this.extensionFactories.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,26 +114,26 @@ export class ExtensionManager {
|
||||
*/
|
||||
initializeExtensionsFromConfig(extensionConfigs: ExtensionConfig[]): void {
|
||||
for (const config of extensionConfigs) {
|
||||
const factory = this.extensionFactories.get(config.id)
|
||||
if (!factory) continue
|
||||
const factory = this.extensionFactories.get(config.id);
|
||||
if (!factory) continue;
|
||||
|
||||
// 验证配置
|
||||
if (factory.validateConfig && !factory.validateConfig(config.config)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建扩展实例
|
||||
const extension = config.enabled ? factory.create(config.config) : []
|
||||
const extension = config.enabled ? factory.create(config.config) : [];
|
||||
|
||||
// 如果状态已存在则更新,否则创建新状态
|
||||
if (this.extensionStates.has(config.id)) {
|
||||
const state = this.extensionStates.get(config.id)!
|
||||
state.config = config.config
|
||||
state.enabled = config.enabled
|
||||
state.extension = extension
|
||||
const state = this.extensionStates.get(config.id)!;
|
||||
state.config = config.config;
|
||||
state.enabled = config.enabled;
|
||||
state.extension = extension;
|
||||
} else {
|
||||
const compartment = new Compartment()
|
||||
const compartment = new Compartment();
|
||||
this.extensionStates.set(config.id, {
|
||||
id: config.id,
|
||||
factory,
|
||||
@@ -141,10 +141,10 @@ export class ExtensionManager {
|
||||
enabled: config.enabled,
|
||||
compartment,
|
||||
extension
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize extension ${config.id}:`, error)
|
||||
console.error(`Failed to initialize extension ${config.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,14 +154,14 @@ export class ExtensionManager {
|
||||
* @returns CodeMirror扩展数组
|
||||
*/
|
||||
getInitialExtensions(): Extension[] {
|
||||
const extensions: Extension[] = []
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
// 为每个注册的扩展添加compartment
|
||||
for (const state of this.extensionStates.values()) {
|
||||
extensions.push(state.compartment.of(state.extension))
|
||||
extensions.push(state.compartment.of(state.extension));
|
||||
}
|
||||
|
||||
return extensions
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,19 +175,19 @@ export class ExtensionManager {
|
||||
view,
|
||||
documentId,
|
||||
registered: true
|
||||
})
|
||||
});
|
||||
|
||||
// 设置当前活动视图
|
||||
this.activeViewId = documentId
|
||||
this.activeViewId = documentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活动视图
|
||||
*/
|
||||
private getActiveView(): EditorView | null {
|
||||
if (this.activeViewId === null) return null
|
||||
const viewInfo = this.viewsMap.get(this.activeViewId)
|
||||
return viewInfo ? viewInfo.view : null
|
||||
if (this.activeViewId === null) return null;
|
||||
const viewInfo = this.viewsMap.get(this.activeViewId);
|
||||
return viewInfo ? viewInfo.view : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,16 +199,16 @@ export class ExtensionManager {
|
||||
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
||||
// 清除之前的定时器
|
||||
if (this.debounceTimers.has(id)) {
|
||||
window.clearTimeout(this.debounceTimers.get(id))
|
||||
window.clearTimeout(this.debounceTimers.get(id));
|
||||
}
|
||||
|
||||
// 设置新的定时器
|
||||
const timerId = window.setTimeout(() => {
|
||||
this.updateExtensionImmediate(id, enabled, config)
|
||||
this.debounceTimers.delete(id)
|
||||
}, this.debounceDelay)
|
||||
this.updateExtensionImmediate(id, enabled, config);
|
||||
this.debounceTimers.delete(id);
|
||||
}, this.debounceDelay);
|
||||
|
||||
this.debounceTimers.set(id, timerId)
|
||||
this.debounceTimers.set(id, timerId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,30 +219,30 @@ export class ExtensionManager {
|
||||
*/
|
||||
updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
||||
// 获取扩展状态
|
||||
const state = this.extensionStates.get(id)
|
||||
if (!state) return
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return;
|
||||
|
||||
// 获取工厂
|
||||
const factory = state.factory
|
||||
const factory = state.factory;
|
||||
|
||||
// 验证配置
|
||||
if (factory.validateConfig && !factory.validateConfig(config)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建新的扩展实例
|
||||
const extension = enabled ? factory.create(config) : []
|
||||
const extension = enabled ? factory.create(config) : [];
|
||||
|
||||
// 更新内部状态
|
||||
state.config = config
|
||||
state.enabled = enabled
|
||||
state.extension = extension
|
||||
state.config = config;
|
||||
state.enabled = enabled;
|
||||
state.extension = extension;
|
||||
|
||||
// 应用到所有视图
|
||||
this.applyExtensionToAllViews(id)
|
||||
this.applyExtensionToAllViews(id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to update extension ${id}:`, error)
|
||||
console.error(`Failed to update extension ${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,19 +251,19 @@ export class ExtensionManager {
|
||||
* @param id 扩展ID
|
||||
*/
|
||||
private applyExtensionToAllViews(id: ExtensionID): void {
|
||||
const state = this.extensionStates.get(id)
|
||||
if (!state) return
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return;
|
||||
|
||||
// 遍历所有视图并应用更改
|
||||
for (const viewInfo of this.viewsMap.values()) {
|
||||
try {
|
||||
if (!viewInfo.registered) continue
|
||||
if (!viewInfo.registered) continue;
|
||||
|
||||
viewInfo.view.dispatch({
|
||||
effects: state.compartment.reconfigure(state.extension)
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error)
|
||||
console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,56 +280,56 @@ export class ExtensionManager {
|
||||
// 清除所有相关的防抖定时器
|
||||
for (const update of updates) {
|
||||
if (this.debounceTimers.has(update.id)) {
|
||||
window.clearTimeout(this.debounceTimers.get(update.id))
|
||||
this.debounceTimers.delete(update.id)
|
||||
window.clearTimeout(this.debounceTimers.get(update.id));
|
||||
this.debounceTimers.delete(update.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新所有扩展状态
|
||||
for (const update of updates) {
|
||||
// 获取扩展状态
|
||||
const state = this.extensionStates.get(update.id)
|
||||
if (!state) continue
|
||||
const state = this.extensionStates.get(update.id);
|
||||
if (!state) continue;
|
||||
|
||||
// 获取工厂
|
||||
const factory = state.factory
|
||||
const factory = state.factory;
|
||||
|
||||
// 验证配置
|
||||
if (factory.validateConfig && !factory.validateConfig(update.config)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建新的扩展实例
|
||||
const extension = update.enabled ? factory.create(update.config) : []
|
||||
const extension = update.enabled ? factory.create(update.config) : [];
|
||||
|
||||
// 更新内部状态
|
||||
state.config = update.config
|
||||
state.enabled = update.enabled
|
||||
state.extension = extension
|
||||
state.config = update.config;
|
||||
state.enabled = update.enabled;
|
||||
state.extension = extension;
|
||||
} catch (error) {
|
||||
console.error(`Failed to update extension ${update.id}:`, error)
|
||||
console.error(`Failed to update extension ${update.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 将更改应用到所有视图
|
||||
for (const viewInfo of this.viewsMap.values()) {
|
||||
if (!viewInfo.registered) continue
|
||||
if (!viewInfo.registered) continue;
|
||||
|
||||
const effects: StateEffect<any>[] = []
|
||||
const effects: StateEffect<any>[] = [];
|
||||
|
||||
for (const update of updates) {
|
||||
const state = this.extensionStates.get(update.id)
|
||||
if (!state) continue
|
||||
const state = this.extensionStates.get(update.id);
|
||||
if (!state) continue;
|
||||
|
||||
effects.push(state.compartment.reconfigure(state.extension))
|
||||
effects.push(state.compartment.reconfigure(state.extension));
|
||||
}
|
||||
|
||||
if (effects.length > 0) {
|
||||
try {
|
||||
viewInfo.view.dispatch({ effects })
|
||||
viewInfo.view.dispatch({ effects });
|
||||
} catch (error) {
|
||||
console.error(`Failed to apply extensions to document ${viewInfo.documentId}:`, error)
|
||||
console.error(`Failed to apply extensions to document ${viewInfo.documentId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,13 +343,13 @@ export class ExtensionManager {
|
||||
enabled: boolean
|
||||
config: any
|
||||
} | null {
|
||||
const state = this.extensionStates.get(id)
|
||||
if (!state) return null
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return null;
|
||||
|
||||
return {
|
||||
enabled: state.enabled,
|
||||
config: state.config
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,11 +357,11 @@ export class ExtensionManager {
|
||||
* @param id 扩展ID
|
||||
*/
|
||||
resetExtensionToDefault(id: ExtensionID): void {
|
||||
const state = this.extensionStates.get(id)
|
||||
if (!state) return
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return;
|
||||
|
||||
const defaultConfig = state.factory.getDefaultConfig()
|
||||
this.updateExtension(id, true, defaultConfig)
|
||||
const defaultConfig = state.factory.getDefaultConfig();
|
||||
this.updateExtension(id, true, defaultConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,10 +370,10 @@ export class ExtensionManager {
|
||||
*/
|
||||
removeView(documentId: number): void {
|
||||
if (this.activeViewId === documentId) {
|
||||
this.activeViewId = null
|
||||
this.activeViewId = null;
|
||||
}
|
||||
|
||||
this.viewsMap.delete(documentId)
|
||||
this.viewsMap.delete(documentId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,13 +382,13 @@ export class ExtensionManager {
|
||||
destroy(): void {
|
||||
// 清除所有防抖定时器
|
||||
for (const timerId of this.debounceTimers.values()) {
|
||||
window.clearTimeout(timerId)
|
||||
window.clearTimeout(timerId);
|
||||
}
|
||||
this.debounceTimers.clear()
|
||||
this.debounceTimers.clear();
|
||||
|
||||
this.viewsMap.clear()
|
||||
this.activeViewId = null
|
||||
this.extensionFactories.clear()
|
||||
this.extensionStates.clear()
|
||||
this.viewsMap.clear();
|
||||
this.activeViewId = null;
|
||||
this.extensionFactories.clear();
|
||||
this.extensionStates.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
import {ExtensionFactory, ExtensionManager} from './ExtensionManager'
|
||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'
|
||||
import i18n from '@/i18n'
|
||||
import {ExtensionFactory, ExtensionManager} from './ExtensionManager';
|
||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
// 导入现有扩展的创建函数
|
||||
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'
|
||||
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'
|
||||
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
|
||||
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension';
|
||||
|
||||
import {color} from '../extensions/colorSelector'
|
||||
import {hyperLink} from '../extensions/hyperlink'
|
||||
import {minimap} from '../extensions/minimap'
|
||||
import {vscodeSearch} from '../extensions/vscodeSearch'
|
||||
import {createCheckboxExtension} from '../extensions/checkbox'
|
||||
import {createTranslatorExtension} from '../extensions/translator'
|
||||
import {color} from '../extensions/colorSelector';
|
||||
import {hyperLink} from '../extensions/hyperlink';
|
||||
import {minimap} from '../extensions/minimap';
|
||||
import {vscodeSearch} from '../extensions/vscodeSearch';
|
||||
import {createCheckboxExtension} from '../extensions/checkbox';
|
||||
import {createTranslatorExtension} from '../extensions/translator';
|
||||
|
||||
import {foldingOnIndent} from '../extensions/fold/foldExtension'
|
||||
import {foldingOnIndent} from '../extensions/fold/foldExtension';
|
||||
|
||||
/**
|
||||
* 彩虹括号扩展工厂
|
||||
*/
|
||||
export const rainbowBracketsFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return rainbowBracketsExtension()
|
||||
create(_config: any) {
|
||||
return rainbowBracketsExtension();
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object'
|
||||
return typeof config === 'object';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 文本高亮扩展工厂
|
||||
@@ -38,20 +38,20 @@ export const textHighlightFactory: ExtensionFactory = {
|
||||
return createTextHighlighter({
|
||||
backgroundColor: config.backgroundColor || '#FFD700',
|
||||
opacity: config.opacity || 0.3
|
||||
})
|
||||
});
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
backgroundColor: '#FFD700', // 金黄色
|
||||
opacity: 0.3 // 透明度
|
||||
}
|
||||
};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object' &&
|
||||
(!config.backgroundColor || typeof config.backgroundColor === 'string') &&
|
||||
(!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1))
|
||||
(!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 小地图扩展工厂
|
||||
@@ -62,95 +62,95 @@ export const minimapFactory: ExtensionFactory = {
|
||||
displayText: config.displayText || 'characters',
|
||||
showOverlay: config.showOverlay || 'always',
|
||||
autohide: config.autohide || false
|
||||
}
|
||||
return minimap(options)
|
||||
};
|
||||
return minimap(options);
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
displayText: 'characters',
|
||||
showOverlay: 'always',
|
||||
autohide: false
|
||||
}
|
||||
};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object' &&
|
||||
(!config.displayText || typeof config.displayText === 'string') &&
|
||||
(!config.showOverlay || typeof config.showOverlay === 'string') &&
|
||||
(!config.autohide || typeof config.autohide === 'boolean')
|
||||
(!config.autohide || typeof config.autohide === 'boolean');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 超链接扩展工厂
|
||||
*/
|
||||
export const hyperlinkFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return hyperLink
|
||||
create(_config: any) {
|
||||
return hyperLink;
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object'
|
||||
return typeof config === 'object';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 颜色选择器扩展工厂
|
||||
*/
|
||||
export const colorSelectorFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return color
|
||||
create(_config: any) {
|
||||
return color;
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object'
|
||||
return typeof config === 'object';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索扩展工厂
|
||||
*/
|
||||
export const searchFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return vscodeSearch
|
||||
create(_config: any) {
|
||||
return vscodeSearch;
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object'
|
||||
return typeof config === 'object';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const foldFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
create(_config: any) {
|
||||
return foldingOnIndent;
|
||||
},
|
||||
getDefaultConfig(): any {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any): boolean {
|
||||
return typeof config === 'object'
|
||||
return typeof config === 'object';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择框扩展工厂
|
||||
*/
|
||||
export const checkboxFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return createCheckboxExtension()
|
||||
create(_config: any) {
|
||||
return createCheckboxExtension();
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object'
|
||||
return typeof config === 'object';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译扩展工厂
|
||||
@@ -161,19 +161,19 @@ export const translatorFactory: ExtensionFactory = {
|
||||
defaultTranslator: config.defaultTranslator || 'bing',
|
||||
minSelectionLength: config.minSelectionLength || 2,
|
||||
maxTranslationLength: config.maxTranslationLength || 5000,
|
||||
})
|
||||
});
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
defaultTranslator: 'bing',
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
}
|
||||
};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object'
|
||||
return typeof config === 'object';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 所有扩展的统一配置
|
||||
@@ -232,7 +232,7 @@ const EXTENSION_CONFIGS = {
|
||||
displayNameKey: 'extensions.checkbox.name',
|
||||
descriptionKey: 'extensions.checkbox.description'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册所有扩展工厂到管理器
|
||||
@@ -240,8 +240,8 @@ const EXTENSION_CONFIGS = {
|
||||
*/
|
||||
export function registerAllExtensions(manager: ExtensionManager): void {
|
||||
Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => {
|
||||
manager.registerExtension(id as ExtensionID, config.factory)
|
||||
})
|
||||
manager.registerExtension(id as ExtensionID, config.factory);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,8 +250,8 @@ export function registerAllExtensions(manager: ExtensionManager): void {
|
||||
* @returns 显示名称
|
||||
*/
|
||||
export function getExtensionDisplayName(id: ExtensionID): string {
|
||||
const config = EXTENSION_CONFIGS[id as ExtensionID]
|
||||
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id
|
||||
const config = EXTENSION_CONFIGS[id as ExtensionID];
|
||||
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,8 +260,8 @@ export function getExtensionDisplayName(id: ExtensionID): string {
|
||||
* @returns 描述
|
||||
*/
|
||||
export function getExtensionDescription(id: ExtensionID): string {
|
||||
const config = EXTENSION_CONFIGS[id as ExtensionID]
|
||||
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''
|
||||
const config = EXTENSION_CONFIGS[id as ExtensionID];
|
||||
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,7 +270,7 @@ export function getExtensionDescription(id: ExtensionID): string {
|
||||
* @returns 扩展工厂实例
|
||||
*/
|
||||
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
|
||||
return EXTENSION_CONFIGS[id as ExtensionID]?.factory
|
||||
return EXTENSION_CONFIGS[id as ExtensionID]?.factory;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,8 +279,8 @@ export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefin
|
||||
* @returns 默认配置对象
|
||||
*/
|
||||
export function getExtensionDefaultConfig(id: ExtensionID): any {
|
||||
const factory = getExtensionFactory(id)
|
||||
return factory?.getDefaultConfig() || {}
|
||||
const factory = getExtensionFactory(id);
|
||||
return factory?.getDefaultConfig() || {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,8 +289,8 @@ export function getExtensionDefaultConfig(id: ExtensionID): any {
|
||||
* @returns 是否有配置项
|
||||
*/
|
||||
export function hasExtensionConfig(id: ExtensionID): boolean {
|
||||
const defaultConfig = getExtensionDefaultConfig(id)
|
||||
return Object.keys(defaultConfig).length > 0
|
||||
const defaultConfig = getExtensionDefaultConfig(id);
|
||||
return Object.keys(defaultConfig).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,5 +298,5 @@ export function hasExtensionConfig(id: ExtensionID): boolean {
|
||||
* @returns 扩展ID数组
|
||||
*/
|
||||
export function getAllExtensionIds(): ExtensionID[] {
|
||||
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[]
|
||||
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[];
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {useExtensionStore} from '@/stores/extensionStore'
|
||||
import {ExtensionManager} from './ExtensionManager'
|
||||
import {registerAllExtensions} from './factories'
|
||||
import {Extension} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {useExtensionStore} from '@/stores/extensionStore';
|
||||
import {ExtensionManager} from './ExtensionManager';
|
||||
import {registerAllExtensions} from './factories';
|
||||
|
||||
/**
|
||||
* 全局扩展管理器实例
|
||||
*/
|
||||
const extensionManager = new ExtensionManager()
|
||||
const extensionManager = new ExtensionManager();
|
||||
|
||||
/**
|
||||
* 异步创建动态扩展
|
||||
* 确保扩展配置已加载
|
||||
* @param documentId 可选的文档ID,用于提前初始化视图
|
||||
* @param _documentId 可选的文档ID,用于提前初始化视图
|
||||
*/
|
||||
export const createDynamicExtensions = async (documentId?: number): Promise<Extension[]> => {
|
||||
const extensionStore = useExtensionStore()
|
||||
export const createDynamicExtensions = async (_documentId?: number): Promise<Extension[]> => {
|
||||
const extensionStore = useExtensionStore();
|
||||
|
||||
// 注册所有扩展工厂
|
||||
registerAllExtensions(extensionManager)
|
||||
registerAllExtensions(extensionManager);
|
||||
|
||||
// 确保扩展配置已加载
|
||||
if (extensionStore.extensions.length === 0) {
|
||||
await extensionStore.loadExtensions()
|
||||
await extensionStore.loadExtensions();
|
||||
}
|
||||
|
||||
// 初始化扩展管理器配置
|
||||
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions)
|
||||
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions);
|
||||
|
||||
// 获取初始扩展配置
|
||||
return extensionManager.getInitialExtensions()
|
||||
}
|
||||
return extensionManager.getInitialExtensions();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取扩展管理器实例
|
||||
* @returns 扩展管理器
|
||||
*/
|
||||
export const getExtensionManager = (): ExtensionManager => {
|
||||
return extensionManager
|
||||
}
|
||||
return extensionManager;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置编辑器视图到扩展管理器
|
||||
@@ -46,18 +46,18 @@ export const getExtensionManager = (): ExtensionManager => {
|
||||
* @param documentId 文档ID
|
||||
*/
|
||||
export const setExtensionManagerView = (view: EditorView, documentId: number): void => {
|
||||
extensionManager.setView(view, documentId)
|
||||
}
|
||||
extensionManager.setView(view, documentId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 从扩展管理器移除编辑器视图
|
||||
* @param documentId 文档ID
|
||||
*/
|
||||
export const removeExtensionManagerView = (documentId: number): void => {
|
||||
extensionManager.removeView(documentId)
|
||||
}
|
||||
extensionManager.removeView(documentId);
|
||||
};
|
||||
|
||||
// 导出相关模块
|
||||
export {ExtensionManager} from './ExtensionManager'
|
||||
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './factories'
|
||||
export type {ExtensionFactory} from './ExtensionManager'
|
||||
export {ExtensionManager} from './ExtensionManager';
|
||||
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './factories';
|
||||
export type {ExtensionFactory} from './ExtensionManager';
|
||||
@@ -288,11 +288,6 @@ const updateSystemTheme = async (event: Event) => {
|
||||
// 控制颜色选择器显示状态
|
||||
const showPickerMap = ref<Record<string, boolean>>({});
|
||||
|
||||
// 切换颜色选择器显示状态
|
||||
const toggleColorPicker = (colorKey: string) => {
|
||||
showPickerMap.value[colorKey] = !showPickerMap.value[colorKey];
|
||||
};
|
||||
|
||||
// 颜色变更处理
|
||||
const handleColorChange = (colorKey: string, value: string) => {
|
||||
updateColor(colorKey, value);
|
||||
|
||||
@@ -21,7 +21,7 @@ onMounted(async () => {
|
||||
});
|
||||
onUnmounted(() => {
|
||||
backupStore.clearError();
|
||||
})
|
||||
});
|
||||
|
||||
// 认证方式选项
|
||||
const authMethodOptions = computed(() => [
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useEditorStore} from '@/stores/editorStore'
|
||||
import {useExtensionStore} from '@/stores/extensionStore'
|
||||
import {ExtensionService} from '@/../bindings/voidraft/internal/services'
|
||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'
|
||||
import {getExtensionManager} from '@/views/editor/manager'
|
||||
import {computed, ref} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useExtensionStore} from '@/stores/extensionStore';
|
||||
import {ExtensionService} from '@/../bindings/voidraft/internal/services';
|
||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {
|
||||
getAllExtensionIds,
|
||||
getExtensionDefaultConfig,
|
||||
getExtensionDescription,
|
||||
getExtensionDisplayName,
|
||||
hasExtensionConfig
|
||||
} from '@/views/editor/manager/factories'
|
||||
import SettingSection from '../components/SettingSection.vue'
|
||||
import SettingItem from '../components/SettingItem.vue'
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue'
|
||||
} from '@/views/editor/manager/factories';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
|
||||
const {t} = useI18n()
|
||||
const editorStore = useEditorStore()
|
||||
const extensionStore = useExtensionStore()
|
||||
const {t} = useI18n();
|
||||
const editorStore = useEditorStore();
|
||||
const extensionStore = useExtensionStore();
|
||||
|
||||
// 展开状态管理
|
||||
const expandedExtensions = ref<Set<ExtensionID>>(new Set())
|
||||
const expandedExtensions = ref<Set<ExtensionID>>(new Set());
|
||||
|
||||
// 获取所有可用的扩展
|
||||
const availableExtensions = computed(() => {
|
||||
return getAllExtensionIds().map(id => {
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === id)
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === id);
|
||||
return {
|
||||
id,
|
||||
displayName: getExtensionDisplayName(id),
|
||||
@@ -37,68 +36,68 @@ const availableExtensions = computed(() => {
|
||||
hasConfig: hasExtensionConfig(id),
|
||||
config: extension?.config || {},
|
||||
defaultConfig: getExtensionDefaultConfig(id)
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 切换展开状态
|
||||
const toggleExpanded = (extensionId: ExtensionID) => {
|
||||
if (expandedExtensions.value.has(extensionId)) {
|
||||
expandedExtensions.value.delete(extensionId)
|
||||
expandedExtensions.value.delete(extensionId);
|
||||
} else {
|
||||
expandedExtensions.value.add(extensionId)
|
||||
expandedExtensions.value.add(extensionId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新扩展状态
|
||||
const updateExtension = async (extensionId: ExtensionID, enabled: boolean) => {
|
||||
try {
|
||||
await editorStore.updateExtension(extensionId, enabled)
|
||||
await editorStore.updateExtension(extensionId, enabled);
|
||||
} catch (error) {
|
||||
console.error('Failed to update extension:', error)
|
||||
console.error('Failed to update extension:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新扩展配置
|
||||
const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string, value: any) => {
|
||||
try {
|
||||
// 获取当前扩展状态
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId)
|
||||
if (!extension) return
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
|
||||
if (!extension) return;
|
||||
|
||||
// 更新配置
|
||||
const updatedConfig = {...extension.config, [configKey]: value}
|
||||
const updatedConfig = {...extension.config, [configKey]: value};
|
||||
|
||||
console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`)
|
||||
console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`);
|
||||
|
||||
// 使用editorStore的updateExtension方法更新,确保应用到所有编辑器实例
|
||||
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig)
|
||||
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update extension config:', error)
|
||||
console.error('Failed to update extension config:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 重置扩展到默认配置
|
||||
const resetExtension = async (extensionId: ExtensionID) => {
|
||||
try {
|
||||
// 重置到默认配置(后端)
|
||||
await ExtensionService.ResetExtensionToDefault(extensionId)
|
||||
await ExtensionService.ResetExtensionToDefault(extensionId);
|
||||
|
||||
// 重新加载扩展状态以获取最新配置
|
||||
await extensionStore.loadExtensions()
|
||||
await extensionStore.loadExtensions();
|
||||
|
||||
// 获取重置后的状态,立即应用到所有编辑器视图
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId)
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
|
||||
if (extension) {
|
||||
// 通过editorStore更新,确保所有视图都能同步
|
||||
await editorStore.updateExtension(extensionId, extension.enabled, extension.config)
|
||||
console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`)
|
||||
await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
|
||||
console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reset extension:', error)
|
||||
console.error('Failed to reset extension:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 配置项类型定义
|
||||
type ConfigItemType = 'toggle' | 'number' | 'text' | 'select'
|
||||
@@ -131,25 +130,25 @@ const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItem
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取配置项类型
|
||||
const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => {
|
||||
const meta = extensionConfigMeta[extensionId]?.[configKey]
|
||||
const meta = extensionConfigMeta[extensionId]?.[configKey];
|
||||
if (meta?.type) {
|
||||
return meta.type
|
||||
return meta.type;
|
||||
}
|
||||
|
||||
// 根据默认值类型自动推断
|
||||
if (typeof defaultValue === 'boolean') return 'toggle'
|
||||
if (typeof defaultValue === 'number') return 'number'
|
||||
return 'text'
|
||||
}
|
||||
if (typeof defaultValue === 'boolean') return 'toggle';
|
||||
if (typeof defaultValue === 'number') return 'number';
|
||||
return 'text';
|
||||
};
|
||||
|
||||
// 获取选择框的选项列表
|
||||
const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => {
|
||||
return extensionConfigMeta[extensionId]?.[configKey]?.options || []
|
||||
}
|
||||
return extensionConfigMeta[extensionId]?.[configKey]?.options || [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -62,7 +62,7 @@ const startPolling = () => {
|
||||
const delay = status === MigrationStatus.MigrationStatusCompleted ? 3000 : 5000;
|
||||
hideProgressTimer = setTimeout(hideProgress, delay);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
stopPolling();
|
||||
|
||||
// 使用常量简化错误处理
|
||||
|
||||
@@ -87,52 +87,52 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import * as TestService from '@/../bindings/voidraft/internal/services/testservice'
|
||||
import SettingSection from '../components/SettingSection.vue'
|
||||
import SettingItem from '../components/SettingItem.vue'
|
||||
import { ref } from 'vue';
|
||||
import * as TestService from '@/../bindings/voidraft/internal/services/testservice';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
|
||||
// Badge测试状态
|
||||
const badgeText = ref('')
|
||||
const badgeStatus = ref<{ type: string; message: string } | null>(null)
|
||||
const badgeText = ref('');
|
||||
const badgeStatus = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
// 通知测试状态
|
||||
const notificationTitle = ref('')
|
||||
const notificationSubtitle = ref('')
|
||||
const notificationBody = ref('')
|
||||
const notificationStatus = ref<{ type: string; message: string } | null>(null)
|
||||
const notificationTitle = ref('');
|
||||
const notificationSubtitle = ref('');
|
||||
const notificationBody = ref('');
|
||||
const notificationStatus = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
// 清除状态
|
||||
const clearStatus = ref<{ type: string; message: string } | null>(null)
|
||||
const clearStatus = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
// 显示状态消息的辅助函数
|
||||
const showStatus = (statusRef: any, type: 'success' | 'error', message: string) => {
|
||||
statusRef.value = { type, message }
|
||||
statusRef.value = { type, message };
|
||||
setTimeout(() => {
|
||||
statusRef.value = null
|
||||
}, 5000)
|
||||
}
|
||||
statusRef.value = null;
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// 测试Badge功能
|
||||
const testBadge = async () => {
|
||||
try {
|
||||
await TestService.TestBadge(badgeText.value)
|
||||
showStatus(badgeStatus, 'success', `Badge ${badgeText.value ? 'set to: ' + badgeText.value : 'cleared'} successfully`)
|
||||
await TestService.TestBadge(badgeText.value);
|
||||
showStatus(badgeStatus, 'success', `Badge ${badgeText.value ? 'set to: ' + badgeText.value : 'cleared'} successfully`);
|
||||
} catch (error: any) {
|
||||
showStatus(badgeStatus, 'error', `Failed to set badge: ${error.message || error}`)
|
||||
showStatus(badgeStatus, 'error', `Failed to set badge: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 清除Badge
|
||||
const clearBadge = async () => {
|
||||
try {
|
||||
await TestService.TestBadge('')
|
||||
badgeText.value = ''
|
||||
showStatus(badgeStatus, 'success', 'Badge cleared successfully')
|
||||
await TestService.TestBadge('');
|
||||
badgeText.value = '';
|
||||
showStatus(badgeStatus, 'success', 'Badge cleared successfully');
|
||||
} catch (error: any) {
|
||||
showStatus(badgeStatus, 'error', `Failed to clear badge: ${error.message || error}`)
|
||||
showStatus(badgeStatus, 'error', `Failed to clear badge: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 测试通知功能
|
||||
const testNotification = async () => {
|
||||
@@ -141,37 +141,37 @@ const testNotification = async () => {
|
||||
notificationTitle.value,
|
||||
notificationSubtitle.value,
|
||||
notificationBody.value
|
||||
)
|
||||
showStatus(notificationStatus, 'success', 'Notification sent successfully')
|
||||
);
|
||||
showStatus(notificationStatus, 'success', 'Notification sent successfully');
|
||||
} catch (error: any) {
|
||||
showStatus(notificationStatus, 'error', `Failed to send notification: ${error.message || error}`)
|
||||
showStatus(notificationStatus, 'error', `Failed to send notification: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 测试更新通知
|
||||
const testUpdateNotification = async () => {
|
||||
try {
|
||||
await TestService.TestUpdateNotification()
|
||||
showStatus(notificationStatus, 'success', 'Update notification sent successfully (badge + notification)')
|
||||
await TestService.TestUpdateNotification();
|
||||
showStatus(notificationStatus, 'success', 'Update notification sent successfully (badge + notification)');
|
||||
} catch (error: any) {
|
||||
showStatus(notificationStatus, 'error', `Failed to send update notification: ${error.message || error}`)
|
||||
showStatus(notificationStatus, 'error', `Failed to send update notification: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 清除所有测试状态
|
||||
const clearAll = async () => {
|
||||
try {
|
||||
await TestService.ClearAll()
|
||||
await TestService.ClearAll();
|
||||
// 清空表单
|
||||
badgeText.value = ''
|
||||
notificationTitle.value = ''
|
||||
notificationSubtitle.value = ''
|
||||
notificationBody.value = ''
|
||||
showStatus(clearStatus, 'success', 'All test states cleared successfully')
|
||||
badgeText.value = '';
|
||||
notificationTitle.value = '';
|
||||
notificationSubtitle.value = '';
|
||||
notificationBody.value = '';
|
||||
showStatus(clearStatus, 'success', 'All test states cleared successfully');
|
||||
} catch (error: any) {
|
||||
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`)
|
||||
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {computed} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
|
||||
Reference in New Issue
Block a user