🎨 Updated
This commit is contained in:
@@ -299,16 +299,6 @@ export class EditingConfig {
|
|||||||
*/
|
*/
|
||||||
"autoSaveDelay": number;
|
"autoSaveDelay": number;
|
||||||
|
|
||||||
/**
|
|
||||||
* 变更字符阈值
|
|
||||||
*/
|
|
||||||
"changeThreshold": number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 最小保存间隔(毫秒)
|
|
||||||
*/
|
|
||||||
"minSaveInterval": number;
|
|
||||||
|
|
||||||
/** Creates a new EditingConfig instance. */
|
/** Creates a new EditingConfig instance. */
|
||||||
constructor($$source: Partial<EditingConfig> = {}) {
|
constructor($$source: Partial<EditingConfig> = {}) {
|
||||||
if (!("fontSize" in $$source)) {
|
if (!("fontSize" in $$source)) {
|
||||||
@@ -335,12 +325,6 @@ export class EditingConfig {
|
|||||||
if (!("autoSaveDelay" in $$source)) {
|
if (!("autoSaveDelay" in $$source)) {
|
||||||
this["autoSaveDelay"] = 0;
|
this["autoSaveDelay"] = 0;
|
||||||
}
|
}
|
||||||
if (!("changeThreshold" in $$source)) {
|
|
||||||
this["changeThreshold"] = 0;
|
|
||||||
}
|
|
||||||
if (!("minSaveInterval" in $$source)) {
|
|
||||||
this["minSaveInterval"] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
Object.assign(this, $$source);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DialogService 对话框服务,处理文件选择等对话框操作
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SelectDirectory 打开目录选择对话框
|
||||||
|
*/
|
||||||
|
export function SelectDirectory(): Promise<string> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2249533621) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
@@ -14,10 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as models$0 from "../models/models.js";
|
import * as models$0 from "../models/models.js";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore: Unused imports
|
|
||||||
import * as $models from "./models.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ForceSave 强制保存当前文档
|
* ForceSave 强制保存当前文档
|
||||||
*/
|
*/
|
||||||
@@ -46,30 +42,6 @@ export function GetActiveDocumentContent(): Promise<string> & { cancel(): void }
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GetDiffInfo 获取两个文本之间的详细差异信息
|
|
||||||
*/
|
|
||||||
export function GetDiffInfo(oldText: string, newText: string): Promise<$models.DiffResult> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2490726526, oldText, newText) as any;
|
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
|
||||||
return $$createType2($result);
|
|
||||||
}) as any;
|
|
||||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
|
||||||
return $typingPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetSaveSettings 获取文档保存设置
|
|
||||||
*/
|
|
||||||
export function GetSaveSettings(): Promise<models$0.EditingConfig | null> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(4257471801) as any;
|
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
|
||||||
return $$createType4($result);
|
|
||||||
}) as any;
|
|
||||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
|
||||||
return $typingPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize 初始化文档服务
|
* Initialize 初始化文档服务
|
||||||
*/
|
*/
|
||||||
@@ -79,15 +51,7 @@ export function Initialize(): Promise<void> & { cancel(): void } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LoadDefaultDocument 加载默认文档
|
* SaveDocumentSync 同步保存文档内容
|
||||||
*/
|
|
||||||
export function LoadDefaultDocument(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2343023569) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SaveDocumentSync 同步保存文档内容 (用于页面关闭前同步保存)
|
|
||||||
*/
|
*/
|
||||||
export function SaveDocumentSync(content: string): Promise<void> & { cancel(): void } {
|
export function SaveDocumentSync(content: string): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(3770207288, content) as any;
|
let $resultPromise = $Call.ByID(3770207288, content) as any;
|
||||||
@@ -95,29 +59,13 @@ export function SaveDocumentSync(content: string): Promise<void> & { cancel(): v
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServiceShutdown 实现应用程序关闭时的服务关闭逻辑
|
* ServiceShutdown 服务关闭
|
||||||
*/
|
*/
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(638578044) as any;
|
let $resultPromise = $Call.ByID(638578044) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* SetSaveCallback 设置保存回调函数
|
|
||||||
*/
|
|
||||||
export function SetSaveCallback(callback: any): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(675315211, callback) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shutdown 关闭文档服务,确保所有数据保存
|
|
||||||
*/
|
|
||||||
export function Shutdown(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3444504909) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateActiveDocumentContent 更新当前活动文档内容
|
* UpdateActiveDocumentContent 更新当前活动文档内容
|
||||||
*/
|
*/
|
||||||
@@ -126,17 +74,6 @@ export function UpdateActiveDocumentContent(content: string): Promise<void> & {
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* UpdateSaveSettings 更新文档保存设置
|
|
||||||
*/
|
|
||||||
export function UpdateSaveSettings(docConfig: models$0.EditingConfig): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1245479534, docConfig) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = models$0.Document.createFrom;
|
const $$createType0 = models$0.Document.createFrom;
|
||||||
const $$createType1 = $Create.Nullable($$createType0);
|
const $$createType1 = $Create.Nullable($$createType0);
|
||||||
const $$createType2 = $models.DiffResult.createFrom;
|
|
||||||
const $$createType3 = models$0.EditingConfig.createFrom;
|
|
||||||
const $$createType4 = $Create.Nullable($$createType3);
|
|
||||||
|
@@ -2,11 +2,13 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
import * as ConfigService from "./configservice.js";
|
import * as ConfigService from "./configservice.js";
|
||||||
|
import * as DialogService from "./dialogservice.js";
|
||||||
import * as DocumentService from "./documentservice.js";
|
import * as DocumentService from "./documentservice.js";
|
||||||
import * as HotkeyService from "./hotkeyservice.js";
|
import * as HotkeyService from "./hotkeyservice.js";
|
||||||
import * as SystemService from "./systemservice.js";
|
import * as SystemService from "./systemservice.js";
|
||||||
export {
|
export {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
|
DialogService,
|
||||||
DocumentService,
|
DocumentService,
|
||||||
HotkeyService,
|
HotkeyService,
|
||||||
SystemService
|
SystemService
|
||||||
|
@@ -5,137 +5,6 @@
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import {Create as $Create} from "@wailsio/runtime";
|
import {Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
/**
|
|
||||||
* DiffResult 包含差异比较的结果信息
|
|
||||||
*/
|
|
||||||
export class DiffResult {
|
|
||||||
/**
|
|
||||||
* 编辑操作列表
|
|
||||||
*/
|
|
||||||
"Edits": Edit[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入的字符数
|
|
||||||
*/
|
|
||||||
"InsertCount": number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除的字符数
|
|
||||||
*/
|
|
||||||
"DeleteCount": number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 变更的行数
|
|
||||||
*/
|
|
||||||
"ChangedLines": number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 总变更字符数(插入+删除)
|
|
||||||
*/
|
|
||||||
"TotalChanges": number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 变更的token数(如单词、标识符等)
|
|
||||||
*/
|
|
||||||
"ChangedTokens": number;
|
|
||||||
|
|
||||||
/** Creates a new DiffResult instance. */
|
|
||||||
constructor($$source: Partial<DiffResult> = {}) {
|
|
||||||
if (!("Edits" in $$source)) {
|
|
||||||
this["Edits"] = [];
|
|
||||||
}
|
|
||||||
if (!("InsertCount" in $$source)) {
|
|
||||||
this["InsertCount"] = 0;
|
|
||||||
}
|
|
||||||
if (!("DeleteCount" in $$source)) {
|
|
||||||
this["DeleteCount"] = 0;
|
|
||||||
}
|
|
||||||
if (!("ChangedLines" in $$source)) {
|
|
||||||
this["ChangedLines"] = 0;
|
|
||||||
}
|
|
||||||
if (!("TotalChanges" in $$source)) {
|
|
||||||
this["TotalChanges"] = 0;
|
|
||||||
}
|
|
||||||
if (!("ChangedTokens" in $$source)) {
|
|
||||||
this["ChangedTokens"] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new DiffResult instance from a string or object.
|
|
||||||
*/
|
|
||||||
static createFrom($$source: any = {}): DiffResult {
|
|
||||||
const $$createField0_0 = $$createType1;
|
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
|
||||||
if ("Edits" in $$parsedSource) {
|
|
||||||
$$parsedSource["Edits"] = $$createField0_0($$parsedSource["Edits"]);
|
|
||||||
}
|
|
||||||
return new DiffResult($$parsedSource as Partial<DiffResult>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit 表示单个编辑操作
|
|
||||||
*/
|
|
||||||
export class Edit {
|
|
||||||
/**
|
|
||||||
* 操作类型
|
|
||||||
*/
|
|
||||||
"Type": EditType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作内容
|
|
||||||
*/
|
|
||||||
"Content": string;
|
|
||||||
|
|
||||||
/** Creates a new Edit instance. */
|
|
||||||
constructor($$source: Partial<Edit> = {}) {
|
|
||||||
if (!("Type" in $$source)) {
|
|
||||||
this["Type"] = (0 as EditType);
|
|
||||||
}
|
|
||||||
if (!("Content" in $$source)) {
|
|
||||||
this["Content"] = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new Edit instance from a string or object.
|
|
||||||
*/
|
|
||||||
static createFrom($$source: any = {}): Edit {
|
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
|
||||||
return new Edit($$parsedSource as Partial<Edit>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit 表示编辑操作类型
|
|
||||||
*/
|
|
||||||
export enum EditType {
|
|
||||||
/**
|
|
||||||
* The Go zero value for the underlying type of the enum.
|
|
||||||
*/
|
|
||||||
$zero = 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EditInsert 插入操作
|
|
||||||
*/
|
|
||||||
EditInsert = 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EditDelete 删除操作
|
|
||||||
*/
|
|
||||||
EditDelete = 1,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EditEqual 相等部分
|
|
||||||
*/
|
|
||||||
EditEqual = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MemoryStats 内存统计信息
|
* MemoryStats 内存统计信息
|
||||||
*/
|
*/
|
||||||
@@ -202,7 +71,3 @@ export class MemoryStats {
|
|||||||
return new MemoryStats($$parsedSource as Partial<MemoryStats>);
|
return new MemoryStats($$parsedSource as Partial<MemoryStats>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
|
||||||
const $$createType0 = Edit.createFrom;
|
|
||||||
const $$createType1 = $Create.Array($$createType0);
|
|
||||||
|
@@ -73,10 +73,9 @@ export default {
|
|||||||
selectDirectory: 'Select Directory',
|
selectDirectory: 'Select Directory',
|
||||||
selectDataDirectory: 'Select Data Storage Directory',
|
selectDataDirectory: 'Select Data Storage Directory',
|
||||||
defaultDataPath: 'Default data storage path',
|
defaultDataPath: 'Default data storage path',
|
||||||
enterCustomPath: 'Enter custom data storage path',
|
|
||||||
pathHint: 'Example: C:\\MyData or /home/user/data or relative path ./data',
|
|
||||||
bufferFiles: 'Buffer Files Path',
|
bufferFiles: 'Buffer Files Path',
|
||||||
useCustomLocation: 'Use custom location for buffer files',
|
useCustomLocation: 'Use custom location for buffer files',
|
||||||
|
customDataPath: 'Custom Data Storage Path',
|
||||||
fontSize: 'Font Size',
|
fontSize: 'Font Size',
|
||||||
fontSizeDescription: 'Editor font size',
|
fontSizeDescription: 'Editor font size',
|
||||||
fontSettings: 'Font Settings',
|
fontSettings: 'Font Settings',
|
||||||
@@ -96,7 +95,6 @@ export default {
|
|||||||
restartRequired: '(Restart required)',
|
restartRequired: '(Restart required)',
|
||||||
saveOptions: 'Save Options',
|
saveOptions: 'Save Options',
|
||||||
autoSaveDelay: 'Auto Save Delay (ms)',
|
autoSaveDelay: 'Auto Save Delay (ms)',
|
||||||
changeThreshold: 'Change Threshold',
|
selectDirectoryFailed: 'Failed to select directory'
|
||||||
minSaveInterval: 'Min Save Interval (ms)'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
@@ -73,10 +73,9 @@ export default {
|
|||||||
selectDirectory: '选择目录',
|
selectDirectory: '选择目录',
|
||||||
selectDataDirectory: '选择数据存储目录',
|
selectDataDirectory: '选择数据存储目录',
|
||||||
defaultDataPath: '默认数据存储路径',
|
defaultDataPath: '默认数据存储路径',
|
||||||
enterCustomPath: '请输入自定义数据存储路径',
|
|
||||||
pathHint: '例如: C:\\MyData 或 /home/user/data 或相对路径 ./data',
|
|
||||||
bufferFiles: '缓冲文件路径',
|
bufferFiles: '缓冲文件路径',
|
||||||
useCustomLocation: '使用自定义位置存储缓冲文件',
|
useCustomLocation: '使用自定义位置存储缓冲文件',
|
||||||
|
customDataPath: '自定义数据存储路径',
|
||||||
fontSize: '字体大小',
|
fontSize: '字体大小',
|
||||||
fontSizeDescription: '编辑器字体大小',
|
fontSizeDescription: '编辑器字体大小',
|
||||||
fontSettings: '字体设置',
|
fontSettings: '字体设置',
|
||||||
@@ -96,7 +95,6 @@ export default {
|
|||||||
restartRequired: '(需要重启)',
|
restartRequired: '(需要重启)',
|
||||||
saveOptions: '保存选项',
|
saveOptions: '保存选项',
|
||||||
autoSaveDelay: '自动保存延迟(毫秒)',
|
autoSaveDelay: '自动保存延迟(毫秒)',
|
||||||
changeThreshold: '变更字符阈值',
|
selectDirectoryFailed: '选择目录失败'
|
||||||
minSaveInterval: '最小保存间隔(毫秒)'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
@@ -62,9 +62,7 @@ const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
|||||||
enableTabIndent: 'editing.enable_tab_indent',
|
enableTabIndent: 'editing.enable_tab_indent',
|
||||||
tabSize: 'editing.tab_size',
|
tabSize: 'editing.tab_size',
|
||||||
tabType: 'editing.tab_type',
|
tabType: 'editing.tab_type',
|
||||||
autoSaveDelay: 'editing.auto_save_delay',
|
autoSaveDelay: 'editing.auto_save_delay'
|
||||||
changeThreshold: 'editing.change_threshold',
|
|
||||||
minSaveInterval: 'editing.min_save_interval'
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||||
@@ -137,9 +135,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
enableTabIndent: true,
|
enableTabIndent: true,
|
||||||
tabSize: CONFIG_LIMITS.tabSize.default,
|
tabSize: CONFIG_LIMITS.tabSize.default,
|
||||||
tabType: CONFIG_LIMITS.tabType.default,
|
tabType: CONFIG_LIMITS.tabType.default,
|
||||||
autoSaveDelay: 5000,
|
autoSaveDelay: 5000
|
||||||
changeThreshold: 500,
|
|
||||||
minSaveInterval: 1000
|
|
||||||
},
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
language: LanguageType.LangZhCN
|
language: LanguageType.LangZhCN
|
||||||
@@ -338,9 +334,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
fontFamily: (value: string) => safeCall(() => updateEditingConfig('fontFamily', value), 'config.saveFailed', 'config.saveSuccess'),
|
fontFamily: (value: string) => safeCall(() => updateEditingConfig('fontFamily', value), 'config.saveFailed', 'config.saveSuccess'),
|
||||||
fontWeight: (value: string) => safeCall(() => updateEditingConfig('fontWeight', value), 'config.saveFailed', 'config.saveSuccess'),
|
fontWeight: (value: string) => safeCall(() => updateEditingConfig('fontWeight', value), 'config.saveFailed', 'config.saveSuccess'),
|
||||||
defaultDataPath: (value: string) => safeCall(() => updateGeneralConfig('defaultDataPath', value), 'config.saveFailed', 'config.saveSuccess'),
|
defaultDataPath: (value: string) => safeCall(() => updateGeneralConfig('defaultDataPath', value), 'config.saveFailed', 'config.saveSuccess'),
|
||||||
autoSaveDelay: (value: number) => safeCall(() => updateEditingConfig('autoSaveDelay', value), 'config.saveFailed', 'config.saveSuccess'),
|
autoSaveDelay: (value: number) => safeCall(() => updateEditingConfig('autoSaveDelay', value), 'config.saveFailed', 'config.saveSuccess')
|
||||||
changeThreshold: (value: number) => safeCall(() => updateEditingConfig('changeThreshold', value), 'config.saveFailed', 'config.saveSuccess'),
|
|
||||||
minSaveInterval: (value: number) => safeCall(() => updateEditingConfig('minSaveInterval', value), 'config.saveFailed', 'config.saveSuccess')
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -394,8 +388,6 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
|
|
||||||
// 保存配置相关方法
|
// 保存配置相关方法
|
||||||
setAutoSaveDelay: setters.autoSaveDelay,
|
setAutoSaveDelay: setters.autoSaveDelay,
|
||||||
setChangeThreshold: setters.changeThreshold,
|
|
||||||
setMinSaveInterval: setters.minSaveInterval,
|
|
||||||
|
|
||||||
// 热键配置相关方法
|
// 热键配置相关方法
|
||||||
setEnableGlobalHotkey: (value: boolean) => safeCall(() => updateGeneralConfig('enableGlobalHotkey', value), 'config.saveFailed', 'config.saveSuccess'),
|
setEnableGlobalHotkey: (value: boolean) => safeCall(() => updateGeneralConfig('enableGlobalHotkey', value), 'config.saveFailed', 'config.saveSuccess'),
|
||||||
|
@@ -12,6 +12,12 @@ export interface AutoSaveOptions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建自动保存插件
|
* 创建自动保存插件
|
||||||
|
*
|
||||||
|
* 新的简化保存策略:
|
||||||
|
* - 前端只负责将内容变更传递给后端
|
||||||
|
* - 后端使用定时保存机制,每隔配置的时间间隔自动保存(仅在有变更时)
|
||||||
|
* - 移除了复杂的阈值保存和最小间隔控制
|
||||||
|
*
|
||||||
* @param options 配置选项
|
* @param options 配置选项
|
||||||
* @returns EditorView.Plugin
|
* @returns EditorView.Plugin
|
||||||
*/
|
*/
|
||||||
|
@@ -138,27 +138,31 @@ const handleAutoSaveDelayChange = async (event: Event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeThresholdChange = async (event: Event) => {
|
// 动态字体预览文本
|
||||||
const target = event.target as HTMLInputElement;
|
const fontPreviewText = computed(() => {
|
||||||
const value = parseInt(target.value, 10);
|
const currentFont = configStore.config.editing.fontFamily;
|
||||||
if (!isNaN(value) && value >= 10 && value <= 10000) {
|
|
||||||
await safeCall(
|
|
||||||
() => configStore.setChangeThreshold(value),
|
|
||||||
'config.changeThresholdUpdateFailed'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMinSaveIntervalChange = async (event: Event) => {
|
// 根据字体类型返回不同的预览文本
|
||||||
const target = event.target as HTMLInputElement;
|
if (currentFont.includes('HarmonyOS')) {
|
||||||
const value = parseInt(target.value, 10);
|
return '鸿蒙字体测试';
|
||||||
if (!isNaN(value) && value >= 100 && value <= 10000) {
|
} else if (currentFont.includes('Microsoft YaHei')) {
|
||||||
await safeCall(
|
return '微软雅黑测试';
|
||||||
() => configStore.setMinSaveInterval(value),
|
} else if (currentFont.includes('PingFang')) {
|
||||||
'config.minSaveIntervalUpdateFailed'
|
return '苹方字体测试';
|
||||||
);
|
} else if (currentFont.includes('JetBrains')) {
|
||||||
|
return 'JetBrains Mono';
|
||||||
|
} else if (currentFont.includes('Fira Code')) {
|
||||||
|
return 'Fira Code Test';
|
||||||
|
} else if (currentFont.includes('Source Code')) {
|
||||||
|
return 'Source Code Pro';
|
||||||
|
} else if (currentFont.includes('Cascadia')) {
|
||||||
|
return 'Cascadia Code';
|
||||||
|
} else if (currentFont.includes('SF Mono') || currentFont.includes('Monaco')) {
|
||||||
|
return 'System Monospace';
|
||||||
|
} else {
|
||||||
|
return 'Font Preview';
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -234,7 +238,7 @@ const handleMinSaveIntervalChange = async (event: Event) => {
|
|||||||
<div class="preview-text">
|
<div class="preview-text">
|
||||||
<span>function example() {</span>
|
<span>function example() {</span>
|
||||||
<span class="indent">console.log("Hello, 世界!");</span>
|
<span class="indent">console.log("Hello, 世界!");</span>
|
||||||
<span class="indent">const message = "鸿蒙字体测试";</span>
|
<span class="indent">const message = "{{ fontPreviewText }}";</span>
|
||||||
<span>}</span>
|
<span>}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -276,7 +280,7 @@ const handleMinSaveIntervalChange = async (event: Event) => {
|
|||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection :title="t('settings.saveOptions')">
|
<SettingSection :title="t('settings.saveOptions')">
|
||||||
<SettingItem :title="t('settings.autoSaveDelay')" :description="'单位:毫秒'">
|
<SettingItem :title="t('settings.autoSaveDelay')" :description="'定时保存间隔,每隔指定时间自动保存(仅在有变更时)'">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="number-input"
|
class="number-input"
|
||||||
@@ -284,24 +288,6 @@ const handleMinSaveIntervalChange = async (event: Event) => {
|
|||||||
@change="handleAutoSaveDelayChange"
|
@change="handleAutoSaveDelayChange"
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem :title="t('settings.changeThreshold')" :description="'变更字符超过此阈值时触发保存'">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="number-input"
|
|
||||||
:value="configStore.config.editing.changeThreshold"
|
|
||||||
@change="handleChangeThresholdChange"
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
<SettingItem :title="t('settings.minSaveInterval')" :description="'单位:毫秒'">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="number-input"
|
|
||||||
:value="configStore.config.editing.minSaveInterval"
|
|
||||||
@change="handleMinSaveIntervalChange"
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useConfigStore } from '@/stores/configStore';
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed} from 'vue';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
import SettingItem from '../components/SettingItem.vue';
|
import SettingItem from '../components/SettingItem.vue';
|
||||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||||
import { useErrorHandler } from '@/utils/errorHandler';
|
import { useErrorHandler } from '@/utils/errorHandler';
|
||||||
|
import { DialogService } from '@/../bindings/voidraft/internal/services';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -92,27 +93,28 @@ const useCustomDataPath = computed({
|
|||||||
set: (value: boolean) => configStore.setUseCustomDataPath(value)
|
set: (value: boolean) => configStore.setUseCustomDataPath(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自定义路径输入
|
// 自定义路径显示
|
||||||
const customPathInput = ref('');
|
const customPathDisplay = computed(() => {
|
||||||
|
return configStore.config.general.customDataPath || '';
|
||||||
|
});
|
||||||
|
|
||||||
// 监听自定义路径的变化,同步到配置
|
const selectDirectory = async () => {
|
||||||
const updateCustomPath = () => {
|
// 只有开启自定义路径时才能选择
|
||||||
configStore.setCustomDataPath(customPathInput.value.trim());
|
if (!useCustomDataPath.value) return;
|
||||||
};
|
|
||||||
|
|
||||||
// 当启用自定义路径时,初始化输入框的值
|
try {
|
||||||
const initCustomPath = () => {
|
const selectedPath = await DialogService.SelectDirectory();
|
||||||
if (useCustomDataPath.value) {
|
|
||||||
customPathInput.value = configStore.config.general.customDataPath || '';
|
if (selectedPath && selectedPath.trim()) {
|
||||||
|
await configStore.setCustomDataPath(selectedPath.trim());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 可以添加用户友好的错误提示
|
||||||
|
await safeCall(async () => {
|
||||||
|
throw error;
|
||||||
|
}, 'settings.selectDirectoryFailed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听useCustomDataPath的变化
|
|
||||||
watch(() => useCustomDataPath.value, (newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
initCustomPath();
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -163,20 +165,25 @@ watch(() => useCustomDataPath.value, (newVal) => {
|
|||||||
<SettingItem :title="t('settings.useCustomDataPath')">
|
<SettingItem :title="t('settings.useCustomDataPath')">
|
||||||
<ToggleSwitch v-model="useCustomDataPath" />
|
<ToggleSwitch v-model="useCustomDataPath" />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<div class="path-input-section" v-if="useCustomDataPath">
|
|
||||||
|
<!-- 路径显示区域 -->
|
||||||
|
<div class="path-section">
|
||||||
|
<div class="path-input-container">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="customPathInput"
|
:value="useCustomDataPath ? customPathDisplay : configStore.config.general.defaultDataPath"
|
||||||
@blur="updateCustomPath"
|
readonly
|
||||||
@keyup.enter="updateCustomPath"
|
:placeholder="useCustomDataPath ? t('settings.selectDataDirectory') : t('settings.defaultDataPath')"
|
||||||
:placeholder="t('settings.enterCustomPath')"
|
:class="[
|
||||||
class="path-input"
|
'path-display-input',
|
||||||
|
{ 'clickable': useCustomDataPath, 'disabled': !useCustomDataPath }
|
||||||
|
]"
|
||||||
|
@click="selectDirectory"
|
||||||
/>
|
/>
|
||||||
<div class="path-hint">{{ t('settings.pathHint') }}</div>
|
<div class="path-label">
|
||||||
|
{{ useCustomDataPath ? t('settings.customDataPath') : t('settings.defaultDataPath') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="default-path-info" v-else>
|
|
||||||
<div class="path-display default">{{ configStore.config.general.defaultDataPath }}</div>
|
|
||||||
<span class="path-label">{{ t('settings.defaultDataPath') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
@@ -305,12 +312,12 @@ watch(() => useCustomDataPath.value, (newVal) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-input-section {
|
.path-section {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding-left: 20px;
|
padding: 0 20px;
|
||||||
padding-right: 20px;
|
|
||||||
|
|
||||||
.path-input {
|
.path-input-container {
|
||||||
|
.path-display-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -322,53 +329,39 @@ watch(() => useCustomDataPath.value, (newVal) => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border-color: #444444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #4a9eff;
|
||||||
|
background-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #4a9eff;
|
border-color: #4a9eff;
|
||||||
background-color: #404040;
|
background-color: #404040;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #888888;
|
color: #888888;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-hint {
|
|
||||||
margin-top: 5px;
|
|
||||||
color: #888888;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-path-info {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
|
|
||||||
.path-display {
|
|
||||||
padding: 8px 12px;
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
border: 1px solid #444444;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #888888;
|
|
||||||
font-size: 13px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&.default {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-label {
|
.path-label {
|
||||||
display: block;
|
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #666666;
|
color: #888888;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-zone {
|
.danger-zone {
|
||||||
|
@@ -64,8 +64,6 @@ type EditingConfig struct {
|
|||||||
|
|
||||||
// 保存选项
|
// 保存选项
|
||||||
AutoSaveDelay int `json:"autoSaveDelay" yaml:"auto_save_delay" mapstructure:"auto_save_delay"` // 自动保存延迟(毫秒)
|
AutoSaveDelay int `json:"autoSaveDelay" yaml:"auto_save_delay" mapstructure:"auto_save_delay"` // 自动保存延迟(毫秒)
|
||||||
ChangeThreshold int `json:"changeThreshold" yaml:"change_threshold" mapstructure:"change_threshold"` // 变更字符阈值
|
|
||||||
MinSaveInterval int `json:"minSaveInterval" yaml:"min_save_interval" mapstructure:"min_save_interval"` // 最小保存间隔(毫秒)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppearanceConfig 外观设置配置
|
// AppearanceConfig 外观设置配置
|
||||||
@@ -137,8 +135,6 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
TabType: TabTypeSpaces,
|
TabType: TabTypeSpaces,
|
||||||
// 保存选项
|
// 保存选项
|
||||||
AutoSaveDelay: 5000, // 5秒后自动保存
|
AutoSaveDelay: 5000, // 5秒后自动保存
|
||||||
ChangeThreshold: 500, // 500个字符变更触发保存
|
|
||||||
MinSaveInterval: 1000, // 最小间隔1000毫秒
|
|
||||||
},
|
},
|
||||||
Appearance: AppearanceConfig{
|
Appearance: AppearanceConfig{
|
||||||
Language: LangZhCN,
|
Language: LangZhCN,
|
||||||
|
@@ -124,8 +124,6 @@ func setDefaults(v *viper.Viper) {
|
|||||||
v.SetDefault("editing.tab_size", defaultConfig.Editing.TabSize)
|
v.SetDefault("editing.tab_size", defaultConfig.Editing.TabSize)
|
||||||
v.SetDefault("editing.tab_type", defaultConfig.Editing.TabType)
|
v.SetDefault("editing.tab_type", defaultConfig.Editing.TabType)
|
||||||
v.SetDefault("editing.auto_save_delay", defaultConfig.Editing.AutoSaveDelay)
|
v.SetDefault("editing.auto_save_delay", defaultConfig.Editing.AutoSaveDelay)
|
||||||
v.SetDefault("editing.change_threshold", defaultConfig.Editing.ChangeThreshold)
|
|
||||||
v.SetDefault("editing.min_save_interval", defaultConfig.Editing.MinSaveInterval)
|
|
||||||
|
|
||||||
// 外观设置默认值
|
// 外观设置默认值
|
||||||
v.SetDefault("appearance.language", defaultConfig.Appearance.Language)
|
v.SetDefault("appearance.language", defaultConfig.Appearance.Language)
|
||||||
|
66
internal/services/dialog_service.go
Normal file
66
internal/services/dialog_service.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DialogService 对话框服务,处理文件选择等对话框操作
|
||||||
|
type DialogService struct {
|
||||||
|
logger *log.LoggerService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDialogService 创建新的对话框服务实例
|
||||||
|
func NewDialogService(logger *log.LoggerService) *DialogService {
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DialogService{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectDirectory 打开目录选择对话框
|
||||||
|
func (ds *DialogService) SelectDirectory() (string, error) {
|
||||||
|
dialog := application.OpenFileDialogWithOptions(&application.OpenFileDialogOptions{
|
||||||
|
// 目录选择配置
|
||||||
|
CanChooseDirectories: true, // 允许选择目录
|
||||||
|
CanChooseFiles: false, // 不允许选择文件
|
||||||
|
CanCreateDirectories: true, // 允许创建新目录
|
||||||
|
AllowsMultipleSelection: false, // 单选模式
|
||||||
|
|
||||||
|
// 显示配置
|
||||||
|
ShowHiddenFiles: false, // 不显示隐藏文件
|
||||||
|
HideExtension: false, // 不隐藏扩展名
|
||||||
|
CanSelectHiddenExtension: false, // 不允许选择隐藏扩展名
|
||||||
|
TreatsFilePackagesAsDirectories: false, // 不将文件包当作目录处理
|
||||||
|
AllowsOtherFileTypes: false, // 不允许其他文件类型
|
||||||
|
|
||||||
|
// 系统配置
|
||||||
|
ResolvesAliases: true, // 解析别名/快捷方式
|
||||||
|
|
||||||
|
// 对话框文本配置
|
||||||
|
Title: "选择数据存储目录",
|
||||||
|
Message: "请选择用于存储应用数据的文件夹",
|
||||||
|
ButtonText: "选择",
|
||||||
|
|
||||||
|
// 不设置过滤器,因为我们选择目录
|
||||||
|
Filters: nil,
|
||||||
|
|
||||||
|
// 不指定默认目录,让系统决定
|
||||||
|
Directory: "",
|
||||||
|
|
||||||
|
// 不绑定到特定窗口,使用默认行为
|
||||||
|
Window: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
path, err := dialog.PromptForSingleSelection()
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Failed to select directory", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.logger.Info("Directory selected", "path", path)
|
||||||
|
return path, nil
|
||||||
|
}
|
@@ -1,306 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
// Edit 表示编辑操作类型
|
|
||||||
type EditType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// EditInsert 插入操作
|
|
||||||
EditInsert EditType = iota
|
|
||||||
// EditDelete 删除操作
|
|
||||||
EditDelete
|
|
||||||
// EditEqual 相等部分
|
|
||||||
EditEqual
|
|
||||||
)
|
|
||||||
|
|
||||||
// Edit 表示单个编辑操作
|
|
||||||
type Edit struct {
|
|
||||||
Type EditType // 操作类型
|
|
||||||
Content string // 操作内容
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffResult 包含差异比较的结果信息
|
|
||||||
type DiffResult struct {
|
|
||||||
Edits []Edit // 编辑操作列表
|
|
||||||
InsertCount int // 插入的字符数
|
|
||||||
DeleteCount int // 删除的字符数
|
|
||||||
ChangedLines int // 变更的行数
|
|
||||||
TotalChanges int // 总变更字符数(插入+删除)
|
|
||||||
ChangedTokens int // 变更的token数(如单词、标识符等)
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculateChangesDetailed 使用Myers差分算法计算两个字符串之间的具体变更
|
|
||||||
func calculateChangesDetailed(oldText, newText string) DiffResult {
|
|
||||||
// 将文本分割成行
|
|
||||||
oldLines := splitLines(oldText)
|
|
||||||
newLines := splitLines(newText)
|
|
||||||
|
|
||||||
// 计算行级别的差异
|
|
||||||
edits := computeLineEdits(oldLines, newLines)
|
|
||||||
|
|
||||||
// 计算变更统计
|
|
||||||
result := DiffResult{
|
|
||||||
Edits: edits,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计变更
|
|
||||||
for _, edit := range edits {
|
|
||||||
switch edit.Type {
|
|
||||||
case EditInsert:
|
|
||||||
result.InsertCount += len(edit.Content)
|
|
||||||
result.ChangedLines++
|
|
||||||
case EditDelete:
|
|
||||||
result.DeleteCount += len(edit.Content)
|
|
||||||
result.ChangedLines++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.TotalChanges = result.InsertCount + result.DeleteCount
|
|
||||||
result.ChangedTokens = estimateChangedTokens(edits)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitLines 将文本分割成行
|
|
||||||
func splitLines(text string) []string {
|
|
||||||
var lines []string
|
|
||||||
var currentLine string
|
|
||||||
|
|
||||||
for _, char := range text {
|
|
||||||
if char == '\n' {
|
|
||||||
lines = append(lines, currentLine)
|
|
||||||
currentLine = ""
|
|
||||||
} else {
|
|
||||||
currentLine += string(char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加最后一行(如果不是以换行符结尾)
|
|
||||||
if currentLine != "" {
|
|
||||||
lines = append(lines, currentLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
// computeLineEdits 使用Myers差分算法计算行级别的差异
|
|
||||||
func computeLineEdits(oldLines, newLines []string) []Edit {
|
|
||||||
var edits []Edit
|
|
||||||
|
|
||||||
// 使用Myers差分算法计算行级别的差异
|
|
||||||
script := myersDiff(oldLines, newLines)
|
|
||||||
|
|
||||||
// 将差异脚本转换为编辑操作
|
|
||||||
for _, op := range script {
|
|
||||||
switch op.Type {
|
|
||||||
case EditEqual:
|
|
||||||
edits = append(edits, Edit{
|
|
||||||
Type: EditEqual,
|
|
||||||
Content: oldLines[op.OldStart],
|
|
||||||
})
|
|
||||||
case EditDelete:
|
|
||||||
edits = append(edits, Edit{
|
|
||||||
Type: EditDelete,
|
|
||||||
Content: oldLines[op.OldStart],
|
|
||||||
})
|
|
||||||
case EditInsert:
|
|
||||||
edits = append(edits, Edit{
|
|
||||||
Type: EditInsert,
|
|
||||||
Content: newLines[op.NewStart],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return edits
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffOp 表示差分操作
|
|
||||||
type DiffOp struct {
|
|
||||||
Type EditType
|
|
||||||
OldStart int
|
|
||||||
OldEnd int
|
|
||||||
NewStart int
|
|
||||||
NewEnd int
|
|
||||||
}
|
|
||||||
|
|
||||||
// myersDiff 实现Myers差分算法
|
|
||||||
func myersDiff(oldLines, newLines []string) []DiffOp {
|
|
||||||
// 基本思路:Myers差分算法通过建立编辑图来寻找最短编辑路径
|
|
||||||
// 简化版实现
|
|
||||||
var script []DiffOp
|
|
||||||
|
|
||||||
oldLen := len(oldLines)
|
|
||||||
newLen := len(newLines)
|
|
||||||
|
|
||||||
// 使用动态规划找出最长公共子序列(LCS)
|
|
||||||
lcs := longestCommonSubsequence(oldLines, newLines)
|
|
||||||
|
|
||||||
// 根据LCS构建差分脚本
|
|
||||||
oldIndex, newIndex := 0, 0
|
|
||||||
for _, entry := range lcs {
|
|
||||||
// 处理LCS之前的差异
|
|
||||||
for oldIndex < entry.OldIndex {
|
|
||||||
script = append(script, DiffOp{
|
|
||||||
Type: EditDelete,
|
|
||||||
OldStart: oldIndex,
|
|
||||||
OldEnd: oldIndex + 1,
|
|
||||||
NewStart: newIndex,
|
|
||||||
NewEnd: newIndex,
|
|
||||||
})
|
|
||||||
oldIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
for newIndex < entry.NewIndex {
|
|
||||||
script = append(script, DiffOp{
|
|
||||||
Type: EditInsert,
|
|
||||||
OldStart: oldIndex,
|
|
||||||
OldEnd: oldIndex,
|
|
||||||
NewStart: newIndex,
|
|
||||||
NewEnd: newIndex + 1,
|
|
||||||
})
|
|
||||||
newIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理相等部分
|
|
||||||
script = append(script, DiffOp{
|
|
||||||
Type: EditEqual,
|
|
||||||
OldStart: oldIndex,
|
|
||||||
OldEnd: oldIndex + 1,
|
|
||||||
NewStart: newIndex,
|
|
||||||
NewEnd: newIndex + 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
oldIndex++
|
|
||||||
newIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理剩余差异
|
|
||||||
for oldIndex < oldLen {
|
|
||||||
script = append(script, DiffOp{
|
|
||||||
Type: EditDelete,
|
|
||||||
OldStart: oldIndex,
|
|
||||||
OldEnd: oldIndex + 1,
|
|
||||||
NewStart: newIndex,
|
|
||||||
NewEnd: newIndex,
|
|
||||||
})
|
|
||||||
oldIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
for newIndex < newLen {
|
|
||||||
script = append(script, DiffOp{
|
|
||||||
Type: EditInsert,
|
|
||||||
OldStart: oldIndex,
|
|
||||||
OldEnd: oldIndex,
|
|
||||||
NewStart: newIndex,
|
|
||||||
NewEnd: newIndex + 1,
|
|
||||||
})
|
|
||||||
newIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
return script
|
|
||||||
}
|
|
||||||
|
|
||||||
// LCSEntry 表示最长公共子序列中的一个条目
|
|
||||||
type LCSEntry struct {
|
|
||||||
OldIndex int
|
|
||||||
NewIndex int
|
|
||||||
}
|
|
||||||
|
|
||||||
// longestCommonSubsequence 寻找两个字符串数组的最长公共子序列
|
|
||||||
func longestCommonSubsequence(oldLines, newLines []string) []LCSEntry {
|
|
||||||
oldLen := len(oldLines)
|
|
||||||
newLen := len(newLines)
|
|
||||||
|
|
||||||
// 创建动态规划表
|
|
||||||
dp := make([][]int, oldLen+1)
|
|
||||||
for i := range dp {
|
|
||||||
dp[i] = make([]int, newLen+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 填充DP表
|
|
||||||
for i := 1; i <= oldLen; i++ {
|
|
||||||
for j := 1; j <= newLen; j++ {
|
|
||||||
if oldLines[i-1] == newLines[j-1] {
|
|
||||||
dp[i][j] = dp[i-1][j-1] + 1
|
|
||||||
} else {
|
|
||||||
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回溯找出LCS
|
|
||||||
var lcs []LCSEntry
|
|
||||||
i, j := oldLen, newLen
|
|
||||||
for i > 0 && j > 0 {
|
|
||||||
if oldLines[i-1] == newLines[j-1] {
|
|
||||||
lcs = append([]LCSEntry{{OldIndex: i - 1, NewIndex: j - 1}}, lcs...)
|
|
||||||
i--
|
|
||||||
j--
|
|
||||||
} else if dp[i-1][j] > dp[i][j-1] {
|
|
||||||
i--
|
|
||||||
} else {
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lcs
|
|
||||||
}
|
|
||||||
|
|
||||||
// max 返回两个整数中的较大值
|
|
||||||
func max(a, b int) int {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// estimateChangedTokens 估计变更的token数量
|
|
||||||
// 这里使用简单的单词分割来估计
|
|
||||||
func estimateChangedTokens(edits []Edit) int {
|
|
||||||
tokenCount := 0
|
|
||||||
|
|
||||||
for _, edit := range edits {
|
|
||||||
switch edit.Type {
|
|
||||||
case EditInsert, EditDelete:
|
|
||||||
// 简单地将内容按空白字符分割成单词
|
|
||||||
words := splitIntoWords(edit.Content)
|
|
||||||
tokenCount += len(words)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenCount
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitIntoWords 将文本分割成单词
|
|
||||||
func splitIntoWords(text string) []string {
|
|
||||||
var words []string
|
|
||||||
var currentWord string
|
|
||||||
|
|
||||||
// 简单的状态机:
|
|
||||||
// - 如果是字母、数字或下划线,添加到当前单词
|
|
||||||
// - 否则,结束当前单词并开始新单词
|
|
||||||
for _, char := range text {
|
|
||||||
if isWordChar(char) {
|
|
||||||
currentWord += string(char)
|
|
||||||
} else {
|
|
||||||
if currentWord != "" {
|
|
||||||
words = append(words, currentWord)
|
|
||||||
currentWord = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加最后一个单词(如果有)
|
|
||||||
if currentWord != "" {
|
|
||||||
words = append(words, currentWord)
|
|
||||||
}
|
|
||||||
|
|
||||||
return words
|
|
||||||
}
|
|
||||||
|
|
||||||
// isWordChar 判断字符是否是单词字符(字母、数字或下划线)
|
|
||||||
func isWordChar(char rune) bool {
|
|
||||||
return (char >= 'a' && char <= 'z') ||
|
|
||||||
(char >= 'A' && char <= 'Z') ||
|
|
||||||
(char >= '0' && char <= '9') ||
|
|
||||||
char == '_'
|
|
||||||
}
|
|
@@ -11,20 +11,6 @@ import (
|
|||||||
"voidraft/internal/models"
|
"voidraft/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SaveTrigger 保存触发器类型
|
|
||||||
type SaveTrigger int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// SaveTriggerAuto 自动保存
|
|
||||||
SaveTriggerAuto SaveTrigger = iota
|
|
||||||
// SaveTriggerManual 手动触发保存
|
|
||||||
SaveTriggerManual
|
|
||||||
// SaveTriggerThreshold 超过阈值触发保存
|
|
||||||
SaveTriggerThreshold
|
|
||||||
// SaveTriggerShutdown 程序关闭触发保存
|
|
||||||
SaveTriggerShutdown
|
|
||||||
)
|
|
||||||
|
|
||||||
// DocumentError 文档操作错误
|
// DocumentError 文档操作错误
|
||||||
type DocumentError struct {
|
type DocumentError struct {
|
||||||
Operation string // 操作名称
|
Operation string // 操作名称
|
||||||
@@ -45,18 +31,18 @@ func (e *DocumentError) Unwrap() error {
|
|||||||
type DocumentService struct {
|
type DocumentService struct {
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
logger *log.LoggerService
|
logger *log.LoggerService
|
||||||
activeDoc *models.Document
|
document *models.Document // 文档实例
|
||||||
docStore *Store[models.Document]
|
docStore *Store[models.Document]
|
||||||
memoryCache *models.Document // 内存缓存,小改动只更新此缓存
|
mutex sync.RWMutex // 保护document的读写
|
||||||
lock sync.RWMutex
|
|
||||||
lastSaveTime time.Time
|
// 定时保存
|
||||||
changeCounter int // 变更计数器,记录自上次保存后的变更数量
|
saveTimer *time.Timer
|
||||||
saveTimer *time.Timer // 自动保存定时器
|
timerMutex sync.Mutex // 保护定时器操作
|
||||||
pendingSave bool // 是否有等待保存的更改
|
isDirty bool // 文档是否有未保存的变更
|
||||||
saveChannel chan SaveTrigger // 保存通道,用于接收保存触发信号
|
|
||||||
shutdownChan chan struct{} // 关闭通道,用于程序退出时通知保存协程
|
// 服务控制
|
||||||
shutdownWg sync.WaitGroup // 等待组,用于确保保存协程正常退出
|
shutdown chan struct{}
|
||||||
onSaveCallback func(trigger SaveTrigger) // 保存回调函数
|
shutdownOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDocumentService 创建新的文档服务实例
|
// NewDocumentService 创建新的文档服务实例
|
||||||
@@ -68,9 +54,7 @@ func NewDocumentService(configService *ConfigService, logger *log.LoggerService)
|
|||||||
service := &DocumentService{
|
service := &DocumentService{
|
||||||
configService: configService,
|
configService: configService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
saveChannel: make(chan SaveTrigger, 10),
|
shutdown: make(chan struct{}),
|
||||||
shutdownChan: make(chan struct{}),
|
|
||||||
lastSaveTime: time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return service
|
return service
|
||||||
@@ -79,203 +63,101 @@ func NewDocumentService(configService *ConfigService, logger *log.LoggerService)
|
|||||||
// Initialize 初始化文档服务
|
// Initialize 初始化文档服务
|
||||||
func (ds *DocumentService) Initialize() error {
|
func (ds *DocumentService) Initialize() error {
|
||||||
// 确保文档目录存在
|
// 确保文档目录存在
|
||||||
err := ds.ensureDocumentsDir()
|
if err := ds.ensureDocumentsDir(); err != nil {
|
||||||
if err != nil {
|
|
||||||
ds.logger.Error("Document: Failed to ensure documents directory", "error", err)
|
ds.logger.Error("Document: Failed to ensure documents directory", "error", err)
|
||||||
return &DocumentError{Operation: "initialize", Err: err}
|
return &DocumentError{Operation: "initialize", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化文档存储
|
// 初始化文档存储
|
||||||
err = ds.initDocumentStore()
|
if err := ds.initDocumentStore(); err != nil {
|
||||||
if err != nil {
|
|
||||||
ds.logger.Error("Document: Failed to initialize document store", "error", err)
|
ds.logger.Error("Document: Failed to initialize document store", "error", err)
|
||||||
return &DocumentError{Operation: "init_store", Err: err}
|
return &DocumentError{Operation: "init_store", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载默认文档
|
// 加载默认文档
|
||||||
err = ds.LoadDefaultDocument()
|
if err := ds.loadDefaultDocument(); err != nil {
|
||||||
if err != nil {
|
|
||||||
ds.logger.Error("Document: Failed to load default document", "error", err)
|
ds.logger.Error("Document: Failed to load default document", "error", err)
|
||||||
return &DocumentError{Operation: "load_default", Err: err}
|
return &DocumentError{Operation: "load_default", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动保存处理协程
|
// 启动定时保存
|
||||||
ds.startSaveProcessor()
|
ds.startAutoSave()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// startSaveProcessor 启动保存处理协程
|
// startAutoSave 启动定时保存
|
||||||
func (ds *DocumentService) startSaveProcessor() {
|
func (ds *DocumentService) startAutoSave() {
|
||||||
ds.shutdownWg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer ds.shutdownWg.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case trigger := <-ds.saveChannel:
|
|
||||||
// 接收到保存信号,执行保存
|
|
||||||
ds.saveToStore(trigger)
|
|
||||||
case <-ds.shutdownChan:
|
|
||||||
// 接收到关闭信号,保存并退出
|
|
||||||
if ds.pendingSave {
|
|
||||||
ds.saveToStore(SaveTriggerShutdown)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// scheduleAutoSave 安排自动保存
|
|
||||||
func (ds *DocumentService) scheduleAutoSave() {
|
|
||||||
// 获取配置
|
|
||||||
config, err := ds.configService.GetConfig()
|
config, err := ds.configService.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ds.logger.Error("Document: Failed to get config for auto save", "error", err)
|
ds.logger.Error("Document: Failed to get config for auto save", "error", err)
|
||||||
// 使用默认值2秒
|
ds.scheduleNextSave(5 * time.Second) // 默认5秒
|
||||||
ds.scheduleTimerWithDelay(2000)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查配置有效性
|
delay := time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond
|
||||||
if config == nil {
|
ds.scheduleNextSave(delay)
|
||||||
ds.logger.Error("Document: Config is nil, using default delay")
|
}
|
||||||
ds.scheduleTimerWithDelay(2000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打印保存设置,便于调试
|
// scheduleNextSave 安排下次保存
|
||||||
ds.logger.Debug("Document: Auto save settings",
|
func (ds *DocumentService) scheduleNextSave(delay time.Duration) {
|
||||||
"autoSaveDelay", config.Editing.AutoSaveDelay,
|
ds.timerMutex.Lock()
|
||||||
"changeThreshold", config.Editing.ChangeThreshold,
|
defer ds.timerMutex.Unlock()
|
||||||
"minSaveInterval", config.Editing.MinSaveInterval)
|
|
||||||
|
|
||||||
ds.lock.Lock()
|
// 停止现有定时器
|
||||||
defer ds.lock.Unlock()
|
|
||||||
|
|
||||||
// 重置自动保存定时器
|
|
||||||
if ds.saveTimer != nil {
|
if ds.saveTimer != nil {
|
||||||
ds.saveTimer.Stop()
|
ds.saveTimer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新的自动保存定时器
|
// 创建新的定时器
|
||||||
autoSaveDelay := config.Editing.AutoSaveDelay
|
ds.saveTimer = time.AfterFunc(delay, func() {
|
||||||
ds.logger.Debug("Document: Scheduling auto save", "delay", autoSaveDelay)
|
ds.performAutoSave()
|
||||||
ds.scheduleTimerWithDelay(autoSaveDelay)
|
// 安排下次保存
|
||||||
}
|
ds.startAutoSave()
|
||||||
|
|
||||||
// scheduleTimerWithDelay 使用指定延迟创建定时器
|
|
||||||
func (ds *DocumentService) scheduleTimerWithDelay(delayMs int) {
|
|
||||||
ds.saveTimer = time.AfterFunc(time.Duration(delayMs)*time.Millisecond, func() {
|
|
||||||
// 只有在有待保存的更改时才触发保存
|
|
||||||
if ds.pendingSave {
|
|
||||||
ds.saveChannel <- SaveTriggerAuto
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveToStore 保存文档到存储
|
// performAutoSave 执行自动保存
|
||||||
func (ds *DocumentService) saveToStore(trigger SaveTrigger) {
|
func (ds *DocumentService) performAutoSave() {
|
||||||
ds.lock.Lock()
|
ds.mutex.Lock()
|
||||||
defer ds.lock.Unlock()
|
defer ds.mutex.Unlock()
|
||||||
|
|
||||||
// 如果没有内存缓存或活动文档,直接返回
|
// 如果没有变更,跳过保存
|
||||||
if ds.memoryCache == nil || ds.activeDoc == nil {
|
if !ds.isDirty || ds.document == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取配置
|
// 更新元数据并保存
|
||||||
config, err := ds.configService.GetConfig()
|
ds.document.Meta.LastUpdated = time.Now()
|
||||||
if err != nil {
|
|
||||||
ds.logger.Error("Document: Failed to get config for save", "error", err)
|
|
||||||
// 继续使用默认值
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认值
|
ds.logger.Info("Document: Auto saving document",
|
||||||
minInterval := 500 // 默认500毫秒
|
"id", ds.document.Meta.ID,
|
||||||
|
"contentLength", len(ds.document.Content))
|
||||||
|
|
||||||
// 如果成功获取了配置,使用配置值
|
if err := ds.docStore.Set(*ds.document); err != nil {
|
||||||
if err == nil && config != nil {
|
ds.logger.Error("Document: Failed to auto save document", "error", err)
|
||||||
minInterval = config.Editing.MinSaveInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是自动保存,检查最小保存间隔
|
|
||||||
if trigger == SaveTriggerAuto {
|
|
||||||
now := time.Now()
|
|
||||||
elapsed := now.Sub(ds.lastSaveTime).Milliseconds()
|
|
||||||
|
|
||||||
// 如果距离上次保存时间太短,重新安排保存
|
|
||||||
if elapsed < int64(minInterval) {
|
|
||||||
// 重新安排保存,延迟 = 最小间隔 - 已经过的时间
|
|
||||||
delayMs := minInterval - int(elapsed)
|
|
||||||
ds.logger.Debug("Document: Rescheduling save due to min interval",
|
|
||||||
"minInterval", minInterval,
|
|
||||||
"elapsed", elapsed,
|
|
||||||
"delayMs", delayMs)
|
|
||||||
|
|
||||||
ds.lock.Unlock() // 解锁后再启动定时器,避免死锁
|
|
||||||
ds.scheduleTimerWithDelay(delayMs)
|
|
||||||
ds.lock.Lock() // 恢复锁
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新活动文档
|
|
||||||
ds.activeDoc = ds.memoryCache
|
|
||||||
ds.activeDoc.Meta.LastUpdated = time.Now()
|
|
||||||
|
|
||||||
// 保存到存储
|
|
||||||
ds.logger.Info("Document: Saving document to disk",
|
|
||||||
"trigger", trigger,
|
|
||||||
"id", ds.activeDoc.Meta.ID,
|
|
||||||
"contentLength", len(ds.activeDoc.Content))
|
|
||||||
|
|
||||||
err = ds.docStore.Set(*ds.activeDoc)
|
|
||||||
if err != nil {
|
|
||||||
ds.logger.Error("Document: Failed to save document", "trigger", trigger, "error", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制确保保存到磁盘
|
if err := ds.docStore.Save(); err != nil {
|
||||||
err = ds.docStore.Save()
|
ds.logger.Error("Document: Failed to force save document", "error", err)
|
||||||
if err != nil {
|
|
||||||
ds.logger.Error("Document: Failed to force save document", "trigger", trigger, "error", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置计数器和状态
|
// 重置脏标记
|
||||||
ds.changeCounter = 0
|
ds.isDirty = false
|
||||||
ds.pendingSave = false
|
ds.logger.Info("Document: Auto save completed")
|
||||||
ds.lastSaveTime = time.Now()
|
|
||||||
|
|
||||||
// 触发回调
|
|
||||||
if ds.onSaveCallback != nil {
|
|
||||||
ds.onSaveCallback(trigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.logger.Info("Document: Saved document", "trigger", trigger, "id", ds.activeDoc.Meta.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown 关闭文档服务,确保所有数据保存
|
// stopTimer 停止定时器
|
||||||
func (ds *DocumentService) Shutdown() {
|
func (ds *DocumentService) stopTimer() {
|
||||||
// 发送关闭信号
|
ds.timerMutex.Lock()
|
||||||
close(ds.shutdownChan)
|
defer ds.timerMutex.Unlock()
|
||||||
|
|
||||||
// 等待保存协程退出
|
|
||||||
ds.shutdownWg.Wait()
|
|
||||||
|
|
||||||
// 停止定时器
|
|
||||||
if ds.saveTimer != nil {
|
if ds.saveTimer != nil {
|
||||||
ds.saveTimer.Stop()
|
ds.saveTimer.Stop()
|
||||||
|
ds.saveTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.logger.Info("Document: Service shutdown completed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSaveCallback 设置保存回调函数
|
|
||||||
func (ds *DocumentService) SetSaveCallback(callback func(trigger SaveTrigger)) {
|
|
||||||
ds.onSaveCallback = callback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initDocumentStore 初始化文档存储
|
// initDocumentStore 初始化文档存储
|
||||||
@@ -287,10 +169,9 @@ func (ds *DocumentService) initDocumentStore() error {
|
|||||||
|
|
||||||
ds.logger.Info("Document: Initializing document store", "path", docPath)
|
ds.logger.Info("Document: Initializing document store", "path", docPath)
|
||||||
|
|
||||||
// 创建文档存储,强制保存和Service触发的保存都使用同步保存到磁盘
|
|
||||||
ds.docStore = NewStore[models.Document](StoreOption{
|
ds.docStore = NewStore[models.Document](StoreOption{
|
||||||
FilePath: docPath,
|
FilePath: docPath,
|
||||||
AutoSave: true, // 启用自动保存,确保Set操作直接写入磁盘
|
AutoSave: true,
|
||||||
Logger: ds.logger,
|
Logger: ds.logger,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -304,7 +185,6 @@ func (ds *DocumentService) ensureDocumentsDir() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取实际的数据路径(如果启用自定义路径则使用自定义路径,否则使用默认路径)
|
|
||||||
var dataPath string
|
var dataPath string
|
||||||
if config.General.UseCustomDataPath && config.General.CustomDataPath != "" {
|
if config.General.UseCustomDataPath && config.General.CustomDataPath != "" {
|
||||||
dataPath = config.General.CustomDataPath
|
dataPath = config.General.CustomDataPath
|
||||||
@@ -312,14 +192,8 @@ func (ds *DocumentService) ensureDocumentsDir() error {
|
|||||||
dataPath = config.General.DefaultDataPath
|
dataPath = config.General.DefaultDataPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建文档目录
|
|
||||||
docsDir := filepath.Join(dataPath, "docs")
|
docsDir := filepath.Join(dataPath, "docs")
|
||||||
err = os.MkdirAll(docsDir, 0755)
|
return os.MkdirAll(docsDir, 0755)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDocumentsDir 获取文档目录路径
|
// getDocumentsDir 获取文档目录路径
|
||||||
@@ -329,7 +203,6 @@ func (ds *DocumentService) getDocumentsDir() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取实际的数据路径(如果启用自定义路径则使用自定义路径,否则使用默认路径)
|
|
||||||
var dataPath string
|
var dataPath string
|
||||||
if config.General.UseCustomDataPath && config.General.CustomDataPath != "" {
|
if config.General.UseCustomDataPath && config.General.CustomDataPath != "" {
|
||||||
dataPath = config.General.CustomDataPath
|
dataPath = config.General.CustomDataPath
|
||||||
@@ -349,153 +222,99 @@ func (ds *DocumentService) getDefaultDocumentPath() (string, error) {
|
|||||||
return filepath.Join(docsDir, "default.json"), nil
|
return filepath.Join(docsDir, "default.json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDefaultDocument 加载默认文档
|
// loadDefaultDocument 加载默认文档
|
||||||
func (ds *DocumentService) LoadDefaultDocument() error {
|
func (ds *DocumentService) loadDefaultDocument() error {
|
||||||
// 从Store加载文档
|
|
||||||
doc := ds.docStore.Get()
|
doc := ds.docStore.Get()
|
||||||
|
|
||||||
// 检查文档是否有效
|
ds.mutex.Lock()
|
||||||
|
defer ds.mutex.Unlock()
|
||||||
|
|
||||||
if doc.Meta.ID == "" {
|
if doc.Meta.ID == "" {
|
||||||
// 创建默认文档
|
// 创建默认文档
|
||||||
defaultDoc := models.NewDefaultDocument()
|
ds.document = models.NewDefaultDocument()
|
||||||
ds.lock.Lock()
|
|
||||||
ds.activeDoc = defaultDoc
|
|
||||||
ds.memoryCache = defaultDoc // 同时更新内存缓存
|
|
||||||
ds.lock.Unlock()
|
|
||||||
|
|
||||||
// 保存默认文档
|
// 保存默认文档
|
||||||
err := ds.docStore.Set(*defaultDoc)
|
if err := ds.docStore.Set(*ds.document); err != nil {
|
||||||
if err != nil {
|
|
||||||
return &DocumentError{Operation: "save_default", Err: err}
|
return &DocumentError{Operation: "save_default", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.logger.Info("Document: Created and saved default document")
|
ds.logger.Info("Document: Created and saved default document")
|
||||||
return nil
|
} else {
|
||||||
|
ds.document = &doc
|
||||||
|
ds.logger.Info("Document: Loaded default document", "id", doc.Meta.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置为活动文档
|
|
||||||
ds.lock.Lock()
|
|
||||||
ds.activeDoc = &doc
|
|
||||||
ds.memoryCache = &doc // 同时更新内存缓存
|
|
||||||
ds.lock.Unlock()
|
|
||||||
|
|
||||||
ds.logger.Info("Document: Loaded default document", "id", doc.Meta.ID)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveDocument 获取当前活动文档
|
// GetActiveDocument 获取当前活动文档
|
||||||
func (ds *DocumentService) GetActiveDocument() (*models.Document, error) {
|
func (ds *DocumentService) GetActiveDocument() (*models.Document, error) {
|
||||||
ds.lock.RLock()
|
ds.mutex.RLock()
|
||||||
defer ds.lock.RUnlock()
|
defer ds.mutex.RUnlock()
|
||||||
|
|
||||||
if ds.memoryCache == nil {
|
if ds.document == nil {
|
||||||
return nil, errors.New("no active document loaded")
|
return nil, errors.New("no active document loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回内存缓存中的文档,确保获得最新版本
|
// 返回副本以防止外部修改
|
||||||
return ds.memoryCache, nil
|
docCopy := *ds.document
|
||||||
|
return &docCopy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveDocumentContent 获取当前活动文档内容
|
// GetActiveDocumentContent 获取当前活动文档内容
|
||||||
func (ds *DocumentService) GetActiveDocumentContent() (string, error) {
|
func (ds *DocumentService) GetActiveDocumentContent() (string, error) {
|
||||||
ds.lock.RLock()
|
ds.mutex.RLock()
|
||||||
defer ds.lock.RUnlock()
|
defer ds.mutex.RUnlock()
|
||||||
|
|
||||||
if ds.memoryCache == nil {
|
if ds.document == nil {
|
||||||
return "", errors.New("no active document loaded")
|
return "", errors.New("no active document loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ds.memoryCache.Content, nil
|
return ds.document.Content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateActiveDocumentContent 更新当前活动文档内容
|
// UpdateActiveDocumentContent 更新当前活动文档内容
|
||||||
func (ds *DocumentService) UpdateActiveDocumentContent(content string) error {
|
func (ds *DocumentService) UpdateActiveDocumentContent(content string) error {
|
||||||
// 获取配置
|
ds.mutex.Lock()
|
||||||
config, err := ds.configService.GetConfig()
|
defer ds.mutex.Unlock()
|
||||||
if err != nil {
|
|
||||||
ds.logger.Error("Document: Failed to get config for content update", "error", err)
|
|
||||||
// 出错时仍继续,使用默认行为
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认配置值
|
if ds.document == nil {
|
||||||
threshold := 100 // 默认值
|
|
||||||
|
|
||||||
// 如果成功获取了配置,使用配置值
|
|
||||||
if err == nil && config != nil {
|
|
||||||
threshold = config.Editing.ChangeThreshold
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.lock.Lock()
|
|
||||||
|
|
||||||
if ds.memoryCache == nil {
|
|
||||||
ds.lock.Unlock()
|
|
||||||
return errors.New("no active document loaded")
|
return errors.New("no active document loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算变更数量
|
// 更新文档内容并标记为脏
|
||||||
oldContent := ds.memoryCache.Content
|
ds.document.Content = content
|
||||||
changedChars := calculateChanges(oldContent, content)
|
ds.document.Meta.LastUpdated = time.Now()
|
||||||
ds.changeCounter += changedChars
|
ds.isDirty = true
|
||||||
|
|
||||||
// 调试信息
|
|
||||||
ds.logger.Debug("Document: Content updated",
|
|
||||||
"changedChars", changedChars,
|
|
||||||
"totalChanges", ds.changeCounter,
|
|
||||||
"threshold", threshold)
|
|
||||||
|
|
||||||
// 更新内存缓存
|
|
||||||
ds.memoryCache.Content = content
|
|
||||||
ds.memoryCache.Meta.LastUpdated = time.Now()
|
|
||||||
ds.pendingSave = true
|
|
||||||
|
|
||||||
// 如果变更超过阈值,触发保存
|
|
||||||
if ds.changeCounter >= threshold {
|
|
||||||
ds.logger.Info("Document: Change threshold reached, triggering save",
|
|
||||||
"threshold", threshold,
|
|
||||||
"changes", ds.changeCounter)
|
|
||||||
|
|
||||||
// 提前解锁,避免死锁
|
|
||||||
ds.lock.Unlock()
|
|
||||||
ds.saveChannel <- SaveTriggerThreshold
|
|
||||||
} else {
|
|
||||||
// 否则安排自动保存
|
|
||||||
ds.lock.Unlock()
|
|
||||||
ds.scheduleAutoSave()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveDocumentSync 同步保存文档内容 (用于页面关闭前同步保存)
|
// SaveDocumentSync 同步保存文档内容
|
||||||
func (ds *DocumentService) SaveDocumentSync(content string) error {
|
func (ds *DocumentService) SaveDocumentSync(content string) error {
|
||||||
ds.lock.Lock()
|
ds.mutex.Lock()
|
||||||
|
defer ds.mutex.Unlock()
|
||||||
|
|
||||||
if ds.memoryCache == nil {
|
if ds.document == nil {
|
||||||
ds.lock.Unlock()
|
|
||||||
return errors.New("no active document loaded")
|
return errors.New("no active document loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新内存缓存
|
// 更新内容
|
||||||
ds.memoryCache.Content = content
|
ds.document.Content = content
|
||||||
ds.memoryCache.Meta.LastUpdated = time.Now()
|
ds.document.Meta.LastUpdated = time.Now()
|
||||||
|
|
||||||
// 直接保存到存储
|
// 立即保存
|
||||||
doc := *ds.memoryCache
|
if err := ds.docStore.Set(*ds.document); err != nil {
|
||||||
ds.lock.Unlock()
|
|
||||||
|
|
||||||
err := ds.docStore.Set(doc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置状态
|
if err := ds.docStore.Save(); err != nil {
|
||||||
ds.lock.Lock()
|
return err
|
||||||
ds.pendingSave = false
|
}
|
||||||
ds.changeCounter = 0
|
|
||||||
ds.lastSaveTime = time.Now()
|
|
||||||
ds.lock.Unlock()
|
|
||||||
|
|
||||||
ds.logger.Info("Document: Synced document save completed")
|
// 重置脏标记
|
||||||
|
ds.isDirty = false
|
||||||
|
ds.logger.Info("Document: Sync save completed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,105 +322,47 @@ func (ds *DocumentService) SaveDocumentSync(content string) error {
|
|||||||
func (ds *DocumentService) ForceSave() error {
|
func (ds *DocumentService) ForceSave() error {
|
||||||
ds.logger.Info("Document: Force save triggered")
|
ds.logger.Info("Document: Force save triggered")
|
||||||
|
|
||||||
// 获取当前文档内容
|
ds.mutex.RLock()
|
||||||
ds.lock.RLock()
|
if ds.document == nil {
|
||||||
if ds.memoryCache == nil {
|
ds.mutex.RUnlock()
|
||||||
ds.lock.RUnlock()
|
|
||||||
return errors.New("no active document loaded")
|
return errors.New("no active document loaded")
|
||||||
}
|
}
|
||||||
content := ds.memoryCache.Content
|
content := ds.document.Content
|
||||||
ds.lock.RUnlock()
|
ds.mutex.RUnlock()
|
||||||
|
|
||||||
// 使用同步方法直接保存到磁盘
|
return ds.SaveDocumentSync(content)
|
||||||
if err := ds.SaveDocumentSync(content); err != nil {
|
|
||||||
ds.logger.Error("Document: Force save failed", "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.logger.Info("Document: Force save completed successfully")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateChanges 计算两个字符串之间的变更数量
|
// ServiceShutdown 服务关闭
|
||||||
func calculateChanges(old, new string) int {
|
|
||||||
// 使用详细的差分算法计算变更
|
|
||||||
result := calculateChangesDetailed(old, new)
|
|
||||||
|
|
||||||
// 返回总变更字符数
|
|
||||||
return result.TotalChanges
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDiffInfo 获取两个文本之间的详细差异信息
|
|
||||||
func (ds *DocumentService) GetDiffInfo(oldText, newText string) DiffResult {
|
|
||||||
return calculateChangesDetailed(oldText, newText)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSaveSettings 获取文档保存设置
|
|
||||||
func (ds *DocumentService) GetSaveSettings() (*models.EditingConfig, error) {
|
|
||||||
config, err := ds.configService.GetConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, &DocumentError{Operation: "get_save_settings", Err: err}
|
|
||||||
}
|
|
||||||
return &config.Editing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateSaveSettings 更新文档保存设置
|
|
||||||
func (ds *DocumentService) UpdateSaveSettings(docConfig models.EditingConfig) error {
|
|
||||||
// 使用配置服务的 Set 方法更新文档配置
|
|
||||||
if err := ds.configService.Set("editing.auto_save_delay", docConfig.AutoSaveDelay); err != nil {
|
|
||||||
return &DocumentError{Operation: "update_save_settings_auto_save_delay", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ds.configService.Set("editing.change_threshold", docConfig.ChangeThreshold); err != nil {
|
|
||||||
return &DocumentError{Operation: "update_save_settings_change_threshold", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ds.configService.Set("editing.min_save_interval", docConfig.MinSaveInterval); err != nil {
|
|
||||||
return &DocumentError{Operation: "update_save_settings_min_save_interval", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 安排自动保存
|
|
||||||
ds.scheduleAutoSave()
|
|
||||||
|
|
||||||
ds.logger.Info("Document: Updated save settings")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceShutdown 实现应用程序关闭时的服务关闭逻辑
|
|
||||||
func (ds *DocumentService) ServiceShutdown() error {
|
func (ds *DocumentService) ServiceShutdown() error {
|
||||||
ds.logger.Info("Document: Service is shutting down, saving document...")
|
ds.logger.Info("Document: Service is shutting down...")
|
||||||
|
|
||||||
// 获取当前活动文档
|
// 确保只执行一次关闭
|
||||||
ds.lock.RLock()
|
var shutdownErr error
|
||||||
if ds.memoryCache == nil {
|
ds.shutdownOnce.Do(func() {
|
||||||
ds.lock.RUnlock()
|
// 停止定时器
|
||||||
ds.logger.Info("Document: No active document to save on shutdown")
|
ds.stopTimer()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取要保存的内容
|
// 执行最后一次保存
|
||||||
content := ds.memoryCache.Content
|
ds.mutex.RLock()
|
||||||
ds.lock.RUnlock()
|
if ds.document != nil && ds.isDirty {
|
||||||
|
content := ds.document.Content
|
||||||
// 同步保存文档内容
|
ds.mutex.RUnlock()
|
||||||
err := ds.SaveDocumentSync(content)
|
|
||||||
if err != nil {
|
|
||||||
ds.logger.Error("Document: Failed to save document on shutdown", "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if err := ds.SaveDocumentSync(content); err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to save on shutdown", "error", err)
|
||||||
|
shutdownErr = err
|
||||||
|
} else {
|
||||||
ds.logger.Info("Document: Document saved successfully on shutdown")
|
ds.logger.Info("Document: Document saved successfully on shutdown")
|
||||||
|
}
|
||||||
// 关闭通道以通知保存协程退出
|
} else {
|
||||||
close(ds.shutdownChan)
|
ds.mutex.RUnlock()
|
||||||
|
|
||||||
// 等待保存协程退出
|
|
||||||
ds.shutdownWg.Wait()
|
|
||||||
|
|
||||||
// 停止所有计时器
|
|
||||||
if ds.saveTimer != nil {
|
|
||||||
ds.saveTimer.Stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// 关闭服务
|
||||||
|
close(ds.shutdown)
|
||||||
|
ds.logger.Info("Document: Service shutdown completed")
|
||||||
|
})
|
||||||
|
|
||||||
|
return shutdownErr
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ type ServiceManager struct {
|
|||||||
documentService *DocumentService
|
documentService *DocumentService
|
||||||
systemService *SystemService
|
systemService *SystemService
|
||||||
hotkeyService *HotkeyService
|
hotkeyService *HotkeyService
|
||||||
|
dialogService *DialogService
|
||||||
logger *log.LoggerService
|
logger *log.LoggerService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ func NewServiceManager() *ServiceManager {
|
|||||||
// 初始化热键服务
|
// 初始化热键服务
|
||||||
hotkeyService := NewHotkeyService(configService, logger)
|
hotkeyService := NewHotkeyService(configService, logger)
|
||||||
|
|
||||||
|
// 初始化对话服务
|
||||||
|
dialogService := NewDialogService(logger)
|
||||||
|
|
||||||
// 使用新的配置通知系统设置热键配置变更监听
|
// 使用新的配置通知系统设置热键配置变更监听
|
||||||
err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||||
return hotkeyService.UpdateHotkey(enable, hotkey)
|
return hotkeyService.UpdateHotkey(enable, hotkey)
|
||||||
@@ -54,6 +58,7 @@ func NewServiceManager() *ServiceManager {
|
|||||||
documentService: documentService,
|
documentService: documentService,
|
||||||
systemService: systemService,
|
systemService: systemService,
|
||||||
hotkeyService: hotkeyService,
|
hotkeyService: hotkeyService,
|
||||||
|
dialogService: dialogService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +70,7 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
|||||||
application.NewService(sm.documentService),
|
application.NewService(sm.documentService),
|
||||||
application.NewService(sm.systemService),
|
application.NewService(sm.systemService),
|
||||||
application.NewService(sm.hotkeyService),
|
application.NewService(sm.hotkeyService),
|
||||||
|
application.NewService(sm.dialogService),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user