🚧 Optimize

This commit is contained in:
2025-10-01 22:32:57 +08:00
parent 1216b0b67c
commit 2d02bf7f1f
11 changed files with 900 additions and 263 deletions

View File

@@ -0,0 +1,13 @@
/**
* 编辑器相关常量配置
*/
// 编辑器实例管理
export const EDITOR_CONFIG = {
/** 最多缓存的编辑器实例数量 */
MAX_INSTANCES: 5,
/** 语法树缓存过期时间(毫秒) */
SYNTAX_TREE_CACHE_TIMEOUT: 30000,
/** 加载状态延迟时间(毫秒) */
LOADING_DELAY: 800,
} as const;

View 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;
}
}

View 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++;
}
}
}

View 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;
}

View 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);
}
}

View File

@@ -1,5 +1,5 @@
import {defineStore} from 'pinia';
import {nextTick, ref, watch} from 'vue';
import {computed, nextTick, ref, watch} from 'vue';
import {EditorView} from '@codemirror/view';
import {EditorState, Extension} from '@codemirror/state';
import {useConfigStore} from './configStore';
@@ -18,8 +18,11 @@ import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/edito
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
import {useExtensionStore} from './extensionStore';
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例
import {LruCache} from '@/common/utils/lruCache';
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 {
lines: number;
@@ -33,7 +36,7 @@ interface EditorInstance {
content: string;
isDirty: boolean;
lastModified: Date;
autoSaveTimer: number | null;
autoSaveTimer: TimerManager;
syntaxTreeCache: {
lastDocLength: number;
lastContentHash: string;
@@ -49,15 +52,8 @@ export const useEditorStore = defineStore('editor', () => {
const extensionStore = useExtensionStore();
// === 核心状态 ===
const editorCache = ref<{
lru: number[];
instances: Record<number, EditorInstance>;
containerElement: HTMLElement | null;
}>({
lru: [],
instances: {},
containerElement: null
});
const editorCache = new LruCache<number, EditorInstance>(EDITOR_CONFIG.MAX_INSTANCES);
const containerElement = ref<HTMLElement | null>(null);
const currentEditor = ref<EditorView | null>(null);
const documentStats = ref<DocumentStats>({
@@ -69,52 +65,17 @@ export const useEditorStore = defineStore('editor', () => {
// 编辑器加载状态
const isLoading = ref(false);
// 异步操作竞态条件控制
const operationSequence = ref(0);
const pendingOperations = ref(new Map<number, AbortController>());
const currentLoadingDocumentId = ref<number | null>(null);
// 异步操作管理器
const operationManager = new AsyncManager<number>();
// 自动保存设置 - 从配置动态获取
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 instance = editorCache.value.instances[documentId];
const instance = editorCache.get(documentId);
if (!instance) return;
const docLength = view.state.doc.length;
@@ -127,7 +88,7 @@ export const useEditorStore = defineStore('editor', () => {
const shouldRebuild = !cache ||
cache.lastDocLength !== docLength ||
cache.lastContentHash !== contentHash ||
(now.getTime() - cache.lastParsed.getTime()) > 30000; // 30秒过期
(now.getTime() - cache.lastParsed.getTime()) > EDITOR_CONFIG.SYNTAX_TREE_CACHE_TIMEOUT;
if (shouldRebuild) {
try {
@@ -151,12 +112,12 @@ export const useEditorStore = defineStore('editor', () => {
operationId: number,
documentId: number
): Promise<EditorView> => {
if (!editorCache.value.containerElement) {
if (!containerElement.value) {
throw new Error('Editor container not set');
}
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
if (!operationManager.isOperationValid(operationId, documentId)) {
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');
}
@@ -204,7 +165,7 @@ export const useEditorStore = defineStore('editor', () => {
const keymapExtension = await createDynamicKeymapExtension();
// 检查操作有效性
if (!isOperationValid(operationId, documentId)) {
if (!operationManager.isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
@@ -212,7 +173,7 @@ export const useEditorStore = defineStore('editor', () => {
const dynamicExtensions = await createDynamicExtensions(documentId);
// 最终检查操作有效性
if (!isOperationValid(operationId, documentId)) {
if (!operationManager.isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
@@ -252,52 +213,31 @@ export const useEditorStore = defineStore('editor', () => {
// 添加编辑器到缓存
const addEditorToCache = (documentId: number, view: EditorView, content: string) => {
// 如果缓存已满,移除最少使用的编辑器
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] = {
const instance: EditorInstance = {
view,
documentId,
content,
isDirty: false,
lastModified: new Date(),
autoSaveTimer: null,
autoSaveTimer: createTimerManager(),
syntaxTreeCache: null
};
// 添加到LRU列表
editorCache.value.lru.push(documentId);
// 使用LRU缓存的onEvict回调处理被驱逐的实例
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);
};
// 更新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 (
documentId: number,
@@ -305,14 +245,13 @@ export const useEditorStore = defineStore('editor', () => {
operationId: number
): Promise<EditorView> => {
// 检查缓存
const cached = editorCache.value.instances[documentId];
const cached = editorCache.get(documentId);
if (cached) {
updateLRU(documentId);
return cached.view;
}
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
if (!operationManager.isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
@@ -320,7 +259,7 @@ export const useEditorStore = defineStore('editor', () => {
const view = await createEditorInstance(content, operationId, documentId);
// 最终检查操作有效性
if (!isOperationValid(operationId, documentId)) {
if (!operationManager.isOperationValid(operationId, documentId)) {
// 如果操作已取消,清理创建的实例
view.destroy();
throw new Error('Operation cancelled');
@@ -333,8 +272,8 @@ export const useEditorStore = defineStore('editor', () => {
// 显示编辑器
const showEditor = (documentId: number) => {
const instance = editorCache.value.instances[documentId];
if (!instance || !editorCache.value.containerElement) return;
const instance = editorCache.get(documentId);
if (!instance || !containerElement.value) return;
try {
// 移除当前编辑器DOM
@@ -343,18 +282,15 @@ export const useEditorStore = defineStore('editor', () => {
}
// 确保容器为空
editorCache.value.containerElement.innerHTML = '';
containerElement.value.innerHTML = '';
// 将目标编辑器DOM添加到容器
editorCache.value.containerElement.appendChild(instance.view.dom);
containerElement.value.appendChild(instance.view.dom);
currentEditor.value = instance.view;
// 设置扩展管理器视图
setExtensionManagerView(instance.view, documentId);
// 更新LRU
updateLRU(documentId);
// 重新测量和聚焦编辑器
nextTick(() => {
// 将光标定位到文档末尾并滚动到该位置
@@ -377,7 +313,7 @@ export const useEditorStore = defineStore('editor', () => {
// 保存编辑器内容
const saveEditorContent = async (documentId: number): Promise<boolean> => {
const instance = editorCache.value.instances[documentId];
const instance = editorCache.get(documentId);
if (!instance || !instance.isDirty) return true;
try {
@@ -403,7 +339,7 @@ export const useEditorStore = defineStore('editor', () => {
// 内容变化处理
const onContentChange = (documentId: number) => {
const instance = editorCache.value.instances[documentId];
const instance = editorCache.get(documentId);
if (!instance) return;
instance.isDirty = true;
@@ -412,13 +348,8 @@ export const useEditorStore = defineStore('editor', () => {
// 清理语法树缓存,下次访问时重新构建
instance.syntaxTreeCache = null;
// 清除之前的定时器
if (instance.autoSaveTimer) {
clearTimeout(instance.autoSaveTimer);
}
// 设置新的自动保存定时器
instance.autoSaveTimer = window.setTimeout(() => {
// 设置自动保存定时器
instance.autoSaveTimer.set(() => {
saveEditorContent(documentId);
}, getAutoSaveDelay());
};
@@ -427,7 +358,7 @@ export const useEditorStore = defineStore('editor', () => {
// 设置编辑器容器
const setEditorContainer = (container: HTMLElement | null) => {
editorCache.value.containerElement = container;
containerElement.value = container;
// 如果设置容器时已有当前文档,立即加载编辑器
if (container && documentStore.currentDocument) {
@@ -439,9 +370,9 @@ export const useEditorStore = defineStore('editor', () => {
const loadEditor = async (documentId: number, content: string) => {
// 设置加载状态
isLoading.value = true;
// 生成新的操作ID
const operationId = getNextOperationId();
const abortController = new AbortController();
// 开始新的操作
const { operationId } = operationManager.startOperation(documentId);
try {
// 验证参数
@@ -449,11 +380,6 @@ export const useEditorStore = defineStore('editor', () => {
throw new Error('Invalid parameters for loadEditor');
}
// 取消之前的操作并设置当前操作
cancelPreviousOperations();
currentLoadingDocumentId.value = documentId;
pendingOperations.value.set(operationId, abortController);
// 保存当前编辑器内容
if (currentEditor.value) {
const currentDocId = documentStore.currentDocumentId;
@@ -461,7 +387,7 @@ export const useEditorStore = defineStore('editor', () => {
await saveEditorContent(currentDocId);
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
if (!operationManager.isOperationValid(operationId, documentId)) {
return;
}
}
@@ -471,12 +397,12 @@ export const useEditorStore = defineStore('editor', () => {
const view = await getOrCreateEditor(documentId, content, operationId);
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
if (!operationManager.isOperationValid(operationId, documentId)) {
return;
}
// 更新内容(如果需要)
const instance = editorCache.value.instances[documentId];
const instance = editorCache.get(documentId);
if (instance && instance.content !== content) {
// 确保编辑器视图有效
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;
}
@@ -509,35 +435,28 @@ export const useEditorStore = defineStore('editor', () => {
console.error('Failed to load editor:', error);
}
} finally {
// 清理操作记录
pendingOperations.value.delete(operationId);
if (currentLoadingDocumentId.value === documentId) {
currentLoadingDocumentId.value = null;
}
// 完成操作
operationManager.completeOperation(operationId);
// 延迟一段时间后再取消加载状态
setTimeout(() => {
isLoading.value = false;
}, 800);
}, EDITOR_CONFIG.LOADING_DELAY);
}
};
// 移除编辑器
const removeEditor = (documentId: number) => {
const instance = editorCache.value.instances[documentId];
const instance = editorCache.get(documentId);
if (instance) {
try {
// 如果正在加载这个文档,取消操作
if (currentLoadingDocumentId.value === documentId) {
cancelPreviousOperations();
currentLoadingDocumentId.value = null;
if (operationManager.getCurrentContext() === documentId) {
operationManager.cancelAllOperations();
}
// 清除自动保存定时器
if (instance.autoSaveTimer) {
clearTimeout(instance.autoSaveTimer);
instance.autoSaveTimer = null;
}
instance.autoSaveTimer.clear();
// 从扩展管理器中移除视图
removeExtensionManagerView(documentId);
@@ -557,12 +476,8 @@ export const useEditorStore = defineStore('editor', () => {
currentEditor.value = null;
}
delete editorCache.value.instances[documentId];
const lruIndex = editorCache.value.lru.indexOf(documentId);
if (lruIndex > -1) {
editorCache.value.lru.splice(lruIndex, 1);
}
// 从缓存中删除
editorCache.delete(documentId);
} catch (error) {
console.error('Error removing editor:', error);
}
@@ -576,7 +491,7 @@ export const useEditorStore = defineStore('editor', () => {
// 应用字体设置
const applyFontSettings = () => {
Object.values(editorCache.value.instances).forEach(instance => {
editorCache.values().forEach(instance => {
updateFontConfig(instance.view, {
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
@@ -588,7 +503,7 @@ export const useEditorStore = defineStore('editor', () => {
// 应用主题设置
const applyThemeSettings = () => {
Object.values(editorCache.value.instances).forEach(instance => {
editorCache.values().forEach(instance => {
updateEditorTheme(instance.view,
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
);
@@ -597,7 +512,7 @@ export const useEditorStore = defineStore('editor', () => {
// 应用Tab设置
const applyTabSettings = () => {
Object.values(editorCache.value.instances).forEach(instance => {
editorCache.values().forEach(instance => {
updateTabConfig(
instance.view,
configStore.config.editing.tabSize,
@@ -611,7 +526,7 @@ export const useEditorStore = defineStore('editor', () => {
const applyKeymapSettings = async () => {
// 确保所有编辑器实例的快捷键都更新
await Promise.all(
Object.values(editorCache.value.instances).map(instance =>
editorCache.values().map(instance =>
updateKeymapExtension(instance.view)
)
);
@@ -620,14 +535,11 @@ export const useEditorStore = defineStore('editor', () => {
// 清空所有编辑器
const clearAllEditors = () => {
// 取消所有挂起的操作
cancelPreviousOperations();
currentLoadingDocumentId.value = null;
operationManager.cancelAllOperations();
Object.values(editorCache.value.instances).forEach(instance => {
editorCache.clear((_documentId, instance) => {
// 清除自动保存定时器
if (instance.autoSaveTimer) {
clearTimeout(instance.autoSaveTimer);
}
instance.autoSaveTimer.clear();
// 从扩展管理器移除
removeExtensionManagerView(instance.documentId);
@@ -640,8 +552,6 @@ export const useEditorStore = defineStore('editor', () => {
instance.view.destroy();
});
editorCache.value.instances = {};
editorCache.value.lru = [];
currentEditor.value = null;
};
@@ -665,29 +575,36 @@ export const useEditorStore = defineStore('editor', () => {
// 重新加载扩展配置
await extensionStore.loadExtensions();
// 不再需要单独更新当前编辑器的快捷键映射,因为扩展管理器会更新所有实例
// 但我们仍需要确保快捷键配置在所有编辑器上更新
await applyKeymapSettings();
};
// 监听文档切换
watch(() => documentStore.currentDocument, (newDoc) => {
if (newDoc && editorCache.value.containerElement) {
watch(() => documentStore.currentDocument, async (newDoc) => {
if (newDoc && containerElement.value) {
// 使用 nextTick 确保DOM更新完成后再加载编辑器
nextTick(() => {
await nextTick(() => {
loadEditor(newDoc.id, newDoc.content);
});
}
});
// 监听配置变化
watch(() => configStore.config.editing.fontSize, applyFontSettings);
watch(() => configStore.config.editing.fontFamily, applyFontSettings);
watch(() => configStore.config.editing.lineHeight, applyFontSettings);
watch(() => configStore.config.editing.fontWeight, applyFontSettings);
watch(() => configStore.config.editing.tabSize, applyTabSettings);
watch(() => configStore.config.editing.enableTabIndent, applyTabSettings);
watch(() => configStore.config.editing.tabType, applyTabSettings);
// 创建字体配置的计算属性
const fontConfig = computed(() => ({
fontSize: configStore.config.editing.fontSize,
fontFamily: configStore.config.editing.fontFamily,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
}));
// 创建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 {
// 状态

View File

@@ -26,10 +26,10 @@ onMounted(async () => {
// 从URL查询参数中获取documentId
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
// 初始化文档存储优先使用URL参数中的文档ID
await documentStore.initialize(urlDocumentId);
// 设置编辑器容器
editorStore.setEditorContainer(editorElement.value);
@@ -42,12 +42,14 @@ onBeforeUnmount(() => {
if (editorElement.value) {
editorElement.value.removeEventListener('wheel', wheelHandler);
}
editorStore.clearAllEditors();
});
</script>
<template>
<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>
<Toolbar/>
</div>

View File

@@ -16,7 +16,7 @@ export const tabHandler = (view: EditorView, tabSize: number, tabType: TabType):
}
// 根据tabType创建缩进字符
const indent = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
const indent = tabType === TabType.TabTypeSpaces ? ' '.repeat(tabSize) : '\t';
// 在光标位置插入缩进字符
const {state, dispatch} = view;
@@ -29,7 +29,7 @@ export const getTabExtensions = (tabSize: number, enableTabIndent: boolean, tabT
const extensions: Extension[] = [];
// 根据tabType设置缩进单位
const indentStr = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
const indentStr = tabType === TabType.TabTypeSpaces ? ' '.repeat(tabSize) : '\t';
extensions.push(tabSizeCompartment.of(indentUnit.of(indentStr)));
// 如果启用了Tab缩进添加自定义Tab键映射
@@ -59,7 +59,7 @@ export const updateTabConfig = (
if (!view) return;
// 根据tabType更新indentUnit配置
const indentStr = tabType === 'spaces' ? ' '.repeat(tabSize) : '\t';
const indentStr = tabType === TabType.TabTypeSpaces ? ' '.repeat(tabSize) : '\t';
view.dispatch({
effects: tabSizeCompartment.reconfigure(indentUnit.of(indentStr))
});

View File

@@ -38,9 +38,6 @@ export interface CodeBlockOptions {
/** 新建块时的默认语言 */
defaultLanguage?: SupportedLanguage;
/** 新建块时是否默认启用自动检测(添加-a标记 */
defaultAutoDetect?: boolean;
}
/**
@@ -87,7 +84,6 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
showBackground = true,
enableAutoDetection = true,
defaultLanguage = 'text',
defaultAutoDetect = true,
} = options;
return [

View File

@@ -83,56 +83,10 @@ export interface EditorOptions {
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_PREFIX = '\n∞∞∞';
export const DELIMITER_SUFFIX = '\n';
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
View File

@@ -1,14 +1,11 @@
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.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/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/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
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/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/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
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/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.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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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.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/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
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/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.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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
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/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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/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/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
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/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.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/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
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/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/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
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/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
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.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
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/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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
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/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
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/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
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/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
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/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=