🚧 Optimize
This commit is contained in:
13
frontend/src/common/constant/editor.ts
Normal file
13
frontend/src/common/constant/editor.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 编辑器相关常量配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 编辑器实例管理
|
||||||
|
export const EDITOR_CONFIG = {
|
||||||
|
/** 最多缓存的编辑器实例数量 */
|
||||||
|
MAX_INSTANCES: 5,
|
||||||
|
/** 语法树缓存过期时间(毫秒) */
|
||||||
|
SYNTAX_TREE_CACHE_TIMEOUT: 30000,
|
||||||
|
/** 加载状态延迟时间(毫秒) */
|
||||||
|
LOADING_DELAY: 800,
|
||||||
|
} as const;
|
||||||
265
frontend/src/common/utils/asyncManager.ts
Normal file
265
frontend/src/common/utils/asyncManager.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* 操作信息接口
|
||||||
|
*/
|
||||||
|
interface OperationInfo {
|
||||||
|
controller: AbortController;
|
||||||
|
createdAt: number;
|
||||||
|
timeout?: number;
|
||||||
|
timeoutId?: NodeJS.Timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步操作管理器
|
||||||
|
* 用于管理异步操作的竞态条件,确保只有最新的操作有效
|
||||||
|
* 支持操作超时和自动清理机制
|
||||||
|
*
|
||||||
|
* @template T 操作上下文的类型
|
||||||
|
*/
|
||||||
|
export class AsyncManager<T = any> {
|
||||||
|
private operationSequence = 0;
|
||||||
|
private pendingOperations = new Map<number, OperationInfo>();
|
||||||
|
private currentContext: T | null = null;
|
||||||
|
private defaultTimeout: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建异步操作管理器
|
||||||
|
*
|
||||||
|
* @param defaultTimeout 默认超时时间(毫秒),0表示不设置超时
|
||||||
|
*/
|
||||||
|
constructor(defaultTimeout: number = 0) {
|
||||||
|
this.defaultTimeout = defaultTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成新的操作ID
|
||||||
|
*
|
||||||
|
* @returns 新的操作ID
|
||||||
|
*/
|
||||||
|
getNextOperationId(): number {
|
||||||
|
return ++this.operationSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始新的操作
|
||||||
|
*
|
||||||
|
* @param context 操作上下文
|
||||||
|
* @param options 操作选项
|
||||||
|
* @returns 操作ID和AbortController
|
||||||
|
*/
|
||||||
|
startOperation(
|
||||||
|
context: T,
|
||||||
|
options?: {
|
||||||
|
excludeId?: number;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
): { operationId: number; abortController: AbortController } {
|
||||||
|
const operationId = this.getNextOperationId();
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const timeout = options?.timeout ?? this.defaultTimeout;
|
||||||
|
|
||||||
|
// 取消之前的操作
|
||||||
|
this.cancelPreviousOperations(options?.excludeId);
|
||||||
|
|
||||||
|
// 创建操作信息
|
||||||
|
const operationInfo: OperationInfo = {
|
||||||
|
controller: abortController,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
timeout: timeout > 0 ? timeout : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置超时处理
|
||||||
|
if (timeout > 0) {
|
||||||
|
operationInfo.timeoutId = setTimeout(() => {
|
||||||
|
this.cancelOperation(operationId, 'timeout');
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置当前上下文和操作
|
||||||
|
this.currentContext = context;
|
||||||
|
this.pendingOperations.set(operationId, operationInfo);
|
||||||
|
|
||||||
|
return { operationId, abortController };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查操作是否仍然有效
|
||||||
|
*
|
||||||
|
* @param operationId 操作ID
|
||||||
|
* @param context 操作上下文
|
||||||
|
* @returns 操作是否有效
|
||||||
|
*/
|
||||||
|
isOperationValid(operationId: number, context?: T): boolean {
|
||||||
|
const operationInfo = this.pendingOperations.get(operationId);
|
||||||
|
const contextValid = context === undefined || this.currentContext === context;
|
||||||
|
|
||||||
|
return (
|
||||||
|
operationInfo !== undefined &&
|
||||||
|
!operationInfo.controller.signal.aborted &&
|
||||||
|
contextValid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成操作
|
||||||
|
*
|
||||||
|
* @param operationId 操作ID
|
||||||
|
*/
|
||||||
|
completeOperation(operationId: number): void {
|
||||||
|
const operationInfo = this.pendingOperations.get(operationId);
|
||||||
|
if (operationInfo) {
|
||||||
|
// 清理超时定时器
|
||||||
|
if (operationInfo.timeoutId) {
|
||||||
|
clearTimeout(operationInfo.timeoutId);
|
||||||
|
}
|
||||||
|
this.pendingOperations.delete(operationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消指定操作
|
||||||
|
*
|
||||||
|
* @param operationId 操作ID
|
||||||
|
* @param reason 取消原因
|
||||||
|
*/
|
||||||
|
cancelOperation(operationId: number, reason?: string): void {
|
||||||
|
const operationInfo = this.pendingOperations.get(operationId);
|
||||||
|
if (operationInfo) {
|
||||||
|
// 清理超时定时器
|
||||||
|
if (operationInfo.timeoutId) {
|
||||||
|
clearTimeout(operationInfo.timeoutId);
|
||||||
|
}
|
||||||
|
// 取消操作
|
||||||
|
operationInfo.controller.abort(reason);
|
||||||
|
this.pendingOperations.delete(operationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消之前的操作(修复并发bug)
|
||||||
|
*
|
||||||
|
* @param excludeId 要排除的操作ID(不取消该操作)
|
||||||
|
*/
|
||||||
|
cancelPreviousOperations(excludeId?: number): void {
|
||||||
|
// 创建要取消的操作ID数组,避免在遍历时修改Map
|
||||||
|
const operationIdsToCancel: number[] = [];
|
||||||
|
|
||||||
|
for (const [operationId] of this.pendingOperations) {
|
||||||
|
if (excludeId === undefined || operationId !== excludeId) {
|
||||||
|
operationIdsToCancel.push(operationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量取消操作
|
||||||
|
for (const operationId of operationIdsToCancel) {
|
||||||
|
this.cancelOperation(operationId, 'superseded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消所有操作
|
||||||
|
*/
|
||||||
|
cancelAllOperations(): void {
|
||||||
|
// 创建要取消的操作ID数组,避免在遍历时修改Map
|
||||||
|
const operationIdsToCancel = Array.from(this.pendingOperations.keys());
|
||||||
|
|
||||||
|
// 批量取消操作
|
||||||
|
for (const operationId of operationIdsToCancel) {
|
||||||
|
this.cancelOperation(operationId, 'cancelled');
|
||||||
|
}
|
||||||
|
this.currentContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期操作(手动清理超时操作)
|
||||||
|
*
|
||||||
|
* @param maxAge 最大存活时间(毫秒)
|
||||||
|
* @returns 清理的操作数量
|
||||||
|
*/
|
||||||
|
cleanupExpiredOperations(maxAge: number): number {
|
||||||
|
const now = Date.now();
|
||||||
|
const expiredOperationIds: number[] = [];
|
||||||
|
|
||||||
|
for (const [operationId, operationInfo] of this.pendingOperations) {
|
||||||
|
if (now - operationInfo.createdAt > maxAge) {
|
||||||
|
expiredOperationIds.push(operationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量取消过期操作
|
||||||
|
for (const operationId of expiredOperationIds) {
|
||||||
|
this.cancelOperation(operationId, 'expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
return expiredOperationIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取操作统计信息
|
||||||
|
*
|
||||||
|
* @returns 操作统计信息
|
||||||
|
*/
|
||||||
|
getOperationStats(): {
|
||||||
|
total: number;
|
||||||
|
withTimeout: number;
|
||||||
|
averageAge: number;
|
||||||
|
oldestAge: number;
|
||||||
|
} {
|
||||||
|
const now = Date.now();
|
||||||
|
let withTimeout = 0;
|
||||||
|
let totalAge = 0;
|
||||||
|
let oldestAge = 0;
|
||||||
|
|
||||||
|
for (const operationInfo of this.pendingOperations.values()) {
|
||||||
|
const age = now - operationInfo.createdAt;
|
||||||
|
totalAge += age;
|
||||||
|
oldestAge = Math.max(oldestAge, age);
|
||||||
|
|
||||||
|
if (operationInfo.timeout) {
|
||||||
|
withTimeout++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: this.pendingOperations.size,
|
||||||
|
withTimeout,
|
||||||
|
averageAge: this.pendingOperations.size > 0 ? totalAge / this.pendingOperations.size : 0,
|
||||||
|
oldestAge
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前上下文
|
||||||
|
*
|
||||||
|
* @returns 当前上下文
|
||||||
|
*/
|
||||||
|
getCurrentContext(): T | null {
|
||||||
|
return this.currentContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前上下文
|
||||||
|
*
|
||||||
|
* @param context 新的上下文
|
||||||
|
*/
|
||||||
|
setCurrentContext(context: T | null): void {
|
||||||
|
this.currentContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待处理操作数量
|
||||||
|
*
|
||||||
|
* @returns 待处理操作数量
|
||||||
|
*/
|
||||||
|
get pendingCount(): number {
|
||||||
|
return this.pendingOperations.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有待处理的操作
|
||||||
|
*
|
||||||
|
* @returns 是否有待处理的操作
|
||||||
|
*/
|
||||||
|
hasPendingOperations(): boolean {
|
||||||
|
return this.pendingOperations.size > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
280
frontend/src/common/utils/doublyLinkedList.ts
Normal file
280
frontend/src/common/utils/doublyLinkedList.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* 双向链表节点
|
||||||
|
*
|
||||||
|
* @template T 节点数据的类型
|
||||||
|
*/
|
||||||
|
export class DoublyLinkedListNode<T> {
|
||||||
|
public data: T;
|
||||||
|
public prev: DoublyLinkedListNode<T> | null = null;
|
||||||
|
public next: DoublyLinkedListNode<T> | null = null;
|
||||||
|
|
||||||
|
constructor(data: T) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 双向链表实现
|
||||||
|
* 提供 O(1) 时间复杂度的插入、删除和移动操作
|
||||||
|
*
|
||||||
|
* @template T 链表数据的类型
|
||||||
|
*/
|
||||||
|
export class DoublyLinkedList<T> {
|
||||||
|
private head: DoublyLinkedListNode<T> | null = null;
|
||||||
|
private tail: DoublyLinkedListNode<T> | null = null;
|
||||||
|
private _size = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取链表大小
|
||||||
|
*
|
||||||
|
* @returns 链表中节点的数量
|
||||||
|
*/
|
||||||
|
get size(): number {
|
||||||
|
return this._size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查链表是否为空
|
||||||
|
*
|
||||||
|
* @returns 链表是否为空
|
||||||
|
*/
|
||||||
|
get isEmpty(): boolean {
|
||||||
|
return this._size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取头节点
|
||||||
|
*
|
||||||
|
* @returns 头节点,如果链表为空则返回null
|
||||||
|
*/
|
||||||
|
get first(): DoublyLinkedListNode<T> | null {
|
||||||
|
return this.head;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取尾节点
|
||||||
|
*
|
||||||
|
* @returns 尾节点,如果链表为空则返回null
|
||||||
|
*/
|
||||||
|
get last(): DoublyLinkedListNode<T> | null {
|
||||||
|
return this.tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在链表头部添加节点
|
||||||
|
*
|
||||||
|
* @param data 要添加的数据
|
||||||
|
* @returns 新创建的节点
|
||||||
|
*/
|
||||||
|
addFirst(data: T): DoublyLinkedListNode<T> {
|
||||||
|
const newNode = new DoublyLinkedListNode(data);
|
||||||
|
|
||||||
|
if (this.head === null) {
|
||||||
|
this.head = this.tail = newNode;
|
||||||
|
} else {
|
||||||
|
newNode.next = this.head;
|
||||||
|
this.head.prev = newNode;
|
||||||
|
this.head = newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._size++;
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在链表尾部添加节点
|
||||||
|
*
|
||||||
|
* @param data 要添加的数据
|
||||||
|
* @returns 新创建的节点
|
||||||
|
*/
|
||||||
|
addLast(data: T): DoublyLinkedListNode<T> {
|
||||||
|
const newNode = new DoublyLinkedListNode(data);
|
||||||
|
|
||||||
|
if (this.tail === null) {
|
||||||
|
this.head = this.tail = newNode;
|
||||||
|
} else {
|
||||||
|
newNode.prev = this.tail;
|
||||||
|
this.tail.next = newNode;
|
||||||
|
this.tail = newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._size++;
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定节点
|
||||||
|
*
|
||||||
|
* @param node 要删除的节点
|
||||||
|
* @returns 被删除节点的数据
|
||||||
|
*/
|
||||||
|
remove(node: DoublyLinkedListNode<T>): T {
|
||||||
|
if (node.prev) {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
} else {
|
||||||
|
this.head = node.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
} else {
|
||||||
|
this.tail = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理节点引用,防止内存泄漏
|
||||||
|
const data = node.data;
|
||||||
|
node.prev = null;
|
||||||
|
node.next = null;
|
||||||
|
|
||||||
|
this._size--;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除头节点
|
||||||
|
*
|
||||||
|
* @returns 被删除节点的数据,如果链表为空则返回undefined
|
||||||
|
*/
|
||||||
|
removeFirst(): T | undefined {
|
||||||
|
if (this.head === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.remove(this.head);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除尾节点
|
||||||
|
*
|
||||||
|
* @returns 被删除节点的数据,如果链表为空则返回undefined
|
||||||
|
*/
|
||||||
|
removeLast(): T | undefined {
|
||||||
|
if (this.tail === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.remove(this.tail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将节点移动到链表头部
|
||||||
|
*
|
||||||
|
* @param node 要移动的节点
|
||||||
|
*/
|
||||||
|
moveToFirst(node: DoublyLinkedListNode<T>): void {
|
||||||
|
if (node === this.head) {
|
||||||
|
return; // 已经在头部
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从当前位置移除
|
||||||
|
if (node.prev) {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
}
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
} else {
|
||||||
|
this.tail = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动到头部
|
||||||
|
node.prev = null;
|
||||||
|
node.next = this.head;
|
||||||
|
if (this.head) {
|
||||||
|
this.head.prev = node;
|
||||||
|
}
|
||||||
|
this.head = node;
|
||||||
|
|
||||||
|
// 如果链表之前为空,更新尾节点
|
||||||
|
if (this.tail === null) {
|
||||||
|
this.tail = node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将节点移动到链表尾部
|
||||||
|
*
|
||||||
|
* @param node 要移动的节点
|
||||||
|
*/
|
||||||
|
moveToLast(node: DoublyLinkedListNode<T>): void {
|
||||||
|
if (node === this.tail) {
|
||||||
|
return; // 已经在尾部
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从当前位置移除
|
||||||
|
if (node.prev) {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
} else {
|
||||||
|
this.head = node.next;
|
||||||
|
}
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动到尾部
|
||||||
|
node.next = null;
|
||||||
|
node.prev = this.tail;
|
||||||
|
if (this.tail) {
|
||||||
|
this.tail.next = node;
|
||||||
|
}
|
||||||
|
this.tail = node;
|
||||||
|
|
||||||
|
// 如果链表之前为空,更新头节点
|
||||||
|
if (this.head === null) {
|
||||||
|
this.head = node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空链表
|
||||||
|
*
|
||||||
|
* @param onClear 清空时对每个节点数据的回调函数
|
||||||
|
*/
|
||||||
|
clear(onClear?: (data: T) => void): void {
|
||||||
|
let current = this.head;
|
||||||
|
while (current) {
|
||||||
|
const next = current.next;
|
||||||
|
|
||||||
|
if (onClear) {
|
||||||
|
onClear(current.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理节点引用,防止内存泄漏
|
||||||
|
current.prev = null;
|
||||||
|
current.next = null;
|
||||||
|
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.head = null;
|
||||||
|
this.tail = null;
|
||||||
|
this._size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将链表转换为数组
|
||||||
|
*
|
||||||
|
* @returns 包含所有节点数据的数组,按从头到尾的顺序
|
||||||
|
*/
|
||||||
|
toArray(): T[] {
|
||||||
|
const result: T[] = [];
|
||||||
|
let current = this.head;
|
||||||
|
while (current) {
|
||||||
|
result.push(current.data);
|
||||||
|
current = current.next;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历链表
|
||||||
|
*
|
||||||
|
* @param callback 对每个节点数据执行的回调函数
|
||||||
|
*/
|
||||||
|
forEach(callback: (data: T, index: number) => void): void {
|
||||||
|
let current = this.head;
|
||||||
|
let index = 0;
|
||||||
|
while (current) {
|
||||||
|
callback(current.data, index);
|
||||||
|
current = current.next;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
frontend/src/common/utils/hashUtils.ts
Normal file
92
frontend/src/common/utils/hashUtils.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* 高效哈希算法实现
|
||||||
|
* 针对大量文本内容优化的哈希函数集合
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用优化的 xxHash32 算法生成字符串哈希值
|
||||||
|
* 专为大量文本内容设计,性能优异
|
||||||
|
*
|
||||||
|
* xxHash32 特点:
|
||||||
|
* - 极快的处理速度
|
||||||
|
* - 优秀的分布质量,冲突率极低
|
||||||
|
* - 对长文本友好,性能不会随长度线性下降
|
||||||
|
* - 被广泛应用于数据库、压缩工具等
|
||||||
|
*
|
||||||
|
* @param content 要哈希的字符串内容
|
||||||
|
* @returns 32位哈希值的字符串表示
|
||||||
|
*/
|
||||||
|
export const generateContentHash = (content: string): string => {
|
||||||
|
return (generateContentHashInternal(content) >>> 0).toString(36);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从字符串中提取 32 位整数(模拟小端序)
|
||||||
|
*/
|
||||||
|
function getUint32(str: string, index: number): number {
|
||||||
|
return (
|
||||||
|
(str.charCodeAt(index) & 0xff) |
|
||||||
|
((str.charCodeAt(index + 1) & 0xff) << 8) |
|
||||||
|
((str.charCodeAt(index + 2) & 0xff) << 16) |
|
||||||
|
((str.charCodeAt(index + 3) & 0xff) << 24)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 32 位左旋转
|
||||||
|
*/
|
||||||
|
function rotateLeft(value: number, shift: number): number {
|
||||||
|
return (value << shift) | (value >>> (32 - shift));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部哈希计算函数,返回数值
|
||||||
|
*/
|
||||||
|
function generateContentHashInternal(content: string): number {
|
||||||
|
const PRIME1 = 0x9e3779b1;
|
||||||
|
const PRIME2 = 0x85ebca77;
|
||||||
|
const PRIME3 = 0xc2b2ae3d;
|
||||||
|
const PRIME4 = 0x27d4eb2f;
|
||||||
|
const PRIME5 = 0x165667b1;
|
||||||
|
|
||||||
|
const len = content.length;
|
||||||
|
let hash: number;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
if (len >= 16) {
|
||||||
|
let acc1 = PRIME1 + PRIME2;
|
||||||
|
let acc2 = PRIME2;
|
||||||
|
let acc3 = 0;
|
||||||
|
let acc4 = -PRIME1;
|
||||||
|
|
||||||
|
for (; i <= len - 16; i += 16) {
|
||||||
|
acc1 = Math.imul(rotateLeft(acc1 + Math.imul(getUint32(content, i), PRIME2), 13), PRIME1);
|
||||||
|
acc2 = Math.imul(rotateLeft(acc2 + Math.imul(getUint32(content, i + 4), PRIME2), 13), PRIME1);
|
||||||
|
acc3 = Math.imul(rotateLeft(acc3 + Math.imul(getUint32(content, i + 8), PRIME2), 13), PRIME1);
|
||||||
|
acc4 = Math.imul(rotateLeft(acc4 + Math.imul(getUint32(content, i + 12), PRIME2), 13), PRIME1);
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = rotateLeft(acc1, 1) + rotateLeft(acc2, 7) + rotateLeft(acc3, 12) + rotateLeft(acc4, 18);
|
||||||
|
} else {
|
||||||
|
hash = PRIME5;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash += len;
|
||||||
|
|
||||||
|
for (; i <= len - 4; i += 4) {
|
||||||
|
hash = Math.imul(rotateLeft(hash + Math.imul(getUint32(content, i), PRIME3), 17), PRIME4);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < len; i++) {
|
||||||
|
hash = Math.imul(rotateLeft(hash + Math.imul(content.charCodeAt(i), PRIME5), 11), PRIME1);
|
||||||
|
}
|
||||||
|
|
||||||
|
hash ^= hash >>> 15;
|
||||||
|
hash = Math.imul(hash, PRIME2);
|
||||||
|
hash ^= hash >>> 13;
|
||||||
|
hash = Math.imul(hash, PRIME3);
|
||||||
|
hash ^= hash >>> 16;
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
157
frontend/src/common/utils/lruCache.ts
Normal file
157
frontend/src/common/utils/lruCache.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { DoublyLinkedList, DoublyLinkedListNode } from './doublyLinkedList';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LRU缓存项
|
||||||
|
*
|
||||||
|
* @template K 键的类型
|
||||||
|
* @template V 值的类型
|
||||||
|
*/
|
||||||
|
interface LruCacheItem<K, V> {
|
||||||
|
key: K;
|
||||||
|
value: V;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LRU (Least Recently Used) 缓存实现
|
||||||
|
* 使用双向链表 + Map 实现 O(1) 时间复杂度的所有操作
|
||||||
|
*
|
||||||
|
* @template K 键的类型
|
||||||
|
* @template V 值的类型
|
||||||
|
*/
|
||||||
|
export class LruCache<K, V> {
|
||||||
|
private readonly maxSize: number;
|
||||||
|
private readonly cache = new Map<K, DoublyLinkedListNode<LruCacheItem<K, V>>>();
|
||||||
|
private readonly lru = new DoublyLinkedList<LruCacheItem<K, V>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建LRU缓存实例
|
||||||
|
*
|
||||||
|
* @param maxSize 最大缓存大小
|
||||||
|
*/
|
||||||
|
constructor(maxSize: number) {
|
||||||
|
if (maxSize <= 0) {
|
||||||
|
throw new Error('Max size must be greater than 0');
|
||||||
|
}
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存值
|
||||||
|
*
|
||||||
|
* @param key 键
|
||||||
|
* @returns 缓存的值,如果不存在则返回undefined
|
||||||
|
*/
|
||||||
|
get(key: K): V | undefined {
|
||||||
|
const node = this.cache.get(key);
|
||||||
|
if (node) {
|
||||||
|
// 将访问的节点移动到链表尾部(最近使用)
|
||||||
|
this.lru.moveToLast(node);
|
||||||
|
return node.data.value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存值
|
||||||
|
*
|
||||||
|
* @param key 键
|
||||||
|
* @param value 值
|
||||||
|
* @param onEvict 当有项目被驱逐时的回调函数
|
||||||
|
*/
|
||||||
|
set(key: K, value: V, onEvict?: (evictedKey: K, evictedValue: V) => void): void {
|
||||||
|
const existingNode = this.cache.get(key);
|
||||||
|
|
||||||
|
// 如果键已存在,更新值并移动到最近使用
|
||||||
|
if (existingNode) {
|
||||||
|
existingNode.data.value = value;
|
||||||
|
this.lru.moveToLast(existingNode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果缓存已满,移除最少使用的项
|
||||||
|
if (this.cache.size >= this.maxSize) {
|
||||||
|
const oldestNode = this.lru.first;
|
||||||
|
if (oldestNode) {
|
||||||
|
const { key: evictedKey, value: evictedValue } = oldestNode.data;
|
||||||
|
this.cache.delete(evictedKey);
|
||||||
|
this.lru.removeFirst();
|
||||||
|
|
||||||
|
if (onEvict) {
|
||||||
|
onEvict(evictedKey, evictedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新项到链表尾部(最近使用)
|
||||||
|
const newNode = this.lru.addLast({ key, value });
|
||||||
|
this.cache.set(key, newNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查键是否存在
|
||||||
|
*
|
||||||
|
* @param key 键
|
||||||
|
* @returns 是否存在
|
||||||
|
*/
|
||||||
|
has(key: K): boolean {
|
||||||
|
return this.cache.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定键的缓存
|
||||||
|
*
|
||||||
|
* @param key 键
|
||||||
|
* @returns 是否成功删除
|
||||||
|
*/
|
||||||
|
delete(key: K): boolean {
|
||||||
|
const node = this.cache.get(key);
|
||||||
|
if (node) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
this.lru.remove(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空缓存
|
||||||
|
*
|
||||||
|
* @param onEvict 清空时对每个项目的回调函数
|
||||||
|
*/
|
||||||
|
clear(onEvict?: (key: K, value: V) => void): void {
|
||||||
|
if (onEvict) {
|
||||||
|
this.lru.forEach(item => {
|
||||||
|
onEvict(item.key, item.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.cache.clear();
|
||||||
|
this.lru.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存大小
|
||||||
|
*
|
||||||
|
* @returns 当前缓存项数量
|
||||||
|
*/
|
||||||
|
get size(): number {
|
||||||
|
return this.cache.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有键
|
||||||
|
*
|
||||||
|
* @returns 所有键的数组,按最近使用顺序排列(从最少使用到最近使用)
|
||||||
|
*/
|
||||||
|
keys(): K[] {
|
||||||
|
return this.lru.toArray().map(item => item.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有值
|
||||||
|
*
|
||||||
|
* @returns 所有值的数组,按最近使用顺序排列(从最少使用到最近使用)
|
||||||
|
*/
|
||||||
|
values(): V[] {
|
||||||
|
return this.lru.toArray().map(item => item.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {nextTick, ref, watch} from 'vue';
|
import {computed, nextTick, ref, watch} from 'vue';
|
||||||
import {EditorView} from '@codemirror/view';
|
import {EditorView} from '@codemirror/view';
|
||||||
import {EditorState, Extension} from '@codemirror/state';
|
import {EditorState, Extension} from '@codemirror/state';
|
||||||
import {useConfigStore} from './configStore';
|
import {useConfigStore} from './configStore';
|
||||||
@@ -18,8 +18,11 @@ import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/edito
|
|||||||
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
|
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
|
||||||
import {useExtensionStore} from './extensionStore';
|
import {useExtensionStore} from './extensionStore';
|
||||||
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
||||||
|
import {LruCache} from '@/common/utils/lruCache';
|
||||||
const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例
|
import {AsyncManager} from '@/common/utils/asyncManager';
|
||||||
|
import {generateContentHash} from "@/common/utils/hashUtils";
|
||||||
|
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
|
||||||
|
import {EDITOR_CONFIG} from '@/common/constant/editor';
|
||||||
|
|
||||||
export interface DocumentStats {
|
export interface DocumentStats {
|
||||||
lines: number;
|
lines: number;
|
||||||
@@ -33,7 +36,7 @@ interface EditorInstance {
|
|||||||
content: string;
|
content: string;
|
||||||
isDirty: boolean;
|
isDirty: boolean;
|
||||||
lastModified: Date;
|
lastModified: Date;
|
||||||
autoSaveTimer: number | null;
|
autoSaveTimer: TimerManager;
|
||||||
syntaxTreeCache: {
|
syntaxTreeCache: {
|
||||||
lastDocLength: number;
|
lastDocLength: number;
|
||||||
lastContentHash: string;
|
lastContentHash: string;
|
||||||
@@ -49,15 +52,8 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const extensionStore = useExtensionStore();
|
const extensionStore = useExtensionStore();
|
||||||
|
|
||||||
// === 核心状态 ===
|
// === 核心状态 ===
|
||||||
const editorCache = ref<{
|
const editorCache = new LruCache<number, EditorInstance>(EDITOR_CONFIG.MAX_INSTANCES);
|
||||||
lru: number[];
|
const containerElement = ref<HTMLElement | null>(null);
|
||||||
instances: Record<number, EditorInstance>;
|
|
||||||
containerElement: HTMLElement | null;
|
|
||||||
}>({
|
|
||||||
lru: [],
|
|
||||||
instances: {},
|
|
||||||
containerElement: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentEditor = ref<EditorView | null>(null);
|
const currentEditor = ref<EditorView | null>(null);
|
||||||
const documentStats = ref<DocumentStats>({
|
const documentStats = ref<DocumentStats>({
|
||||||
@@ -69,52 +65,17 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 编辑器加载状态
|
// 编辑器加载状态
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// 异步操作竞态条件控制
|
// 异步操作管理器
|
||||||
const operationSequence = ref(0);
|
const operationManager = new AsyncManager<number>();
|
||||||
const pendingOperations = ref(new Map<number, AbortController>());
|
|
||||||
const currentLoadingDocumentId = ref<number | null>(null);
|
|
||||||
|
|
||||||
// 自动保存设置 - 从配置动态获取
|
// 自动保存设置 - 从配置动态获取
|
||||||
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
|
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
|
||||||
|
|
||||||
// 生成新的操作序列号
|
|
||||||
const getNextOperationId = () => ++operationSequence.value;
|
|
||||||
|
|
||||||
// 取消之前的操作
|
|
||||||
const cancelPreviousOperations = (excludeId?: number) => {
|
|
||||||
pendingOperations.value.forEach((controller, id) => {
|
|
||||||
if (id !== excludeId) {
|
|
||||||
controller.abort();
|
|
||||||
pendingOperations.value.delete(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
|
||||||
const isOperationValid = (operationId: number, documentId: number) => {
|
|
||||||
return (
|
|
||||||
pendingOperations.value.has(operationId) &&
|
|
||||||
!pendingOperations.value.get(operationId)?.signal.aborted &&
|
|
||||||
currentLoadingDocumentId.value === documentId
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// === 私有方法 ===
|
// === 私有方法 ===
|
||||||
|
|
||||||
// 生成内容哈希
|
|
||||||
const generateContentHash = (content: string): string => {
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < content.length; i++) {
|
|
||||||
const char = content.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + char;
|
|
||||||
hash = hash & hash; // Convert to 32bit integer
|
|
||||||
}
|
|
||||||
return hash.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存化的语法树确保方法
|
// 缓存化的语法树确保方法
|
||||||
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCache.get(documentId);
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
|
|
||||||
const docLength = view.state.doc.length;
|
const docLength = view.state.doc.length;
|
||||||
@@ -127,7 +88,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const shouldRebuild = !cache ||
|
const shouldRebuild = !cache ||
|
||||||
cache.lastDocLength !== docLength ||
|
cache.lastDocLength !== docLength ||
|
||||||
cache.lastContentHash !== contentHash ||
|
cache.lastContentHash !== contentHash ||
|
||||||
(now.getTime() - cache.lastParsed.getTime()) > 30000; // 30秒过期
|
(now.getTime() - cache.lastParsed.getTime()) > EDITOR_CONFIG.SYNTAX_TREE_CACHE_TIMEOUT;
|
||||||
|
|
||||||
if (shouldRebuild) {
|
if (shouldRebuild) {
|
||||||
try {
|
try {
|
||||||
@@ -151,12 +112,12 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
operationId: number,
|
operationId: number,
|
||||||
documentId: number
|
documentId: number
|
||||||
): Promise<EditorView> => {
|
): Promise<EditorView> => {
|
||||||
if (!editorCache.value.containerElement) {
|
if (!containerElement.value) {
|
||||||
throw new Error('Editor container not set');
|
throw new Error('Editor container not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
// 检查操作是否仍然有效
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +157,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 再次检查操作有效性
|
// 再次检查操作有效性
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +165,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const keymapExtension = await createDynamicKeymapExtension();
|
const keymapExtension = await createDynamicKeymapExtension();
|
||||||
|
|
||||||
// 检查操作有效性
|
// 检查操作有效性
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +173,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const dynamicExtensions = await createDynamicExtensions(documentId);
|
const dynamicExtensions = await createDynamicExtensions(documentId);
|
||||||
|
|
||||||
// 最终检查操作有效性
|
// 最终检查操作有效性
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,52 +213,31 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 添加编辑器到缓存
|
// 添加编辑器到缓存
|
||||||
const addEditorToCache = (documentId: number, view: EditorView, content: string) => {
|
const addEditorToCache = (documentId: number, view: EditorView, content: string) => {
|
||||||
// 如果缓存已满,移除最少使用的编辑器
|
const instance: EditorInstance = {
|
||||||
if (editorCache.value.lru.length >= NUM_EDITOR_INSTANCES) {
|
|
||||||
const oldestId = editorCache.value.lru.shift();
|
|
||||||
if (oldestId && editorCache.value.instances[oldestId]) {
|
|
||||||
const oldInstance = editorCache.value.instances[oldestId];
|
|
||||||
// 清除自动保存定时器
|
|
||||||
if (oldInstance.autoSaveTimer) {
|
|
||||||
clearTimeout(oldInstance.autoSaveTimer);
|
|
||||||
}
|
|
||||||
// 移除DOM元素
|
|
||||||
if (oldInstance.view.dom.parentElement) {
|
|
||||||
oldInstance.view.dom.remove();
|
|
||||||
}
|
|
||||||
oldInstance.view.destroy();
|
|
||||||
delete editorCache.value.instances[oldestId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新的编辑器实例
|
|
||||||
editorCache.value.instances[documentId] = {
|
|
||||||
view,
|
view,
|
||||||
documentId,
|
documentId,
|
||||||
content,
|
content,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
autoSaveTimer: null,
|
autoSaveTimer: createTimerManager(),
|
||||||
syntaxTreeCache: null
|
syntaxTreeCache: null
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加到LRU列表
|
// 使用LRU缓存的onEvict回调处理被驱逐的实例
|
||||||
editorCache.value.lru.push(documentId);
|
editorCache.set(documentId, instance, (_evictedKey, evictedInstance) => {
|
||||||
|
// 清除自动保存定时器
|
||||||
|
evictedInstance.autoSaveTimer.clear();
|
||||||
|
// 移除DOM元素
|
||||||
|
if (evictedInstance.view.dom.parentElement) {
|
||||||
|
evictedInstance.view.dom.remove();
|
||||||
|
}
|
||||||
|
evictedInstance.view.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化语法树缓存
|
// 初始化语法树缓存
|
||||||
ensureSyntaxTreeCached(view, documentId);
|
ensureSyntaxTreeCached(view, documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新LRU
|
|
||||||
const updateLRU = (documentId: number) => {
|
|
||||||
const lru = editorCache.value.lru;
|
|
||||||
const index = lru.indexOf(documentId);
|
|
||||||
if (index > -1) {
|
|
||||||
lru.splice(index, 1);
|
|
||||||
}
|
|
||||||
lru.push(documentId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取或创建编辑器
|
// 获取或创建编辑器
|
||||||
const getOrCreateEditor = async (
|
const getOrCreateEditor = async (
|
||||||
documentId: number,
|
documentId: number,
|
||||||
@@ -305,14 +245,13 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
operationId: number
|
operationId: number
|
||||||
): Promise<EditorView> => {
|
): Promise<EditorView> => {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
const cached = editorCache.value.instances[documentId];
|
const cached = editorCache.get(documentId);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
updateLRU(documentId);
|
|
||||||
return cached.view;
|
return cached.view;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
// 检查操作是否仍然有效
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +259,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const view = await createEditorInstance(content, operationId, documentId);
|
const view = await createEditorInstance(content, operationId, documentId);
|
||||||
|
|
||||||
// 最终检查操作有效性
|
// 最终检查操作有效性
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
// 如果操作已取消,清理创建的实例
|
// 如果操作已取消,清理创建的实例
|
||||||
view.destroy();
|
view.destroy();
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
@@ -333,8 +272,8 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 显示编辑器
|
// 显示编辑器
|
||||||
const showEditor = (documentId: number) => {
|
const showEditor = (documentId: number) => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCache.get(documentId);
|
||||||
if (!instance || !editorCache.value.containerElement) return;
|
if (!instance || !containerElement.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 移除当前编辑器DOM
|
// 移除当前编辑器DOM
|
||||||
@@ -343,18 +282,15 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保容器为空
|
// 确保容器为空
|
||||||
editorCache.value.containerElement.innerHTML = '';
|
containerElement.value.innerHTML = '';
|
||||||
|
|
||||||
// 将目标编辑器DOM添加到容器
|
// 将目标编辑器DOM添加到容器
|
||||||
editorCache.value.containerElement.appendChild(instance.view.dom);
|
containerElement.value.appendChild(instance.view.dom);
|
||||||
currentEditor.value = instance.view;
|
currentEditor.value = instance.view;
|
||||||
|
|
||||||
// 设置扩展管理器视图
|
// 设置扩展管理器视图
|
||||||
setExtensionManagerView(instance.view, documentId);
|
setExtensionManagerView(instance.view, documentId);
|
||||||
|
|
||||||
// 更新LRU
|
|
||||||
updateLRU(documentId);
|
|
||||||
|
|
||||||
// 重新测量和聚焦编辑器
|
// 重新测量和聚焦编辑器
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 将光标定位到文档末尾并滚动到该位置
|
// 将光标定位到文档末尾并滚动到该位置
|
||||||
@@ -377,7 +313,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 保存编辑器内容
|
// 保存编辑器内容
|
||||||
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCache.get(documentId);
|
||||||
if (!instance || !instance.isDirty) return true;
|
if (!instance || !instance.isDirty) return true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -403,7 +339,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 内容变化处理
|
// 内容变化处理
|
||||||
const onContentChange = (documentId: number) => {
|
const onContentChange = (documentId: number) => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCache.get(documentId);
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
|
|
||||||
instance.isDirty = true;
|
instance.isDirty = true;
|
||||||
@@ -412,13 +348,8 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 清理语法树缓存,下次访问时重新构建
|
// 清理语法树缓存,下次访问时重新构建
|
||||||
instance.syntaxTreeCache = null;
|
instance.syntaxTreeCache = null;
|
||||||
|
|
||||||
// 清除之前的定时器
|
// 设置自动保存定时器
|
||||||
if (instance.autoSaveTimer) {
|
instance.autoSaveTimer.set(() => {
|
||||||
clearTimeout(instance.autoSaveTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的自动保存定时器
|
|
||||||
instance.autoSaveTimer = window.setTimeout(() => {
|
|
||||||
saveEditorContent(documentId);
|
saveEditorContent(documentId);
|
||||||
}, getAutoSaveDelay());
|
}, getAutoSaveDelay());
|
||||||
};
|
};
|
||||||
@@ -427,7 +358,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 设置编辑器容器
|
// 设置编辑器容器
|
||||||
const setEditorContainer = (container: HTMLElement | null) => {
|
const setEditorContainer = (container: HTMLElement | null) => {
|
||||||
editorCache.value.containerElement = container;
|
containerElement.value = container;
|
||||||
|
|
||||||
// 如果设置容器时已有当前文档,立即加载编辑器
|
// 如果设置容器时已有当前文档,立即加载编辑器
|
||||||
if (container && documentStore.currentDocument) {
|
if (container && documentStore.currentDocument) {
|
||||||
@@ -439,9 +370,9 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const loadEditor = async (documentId: number, content: string) => {
|
const loadEditor = async (documentId: number, content: string) => {
|
||||||
// 设置加载状态
|
// 设置加载状态
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
// 生成新的操作ID
|
|
||||||
const operationId = getNextOperationId();
|
// 开始新的操作
|
||||||
const abortController = new AbortController();
|
const { operationId } = operationManager.startOperation(documentId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 验证参数
|
// 验证参数
|
||||||
@@ -449,11 +380,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
throw new Error('Invalid parameters for loadEditor');
|
throw new Error('Invalid parameters for loadEditor');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消之前的操作并设置当前操作
|
|
||||||
cancelPreviousOperations();
|
|
||||||
currentLoadingDocumentId.value = documentId;
|
|
||||||
pendingOperations.value.set(operationId, abortController);
|
|
||||||
|
|
||||||
// 保存当前编辑器内容
|
// 保存当前编辑器内容
|
||||||
if (currentEditor.value) {
|
if (currentEditor.value) {
|
||||||
const currentDocId = documentStore.currentDocumentId;
|
const currentDocId = documentStore.currentDocumentId;
|
||||||
@@ -461,7 +387,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
await saveEditorContent(currentDocId);
|
await saveEditorContent(currentDocId);
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
// 检查操作是否仍然有效
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,12 +397,12 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const view = await getOrCreateEditor(documentId, content, operationId);
|
const view = await getOrCreateEditor(documentId, content, operationId);
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
// 检查操作是否仍然有效
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新内容(如果需要)
|
// 更新内容(如果需要)
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCache.get(documentId);
|
||||||
if (instance && instance.content !== content) {
|
if (instance && instance.content !== content) {
|
||||||
// 确保编辑器视图有效
|
// 确保编辑器视图有效
|
||||||
if (view && view.state && view.dispatch) {
|
if (view && view.state && view.dispatch) {
|
||||||
@@ -495,7 +421,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 最终检查操作有效性
|
// 最终检查操作有效性
|
||||||
if (!isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,35 +435,28 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
console.error('Failed to load editor:', error);
|
console.error('Failed to load editor:', error);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// 清理操作记录
|
// 完成操作
|
||||||
pendingOperations.value.delete(operationId);
|
operationManager.completeOperation(operationId);
|
||||||
if (currentLoadingDocumentId.value === documentId) {
|
|
||||||
currentLoadingDocumentId.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟一段时间后再取消加载状态
|
// 延迟一段时间后再取消加载状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}, 800);
|
}, EDITOR_CONFIG.LOADING_DELAY);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 移除编辑器
|
// 移除编辑器
|
||||||
const removeEditor = (documentId: number) => {
|
const removeEditor = (documentId: number) => {
|
||||||
const instance = editorCache.value.instances[documentId];
|
const instance = editorCache.get(documentId);
|
||||||
if (instance) {
|
if (instance) {
|
||||||
try {
|
try {
|
||||||
// 如果正在加载这个文档,取消操作
|
// 如果正在加载这个文档,取消操作
|
||||||
if (currentLoadingDocumentId.value === documentId) {
|
if (operationManager.getCurrentContext() === documentId) {
|
||||||
cancelPreviousOperations();
|
operationManager.cancelAllOperations();
|
||||||
currentLoadingDocumentId.value = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除自动保存定时器
|
// 清除自动保存定时器
|
||||||
if (instance.autoSaveTimer) {
|
instance.autoSaveTimer.clear();
|
||||||
clearTimeout(instance.autoSaveTimer);
|
|
||||||
instance.autoSaveTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从扩展管理器中移除视图
|
// 从扩展管理器中移除视图
|
||||||
removeExtensionManagerView(documentId);
|
removeExtensionManagerView(documentId);
|
||||||
@@ -557,12 +476,8 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
currentEditor.value = null;
|
currentEditor.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete editorCache.value.instances[documentId];
|
// 从缓存中删除
|
||||||
|
editorCache.delete(documentId);
|
||||||
const lruIndex = editorCache.value.lru.indexOf(documentId);
|
|
||||||
if (lruIndex > -1) {
|
|
||||||
editorCache.value.lru.splice(lruIndex, 1);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error removing editor:', error);
|
console.error('Error removing editor:', error);
|
||||||
}
|
}
|
||||||
@@ -576,7 +491,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用字体设置
|
// 应用字体设置
|
||||||
const applyFontSettings = () => {
|
const applyFontSettings = () => {
|
||||||
Object.values(editorCache.value.instances).forEach(instance => {
|
editorCache.values().forEach(instance => {
|
||||||
updateFontConfig(instance.view, {
|
updateFontConfig(instance.view, {
|
||||||
fontFamily: configStore.config.editing.fontFamily,
|
fontFamily: configStore.config.editing.fontFamily,
|
||||||
fontSize: configStore.config.editing.fontSize,
|
fontSize: configStore.config.editing.fontSize,
|
||||||
@@ -588,7 +503,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用主题设置
|
// 应用主题设置
|
||||||
const applyThemeSettings = () => {
|
const applyThemeSettings = () => {
|
||||||
Object.values(editorCache.value.instances).forEach(instance => {
|
editorCache.values().forEach(instance => {
|
||||||
updateEditorTheme(instance.view,
|
updateEditorTheme(instance.view,
|
||||||
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
||||||
);
|
);
|
||||||
@@ -597,7 +512,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用Tab设置
|
// 应用Tab设置
|
||||||
const applyTabSettings = () => {
|
const applyTabSettings = () => {
|
||||||
Object.values(editorCache.value.instances).forEach(instance => {
|
editorCache.values().forEach(instance => {
|
||||||
updateTabConfig(
|
updateTabConfig(
|
||||||
instance.view,
|
instance.view,
|
||||||
configStore.config.editing.tabSize,
|
configStore.config.editing.tabSize,
|
||||||
@@ -611,7 +526,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const applyKeymapSettings = async () => {
|
const applyKeymapSettings = async () => {
|
||||||
// 确保所有编辑器实例的快捷键都更新
|
// 确保所有编辑器实例的快捷键都更新
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.values(editorCache.value.instances).map(instance =>
|
editorCache.values().map(instance =>
|
||||||
updateKeymapExtension(instance.view)
|
updateKeymapExtension(instance.view)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -620,14 +535,11 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 清空所有编辑器
|
// 清空所有编辑器
|
||||||
const clearAllEditors = () => {
|
const clearAllEditors = () => {
|
||||||
// 取消所有挂起的操作
|
// 取消所有挂起的操作
|
||||||
cancelPreviousOperations();
|
operationManager.cancelAllOperations();
|
||||||
currentLoadingDocumentId.value = null;
|
|
||||||
|
|
||||||
Object.values(editorCache.value.instances).forEach(instance => {
|
editorCache.clear((_documentId, instance) => {
|
||||||
// 清除自动保存定时器
|
// 清除自动保存定时器
|
||||||
if (instance.autoSaveTimer) {
|
instance.autoSaveTimer.clear();
|
||||||
clearTimeout(instance.autoSaveTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从扩展管理器移除
|
// 从扩展管理器移除
|
||||||
removeExtensionManagerView(instance.documentId);
|
removeExtensionManagerView(instance.documentId);
|
||||||
@@ -640,8 +552,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
instance.view.destroy();
|
instance.view.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
editorCache.value.instances = {};
|
|
||||||
editorCache.value.lru = [];
|
|
||||||
currentEditor.value = null;
|
currentEditor.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -665,29 +575,36 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 重新加载扩展配置
|
// 重新加载扩展配置
|
||||||
await extensionStore.loadExtensions();
|
await extensionStore.loadExtensions();
|
||||||
|
|
||||||
// 不再需要单独更新当前编辑器的快捷键映射,因为扩展管理器会更新所有实例
|
|
||||||
// 但我们仍需要确保快捷键配置在所有编辑器上更新
|
|
||||||
await applyKeymapSettings();
|
await applyKeymapSettings();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听文档切换
|
// 监听文档切换
|
||||||
watch(() => documentStore.currentDocument, (newDoc) => {
|
watch(() => documentStore.currentDocument, async (newDoc) => {
|
||||||
if (newDoc && editorCache.value.containerElement) {
|
if (newDoc && containerElement.value) {
|
||||||
// 使用 nextTick 确保DOM更新完成后再加载编辑器
|
// 使用 nextTick 确保DOM更新完成后再加载编辑器
|
||||||
nextTick(() => {
|
await nextTick(() => {
|
||||||
loadEditor(newDoc.id, newDoc.content);
|
loadEditor(newDoc.id, newDoc.content);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听配置变化
|
// 创建字体配置的计算属性
|
||||||
watch(() => configStore.config.editing.fontSize, applyFontSettings);
|
const fontConfig = computed(() => ({
|
||||||
watch(() => configStore.config.editing.fontFamily, applyFontSettings);
|
fontSize: configStore.config.editing.fontSize,
|
||||||
watch(() => configStore.config.editing.lineHeight, applyFontSettings);
|
fontFamily: configStore.config.editing.fontFamily,
|
||||||
watch(() => configStore.config.editing.fontWeight, applyFontSettings);
|
lineHeight: configStore.config.editing.lineHeight,
|
||||||
watch(() => configStore.config.editing.tabSize, applyTabSettings);
|
fontWeight: configStore.config.editing.fontWeight
|
||||||
watch(() => configStore.config.editing.enableTabIndent, applyTabSettings);
|
}));
|
||||||
watch(() => configStore.config.editing.tabType, applyTabSettings);
|
// 创建Tab配置的计算属性
|
||||||
|
const tabConfig = computed(() => ({
|
||||||
|
tabSize: configStore.config.editing.tabSize,
|
||||||
|
enableTabIndent: configStore.config.editing.enableTabIndent,
|
||||||
|
tabType: configStore.config.editing.tabType
|
||||||
|
}));
|
||||||
|
// 监听字体配置变化
|
||||||
|
watch(fontConfig, applyFontSettings, { deep: true });
|
||||||
|
// 监听Tab配置变化
|
||||||
|
watch(tabConfig, applyTabSettings, { deep: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 从URL查询参数中获取documentId
|
// 从URL查询参数中获取documentId
|
||||||
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
|
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
|
||||||
|
|
||||||
// 初始化文档存储,优先使用URL参数中的文档ID
|
// 初始化文档存储,优先使用URL参数中的文档ID
|
||||||
await documentStore.initialize(urlDocumentId);
|
await documentStore.initialize(urlDocumentId);
|
||||||
|
|
||||||
// 设置编辑器容器
|
// 设置编辑器容器
|
||||||
editorStore.setEditorContainer(editorElement.value);
|
editorStore.setEditorContainer(editorElement.value);
|
||||||
|
|
||||||
@@ -42,12 +42,14 @@ onBeforeUnmount(() => {
|
|||||||
if (editorElement.value) {
|
if (editorElement.value) {
|
||||||
editorElement.value.removeEventListener('wheel', wheelHandler);
|
editorElement.value.removeEventListener('wheel', wheelHandler);
|
||||||
}
|
}
|
||||||
|
editorStore.clearAllEditors();
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<LoadingScreen v-if="editorStore.isLoading && configStore.config.general?.enableLoadingAnimation" text="VOIDRAFT" />
|
<LoadingScreen v-if="editorStore.isLoading && configStore.config.general?.enableLoadingAnimation" text="VOIDRAFT"/>
|
||||||
<div ref="editorElement" class="editor"></div>
|
<div ref="editorElement" class="editor"></div>
|
||||||
<Toolbar/>
|
<Toolbar/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const tabHandler = (view: EditorView, tabSize: number, tabType: TabType):
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 根据tabType创建缩进字符
|
// 根据tabType创建缩进字符
|
||||||
const indent = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
|
const indent = tabType === TabType.TabTypeSpaces ? ' '.repeat(tabSize) : '\t';
|
||||||
|
|
||||||
// 在光标位置插入缩进字符
|
// 在光标位置插入缩进字符
|
||||||
const {state, dispatch} = view;
|
const {state, dispatch} = view;
|
||||||
@@ -29,7 +29,7 @@ export const getTabExtensions = (tabSize: number, enableTabIndent: boolean, tabT
|
|||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
// 根据tabType设置缩进单位
|
// 根据tabType设置缩进单位
|
||||||
const indentStr = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
|
const indentStr = tabType === TabType.TabTypeSpaces ? ' '.repeat(tabSize) : '\t';
|
||||||
extensions.push(tabSizeCompartment.of(indentUnit.of(indentStr)));
|
extensions.push(tabSizeCompartment.of(indentUnit.of(indentStr)));
|
||||||
|
|
||||||
// 如果启用了Tab缩进,添加自定义Tab键映射
|
// 如果启用了Tab缩进,添加自定义Tab键映射
|
||||||
@@ -59,7 +59,7 @@ export const updateTabConfig = (
|
|||||||
if (!view) return;
|
if (!view) return;
|
||||||
|
|
||||||
// 根据tabType更新indentUnit配置
|
// 根据tabType更新indentUnit配置
|
||||||
const indentStr = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
|
const indentStr = tabType === TabType.TabTypeSpaces ? ' '.repeat(tabSize) : '\t';
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: tabSizeCompartment.reconfigure(indentUnit.of(indentStr))
|
effects: tabSizeCompartment.reconfigure(indentUnit.of(indentStr))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ export interface CodeBlockOptions {
|
|||||||
|
|
||||||
/** 新建块时的默认语言 */
|
/** 新建块时的默认语言 */
|
||||||
defaultLanguage?: SupportedLanguage;
|
defaultLanguage?: SupportedLanguage;
|
||||||
|
|
||||||
/** 新建块时是否默认启用自动检测(添加-a标记) */
|
|
||||||
defaultAutoDetect?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,7 +84,6 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
|
|||||||
showBackground = true,
|
showBackground = true,
|
||||||
enableAutoDetection = true,
|
enableAutoDetection = true,
|
||||||
defaultLanguage = 'text',
|
defaultLanguage = 'text',
|
||||||
defaultAutoDetect = true,
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -83,56 +83,10 @@ export interface EditorOptions {
|
|||||||
defaultBlockAutoDetect: boolean;
|
defaultBlockAutoDetect: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 语言信息接口
|
|
||||||
export interface LanguageInfo {
|
|
||||||
name: SupportedLanguage;
|
|
||||||
auto: boolean; // 是否自动检测语言
|
|
||||||
}
|
|
||||||
|
|
||||||
// 位置范围接口
|
|
||||||
export interface Range {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 代码块核心接口
|
|
||||||
export interface CodeBlock {
|
|
||||||
language: LanguageInfo;
|
|
||||||
content: Range; // 内容区域
|
|
||||||
delimiter: Range; // 分隔符区域
|
|
||||||
range: Range; // 整个块区域(包括分隔符和内容)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 代码块解析选项
|
|
||||||
export interface ParseOptions {
|
|
||||||
fallbackLanguage?: SupportedLanguage;
|
|
||||||
enableAutoDetection?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分隔符格式常量
|
// 分隔符格式常量
|
||||||
export const DELIMITER_REGEX = /^\n∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/gm;
|
export const DELIMITER_REGEX = /^\n∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/gm;
|
||||||
export const DELIMITER_PREFIX = '\n∞∞∞';
|
export const DELIMITER_PREFIX = '\n∞∞∞';
|
||||||
export const DELIMITER_SUFFIX = '\n';
|
export const DELIMITER_SUFFIX = '\n';
|
||||||
export const AUTO_DETECT_SUFFIX = '-a';
|
export const AUTO_DETECT_SUFFIX = '-a';
|
||||||
|
|
||||||
// 代码块操作类型
|
|
||||||
export type BlockOperation =
|
|
||||||
| 'insert-after'
|
|
||||||
| 'insert-before'
|
|
||||||
| 'delete'
|
|
||||||
| 'move-up'
|
|
||||||
| 'move-down'
|
|
||||||
| 'change-language';
|
|
||||||
|
|
||||||
// 代码块状态更新事件
|
|
||||||
export interface BlockStateUpdate {
|
|
||||||
blocks: CodeBlock[];
|
|
||||||
activeBlockIndex: number;
|
|
||||||
operation?: BlockOperation;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 语言检测结果
|
|
||||||
export interface LanguageDetectionResult {
|
|
||||||
language: SupportedLanguage;
|
|
||||||
confidence: number;
|
|
||||||
}
|
|
||||||
39
go.sum
39
go.sum
@@ -1,14 +1,11 @@
|
|||||||
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
||||||
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||||
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
|
||||||
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
@@ -28,12 +25,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
|
|||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.0 h1:4zuFafc/qGpymx7umexxth2y2lJXoBR49c3uI0Hr+zU=
|
github.com/creativeprojects/go-selfupdate v1.5.0 h1:4zuFafc/qGpymx7umexxth2y2lJXoBR49c3uI0Hr+zU=
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.0/go.mod h1:Pewm8hY7Xe1ne7P8irVBAFnXjTkRuxbbkMlBeTdumNQ=
|
github.com/creativeprojects/go-selfupdate v1.5.0/go.mod h1:Pewm8hY7Xe1ne7P8irVBAFnXjTkRuxbbkMlBeTdumNQ=
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk=
|
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||||
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -100,10 +93,6 @@ github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7
|
|||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
|
||||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
|
||||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||||
github.com/knadh/koanf/parsers/json v1.0.0 h1:1pVR1JhMwbqSg5ICzU+surJmeBbdT4bQm7jjgnA+f8o=
|
github.com/knadh/koanf/parsers/json v1.0.0 h1:1pVR1JhMwbqSg5ICzU+surJmeBbdT4bQm7jjgnA+f8o=
|
||||||
@@ -114,8 +103,6 @@ github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ
|
|||||||
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
||||||
github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
|
github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
|
||||||
github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q=
|
github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q=
|
||||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
|
||||||
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -146,8 +133,6 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
|||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
|
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
|
||||||
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -173,20 +158,14 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||||
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
|
||||||
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
|
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
|
||||||
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.25 h1:o05zUiPEvmrq2lqqCs4wqnrnAjGmhryYHRhjQmtkvk8=
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.25 h1:o05zUiPEvmrq2lqqCs4wqnrnAjGmhryYHRhjQmtkvk8=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.25/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.25/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.31 h1:KoDwiLF4OnHx6zqm9nqmczj3pTMe4K9w2zAj9H412yM=
|
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.31/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
|
||||||
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
@@ -197,12 +176,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
|
|||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
|
|
||||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
|
||||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
@@ -214,13 +189,9 @@ golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
|
||||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -236,8 +207,6 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
@@ -246,12 +215,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
|
||||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
@@ -281,8 +246,6 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
|||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
|
||||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -293,8 +256,6 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
|
||||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
Reference in New Issue
Block a user