✨ Complete the document saving service
This commit is contained in:
@@ -18,6 +18,11 @@ export class AppConfig {
|
|||||||
*/
|
*/
|
||||||
"editor": EditorConfig;
|
"editor": EditorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档配置
|
||||||
|
*/
|
||||||
|
"document": DocumentConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径配置
|
* 路径配置
|
||||||
*/
|
*/
|
||||||
@@ -33,6 +38,9 @@ export class AppConfig {
|
|||||||
if (!("editor" in $$source)) {
|
if (!("editor" in $$source)) {
|
||||||
this["editor"] = (new EditorConfig());
|
this["editor"] = (new EditorConfig());
|
||||||
}
|
}
|
||||||
|
if (!("document" in $$source)) {
|
||||||
|
this["document"] = (new DocumentConfig());
|
||||||
|
}
|
||||||
if (!("paths" in $$source)) {
|
if (!("paths" in $$source)) {
|
||||||
this["paths"] = (new PathsConfig());
|
this["paths"] = (new PathsConfig());
|
||||||
}
|
}
|
||||||
@@ -50,15 +58,19 @@ export class AppConfig {
|
|||||||
const $$createField0_0 = $$createType0;
|
const $$createField0_0 = $$createType0;
|
||||||
const $$createField1_0 = $$createType1;
|
const $$createField1_0 = $$createType1;
|
||||||
const $$createField2_0 = $$createType2;
|
const $$createField2_0 = $$createType2;
|
||||||
|
const $$createField3_0 = $$createType3;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("editor" in $$parsedSource) {
|
if ("editor" in $$parsedSource) {
|
||||||
$$parsedSource["editor"] = $$createField0_0($$parsedSource["editor"]);
|
$$parsedSource["editor"] = $$createField0_0($$parsedSource["editor"]);
|
||||||
}
|
}
|
||||||
|
if ("document" in $$parsedSource) {
|
||||||
|
$$parsedSource["document"] = $$createField1_0($$parsedSource["document"]);
|
||||||
|
}
|
||||||
if ("paths" in $$parsedSource) {
|
if ("paths" in $$parsedSource) {
|
||||||
$$parsedSource["paths"] = $$createField1_0($$parsedSource["paths"]);
|
$$parsedSource["paths"] = $$createField2_0($$parsedSource["paths"]);
|
||||||
}
|
}
|
||||||
if ("metadata" in $$parsedSource) {
|
if ("metadata" in $$parsedSource) {
|
||||||
$$parsedSource["metadata"] = $$createField2_0($$parsedSource["metadata"]);
|
$$parsedSource["metadata"] = $$createField3_0($$parsedSource["metadata"]);
|
||||||
}
|
}
|
||||||
return new AppConfig($$parsedSource as Partial<AppConfig>);
|
return new AppConfig($$parsedSource as Partial<AppConfig>);
|
||||||
}
|
}
|
||||||
@@ -99,6 +111,127 @@ export class ConfigMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Document 表示一个文档
|
||||||
|
*/
|
||||||
|
export class Document {
|
||||||
|
/**
|
||||||
|
* 元数据
|
||||||
|
*/
|
||||||
|
"meta": DocumentMeta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档内容
|
||||||
|
*/
|
||||||
|
"content": string;
|
||||||
|
|
||||||
|
/** Creates a new Document instance. */
|
||||||
|
constructor($$source: Partial<Document> = {}) {
|
||||||
|
if (!("meta" in $$source)) {
|
||||||
|
this["meta"] = (new DocumentMeta());
|
||||||
|
}
|
||||||
|
if (!("content" in $$source)) {
|
||||||
|
this["content"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Document instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): Document {
|
||||||
|
const $$createField0_0 = $$createType4;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("meta" in $$parsedSource) {
|
||||||
|
$$parsedSource["meta"] = $$createField0_0($$parsedSource["meta"]);
|
||||||
|
}
|
||||||
|
return new Document($$parsedSource as Partial<Document>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DocumentConfig 定义文档配置
|
||||||
|
*/
|
||||||
|
export class DocumentConfig {
|
||||||
|
/**
|
||||||
|
* 详细保存选项
|
||||||
|
*/
|
||||||
|
"saveOptions": SaveOptions;
|
||||||
|
|
||||||
|
/** Creates a new DocumentConfig instance. */
|
||||||
|
constructor($$source: Partial<DocumentConfig> = {}) {
|
||||||
|
if (!("saveOptions" in $$source)) {
|
||||||
|
this["saveOptions"] = (new SaveOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new DocumentConfig instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): DocumentConfig {
|
||||||
|
const $$createField0_0 = $$createType5;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("saveOptions" in $$parsedSource) {
|
||||||
|
$$parsedSource["saveOptions"] = $$createField0_0($$parsedSource["saveOptions"]);
|
||||||
|
}
|
||||||
|
return new DocumentConfig($$parsedSource as Partial<DocumentConfig>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DocumentMeta 文档元数据
|
||||||
|
*/
|
||||||
|
export class DocumentMeta {
|
||||||
|
/**
|
||||||
|
* 文档唯一标识
|
||||||
|
*/
|
||||||
|
"id": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档标题
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后更新时间
|
||||||
|
*/
|
||||||
|
"lastUpdated": time$0.Time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
"createdAt": time$0.Time;
|
||||||
|
|
||||||
|
/** Creates a new DocumentMeta instance. */
|
||||||
|
constructor($$source: Partial<DocumentMeta> = {}) {
|
||||||
|
if (!("id" in $$source)) {
|
||||||
|
this["id"] = "";
|
||||||
|
}
|
||||||
|
if (!("title" in $$source)) {
|
||||||
|
this["title"] = "";
|
||||||
|
}
|
||||||
|
if (!("lastUpdated" in $$source)) {
|
||||||
|
this["lastUpdated"] = null;
|
||||||
|
}
|
||||||
|
if (!("createdAt" in $$source)) {
|
||||||
|
this["createdAt"] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new DocumentMeta instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): DocumentMeta {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new DocumentMeta($$parsedSource as Partial<DocumentMeta>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EditorConfig 定义编辑器配置
|
* EditorConfig 定义编辑器配置
|
||||||
*/
|
*/
|
||||||
@@ -221,6 +354,49 @@ export class PathsConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SaveOptions 保存选项
|
||||||
|
*/
|
||||||
|
export class SaveOptions {
|
||||||
|
/**
|
||||||
|
* 自动保存延迟(毫秒)- 内容变更后多久自动保存
|
||||||
|
*/
|
||||||
|
"autoSaveDelay": number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更字符阈值,超过此阈值立即触发保存
|
||||||
|
*/
|
||||||
|
"changeThreshold": number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小保存间隔(毫秒)- 两次保存之间的最小时间间隔,避免频繁IO
|
||||||
|
*/
|
||||||
|
"minSaveInterval": number;
|
||||||
|
|
||||||
|
/** Creates a new SaveOptions instance. */
|
||||||
|
constructor($$source: Partial<SaveOptions> = {}) {
|
||||||
|
if (!("autoSaveDelay" in $$source)) {
|
||||||
|
this["autoSaveDelay"] = 0;
|
||||||
|
}
|
||||||
|
if (!("changeThreshold" in $$source)) {
|
||||||
|
this["changeThreshold"] = 0;
|
||||||
|
}
|
||||||
|
if (!("minSaveInterval" in $$source)) {
|
||||||
|
this["minSaveInterval"] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SaveOptions instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): SaveOptions {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new SaveOptions($$parsedSource as Partial<SaveOptions>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TabType 定义了制表符类型
|
* TabType 定义了制表符类型
|
||||||
*/
|
*/
|
||||||
@@ -243,5 +419,8 @@ export enum TabType {
|
|||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = EditorConfig.createFrom;
|
const $$createType0 = EditorConfig.createFrom;
|
||||||
const $$createType1 = PathsConfig.createFrom;
|
const $$createType1 = DocumentConfig.createFrom;
|
||||||
const $$createType2 = ConfigMetadata.createFrom;
|
const $$createType2 = PathsConfig.createFrom;
|
||||||
|
const $$createType3 = ConfigMetadata.createFrom;
|
||||||
|
const $$createType4 = DocumentMeta.createFrom;
|
||||||
|
const $$createType5 = SaveOptions.createFrom;
|
||||||
|
142
frontend/bindings/voidraft/internal/services/documentservice.ts
Normal file
142
frontend/bindings/voidraft/internal/services/documentservice.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DocumentService 提供文档管理功能
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
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 强制保存当前文档
|
||||||
|
*/
|
||||||
|
export function ForceSave(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2767091023) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetActiveDocument 获取当前活动文档
|
||||||
|
*/
|
||||||
|
export function GetActiveDocument(): Promise<models$0.Document | null> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1785823398) as any;
|
||||||
|
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}) as any;
|
||||||
|
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||||
|
return $typingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetActiveDocumentContent 获取当前活动文档内容
|
||||||
|
*/
|
||||||
|
export function GetActiveDocumentContent(): Promise<string> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(922617063) as any;
|
||||||
|
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.DocumentConfig | 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 初始化文档服务
|
||||||
|
*/
|
||||||
|
export function Initialize(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3418008221) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LoadDefaultDocument 加载默认文档
|
||||||
|
*/
|
||||||
|
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 } {
|
||||||
|
let $resultPromise = $Call.ByID(3770207288, content) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceShutdown 实现应用程序关闭时的服务关闭逻辑
|
||||||
|
*/
|
||||||
|
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(638578044) as any;
|
||||||
|
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 更新当前活动文档内容
|
||||||
|
*/
|
||||||
|
export function UpdateActiveDocumentContent(content: string): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1486276638, content) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateSaveSettings 更新文档保存设置
|
||||||
|
*/
|
||||||
|
export function UpdateSaveSettings(docConfig: models$0.DocumentConfig): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1245479534, docConfig) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = models$0.Document.createFrom;
|
||||||
|
const $$createType1 = $Create.Nullable($$createType0);
|
||||||
|
const $$createType2 = $models.DiffResult.createFrom;
|
||||||
|
const $$createType3 = models$0.DocumentConfig.createFrom;
|
||||||
|
const $$createType4 = $Create.Nullable($$createType3);
|
@@ -2,6 +2,10 @@
|
|||||||
// 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 DocumentService from "./documentservice.js";
|
||||||
export {
|
export {
|
||||||
ConfigService
|
ConfigService,
|
||||||
|
DocumentService
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export * from "./models.js";
|
||||||
|
141
frontend/bindings/voidraft/internal/services/models.ts
Normal file
141
frontend/bindings/voidraft/internal/services/models.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = Edit.createFrom;
|
||||||
|
const $$createType1 = $Create.Array($$createType0);
|
@@ -4,30 +4,46 @@ import {EditorState, Extension} from '@codemirror/state';
|
|||||||
import {EditorView} from '@codemirror/view';
|
import {EditorView} from '@codemirror/view';
|
||||||
import {useEditorStore} from '@/stores/editorStore';
|
import {useEditorStore} from '@/stores/editorStore';
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
|
import {useLogStore} from '@/stores/logStore';
|
||||||
import {createBasicSetup} from './extensions/basicSetup';
|
import {createBasicSetup} from './extensions/basicSetup';
|
||||||
import {
|
import {
|
||||||
createStatsUpdateExtension,
|
createStatsUpdateExtension,
|
||||||
createWheelZoomHandler,
|
createWheelZoomHandler,
|
||||||
getTabExtensions,
|
getTabExtensions,
|
||||||
updateStats,
|
updateStats,
|
||||||
updateTabConfig
|
updateTabConfig,
|
||||||
|
createAutoSavePlugin,
|
||||||
|
createSaveShortcutPlugin,
|
||||||
} from './extensions';
|
} from './extensions';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { DocumentService } from '@/../bindings/voidraft/internal/services';
|
||||||
|
|
||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
const documentStore = useDocumentStore();
|
||||||
|
const logStore = useLogStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
initialDoc: {
|
initialDoc: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '// 在此处编写文本...'
|
default: ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const editorElement = ref<HTMLElement | null>(null);
|
const editorElement = ref<HTMLElement | null>(null);
|
||||||
|
const editorCreated = ref(false);
|
||||||
|
let isDestroying = false;
|
||||||
|
|
||||||
// 创建编辑器
|
// 创建编辑器
|
||||||
const createEditor = () => {
|
const createEditor = async () => {
|
||||||
if (!editorElement.value) return;
|
if (!editorElement.value || editorCreated.value) return;
|
||||||
|
editorCreated.value = true;
|
||||||
|
|
||||||
|
// 加载文档内容
|
||||||
|
await documentStore.initialize();
|
||||||
|
const docContent = documentStore.documentContent || props.initialDoc;
|
||||||
|
|
||||||
// 获取基本扩展
|
// 获取基本扩展
|
||||||
const basicExtensions = createBasicSetup();
|
const basicExtensions = createBasicSetup();
|
||||||
@@ -44,16 +60,35 @@ const createEditor = () => {
|
|||||||
editorStore.updateDocumentStats
|
editorStore.updateDocumentStats
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 创建保存快捷键插件
|
||||||
|
const saveShortcutPlugin = createSaveShortcutPlugin(() => {
|
||||||
|
if (editorStore.editorView) {
|
||||||
|
handleManualSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建自动保存插件
|
||||||
|
const autoSavePlugin = createAutoSavePlugin({
|
||||||
|
debounceDelay: 300, // 300毫秒的输入防抖
|
||||||
|
onSave: (success) => {
|
||||||
|
if (success) {
|
||||||
|
documentStore.lastSaved = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 组合所有扩展
|
// 组合所有扩展
|
||||||
const extensions: Extension[] = [
|
const extensions: Extension[] = [
|
||||||
...basicExtensions,
|
...basicExtensions,
|
||||||
...tabExtensions,
|
...tabExtensions,
|
||||||
statsExtension
|
statsExtension,
|
||||||
|
saveShortcutPlugin,
|
||||||
|
autoSavePlugin
|
||||||
];
|
];
|
||||||
|
|
||||||
// 创建编辑器状态
|
// 创建编辑器状态
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: props.initialDoc,
|
doc: docContent,
|
||||||
extensions
|
extensions
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +106,7 @@ const createEditor = () => {
|
|||||||
|
|
||||||
// 立即更新统计信息,不等待用户交互
|
// 立即更新统计信息,不等待用户交互
|
||||||
updateStats(view, editorStore.updateDocumentStats);
|
updateStats(view, editorStore.updateDocumentStats);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建滚轮事件处理器
|
// 创建滚轮事件处理器
|
||||||
@@ -79,6 +115,20 @@ const handleWheel = createWheelZoomHandler(
|
|||||||
configStore.decreaseFontSize
|
configStore.decreaseFontSize
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 手动保存文档
|
||||||
|
const handleManualSave = async () => {
|
||||||
|
if (!editorStore.editorView || isDestroying) return;
|
||||||
|
|
||||||
|
const view = editorStore.editorView as EditorView;
|
||||||
|
const content = view.state.doc.toString();
|
||||||
|
|
||||||
|
// 使用文档存储的强制保存方法
|
||||||
|
const success = await documentStore.forceSaveDocument(content);
|
||||||
|
if (success) {
|
||||||
|
logStore.info(t('document.manualSaveSuccess'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 重新配置编辑器(仅在必要时)
|
// 重新配置编辑器(仅在必要时)
|
||||||
const reconfigureTabSettings = () => {
|
const reconfigureTabSettings = () => {
|
||||||
if (!editorStore.editorView) return;
|
if (!editorStore.editorView) return;
|
||||||
@@ -118,12 +168,14 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
isDestroying = true;
|
||||||
|
|
||||||
// 移除滚轮事件监听
|
// 移除滚轮事件监听
|
||||||
if (editorElement.value) {
|
if (editorElement.value) {
|
||||||
editorElement.value.removeEventListener('wheel', handleWheel);
|
editorElement.value.removeEventListener('wheel', handleWheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 销毁编辑器
|
// 直接销毁编辑器
|
||||||
if (editorStore.editorView) {
|
if (editorStore.editorView) {
|
||||||
editorStore.editorView.destroy();
|
editorStore.editorView.destroy();
|
||||||
editorStore.setEditorView(null);
|
editorStore.setEditorView(null);
|
||||||
|
117
frontend/src/editor/extensions/autoSaveExtension.ts
Normal file
117
frontend/src/editor/extensions/autoSaveExtension.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||||
|
import { DocumentService } from '@/../bindings/voidraft/internal/services';
|
||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
|
||||||
|
// 定义自动保存配置选项
|
||||||
|
export interface AutoSaveOptions {
|
||||||
|
// 保存回调
|
||||||
|
onSave?: (success: boolean) => void;
|
||||||
|
// 内容变更延迟传递(毫秒)- 输入时不会立即发送,有一个小延迟,避免频繁调用后端
|
||||||
|
debounceDelay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建自动保存插件
|
||||||
|
* @param options 配置选项
|
||||||
|
* @returns EditorView.Plugin
|
||||||
|
*/
|
||||||
|
export function createAutoSavePlugin(options: AutoSaveOptions = {}) {
|
||||||
|
const {
|
||||||
|
onSave = () => {},
|
||||||
|
debounceDelay = 1000 // 默认1000ms延迟,原为300ms
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
private isActive: boolean = true;
|
||||||
|
private isSaving: boolean = false;
|
||||||
|
private contentUpdateFn: (view: EditorView) => void;
|
||||||
|
|
||||||
|
constructor(private view: EditorView) {
|
||||||
|
// 创建内容更新函数
|
||||||
|
this.contentUpdateFn = this.createDebouncedUpdateFn(debounceDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建防抖的内容更新函数
|
||||||
|
*/
|
||||||
|
private createDebouncedUpdateFn(delay: number): (view: EditorView) => void {
|
||||||
|
// 使用VueUse的防抖函数创建一个新函数
|
||||||
|
return useDebounceFn(async (view: EditorView) => {
|
||||||
|
// 如果插件已不活跃或正在保存中,不发送
|
||||||
|
if (!this.isActive || this.isSaving) return;
|
||||||
|
|
||||||
|
this.isSaving = true;
|
||||||
|
const content = view.state.doc.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DocumentService.UpdateActiveDocumentContent(content);
|
||||||
|
onSave(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update document content:', err);
|
||||||
|
onSave(false);
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false;
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
// 如果内容没有变化,直接返回
|
||||||
|
if (!update.docChanged) return;
|
||||||
|
|
||||||
|
// 调用防抖函数
|
||||||
|
this.contentUpdateFn(this.view);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// 标记插件不再活跃
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
|
// 直接发送最终内容
|
||||||
|
const content = this.view.state.doc.toString();
|
||||||
|
DocumentService.UpdateActiveDocumentContent(content)
|
||||||
|
.then(() => console.log('Successfully sent final content on destroy'))
|
||||||
|
.catch(err => console.error('Failed to send content on destroy:', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建处理保存快捷键的插件
|
||||||
|
* @param onSave 保存回调
|
||||||
|
* @returns EditorView.Plugin
|
||||||
|
*/
|
||||||
|
export function createSaveShortcutPlugin(onSave: () => void) {
|
||||||
|
return EditorView.domEventHandlers({
|
||||||
|
keydown: (event) => {
|
||||||
|
// Ctrl+S / Cmd+S
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||||
|
event.preventDefault();
|
||||||
|
onSave();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发文档保存
|
||||||
|
* @param view 编辑器视图
|
||||||
|
* @returns Promise<boolean>
|
||||||
|
*/
|
||||||
|
export async function saveDocument(view: EditorView): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const content = view.state.doc.toString();
|
||||||
|
// 更新内容
|
||||||
|
await DocumentService.UpdateActiveDocumentContent(content);
|
||||||
|
// 强制保存到磁盘
|
||||||
|
await DocumentService.ForceSave();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save document:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@@ -2,3 +2,4 @@
|
|||||||
export * from './tabExtension';
|
export * from './tabExtension';
|
||||||
export * from './wheelZoomExtension';
|
export * from './wheelZoomExtension';
|
||||||
export * from './statsExtension';
|
export * from './statsExtension';
|
||||||
|
export * from './autoSaveExtension';
|
@@ -31,5 +31,17 @@ export default {
|
|||||||
languages: {
|
languages: {
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
'en-US': 'English'
|
'en-US': 'English'
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
loadSuccess: 'Document loaded successfully',
|
||||||
|
loadFailed: 'Failed to load document',
|
||||||
|
saveSuccess: 'Document saved successfully',
|
||||||
|
saveFailed: 'Failed to save document',
|
||||||
|
manualSaveSuccess: 'Manually saved successfully',
|
||||||
|
settings: {
|
||||||
|
loadFailed: 'Failed to load save settings',
|
||||||
|
saveSuccess: 'Save settings updated',
|
||||||
|
saveFailed: 'Failed to update save settings'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@@ -31,5 +31,17 @@ export default {
|
|||||||
languages: {
|
languages: {
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
'en-US': 'English'
|
'en-US': 'English'
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
loadSuccess: '文档加载成功',
|
||||||
|
loadFailed: '文档加载失败',
|
||||||
|
saveSuccess: '文档保存成功',
|
||||||
|
saveFailed: '文档保存失败',
|
||||||
|
manualSaveSuccess: '手动保存成功',
|
||||||
|
settings: {
|
||||||
|
loadFailed: '加载保存设置失败',
|
||||||
|
saveSuccess: '保存设置已更新',
|
||||||
|
saveFailed: '保存设置更新失败'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
123
frontend/src/stores/documentStore.ts
Normal file
123
frontend/src/stores/documentStore.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||||
|
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
import {useLogStore} from './logStore';
|
||||||
|
import {useI18n} from 'vue-i18n';
|
||||||
|
|
||||||
|
export const useDocumentStore = defineStore('document', () => {
|
||||||
|
const logStore = useLogStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const activeDocument = ref<Document | null>(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const lastSaved = ref<Date | null>(null);
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const documentContent = computed(() => activeDocument.value?.content || '');
|
||||||
|
const documentTitle = computed(() => activeDocument.value?.meta?.title || '');
|
||||||
|
const hasActiveDocument = computed(() => !!activeDocument.value);
|
||||||
|
const isSaveInProgress = computed(() => isSaving.value);
|
||||||
|
const lastSavedTime = computed(() => lastSaved.value);
|
||||||
|
|
||||||
|
// 加载文档
|
||||||
|
async function loadDocument() {
|
||||||
|
if (isLoading.value) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
activeDocument.value = await DocumentService.GetActiveDocument();
|
||||||
|
logStore.info(t('document.loadSuccess'));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load document:', err);
|
||||||
|
logStore.error(t('document.loadFailed'));
|
||||||
|
activeDocument.value = null;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文档
|
||||||
|
async function saveDocument(content: string): Promise<boolean> {
|
||||||
|
if (isSaving.value) return false;
|
||||||
|
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
await DocumentService.UpdateActiveDocumentContent(content);
|
||||||
|
lastSaved.value = new Date();
|
||||||
|
|
||||||
|
// 如果我们有活动文档,更新本地副本
|
||||||
|
if (activeDocument.value) {
|
||||||
|
activeDocument.value.content = content;
|
||||||
|
activeDocument.value.meta.lastUpdated = lastSaved.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
logStore.info(t('document.saveSuccess'));
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save document:', err);
|
||||||
|
logStore.error(t('document.saveFailed'));
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制保存文档到磁盘
|
||||||
|
async function forceSaveDocument(content: string): Promise<boolean> {
|
||||||
|
if (isSaving.value) return false;
|
||||||
|
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
// 先更新内容
|
||||||
|
await DocumentService.UpdateActiveDocumentContent(content);
|
||||||
|
// 然后强制保存
|
||||||
|
await DocumentService.ForceSave();
|
||||||
|
|
||||||
|
lastSaved.value = new Date();
|
||||||
|
|
||||||
|
// 如果我们有活动文档,更新本地副本
|
||||||
|
if (activeDocument.value) {
|
||||||
|
activeDocument.value.content = content;
|
||||||
|
activeDocument.value.meta.lastUpdated = lastSaved.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
logStore.info(t('document.manualSaveSuccess'));
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to force save document:', err);
|
||||||
|
logStore.error(t('document.saveFailed'));
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
async function initialize() {
|
||||||
|
await loadDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
activeDocument,
|
||||||
|
isLoading,
|
||||||
|
isSaving,
|
||||||
|
lastSaved,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
documentContent,
|
||||||
|
documentTitle,
|
||||||
|
hasActiveDocument,
|
||||||
|
isSaveInProgress,
|
||||||
|
lastSavedTime,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
loadDocument,
|
||||||
|
saveDocument,
|
||||||
|
forceSaveDocument,
|
||||||
|
initialize
|
||||||
|
};
|
||||||
|
});
|
@@ -16,6 +16,21 @@ const (
|
|||||||
TabTypeTab TabType = "tab"
|
TabTypeTab TabType = "tab"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SaveOptions 保存选项
|
||||||
|
type SaveOptions struct {
|
||||||
|
// 自动保存延迟(毫秒)- 内容变更后多久自动保存
|
||||||
|
AutoSaveDelay int `json:"autoSaveDelay"`
|
||||||
|
// 变更字符阈值,超过此阈值立即触发保存
|
||||||
|
ChangeThreshold int `json:"changeThreshold"`
|
||||||
|
// 最小保存间隔(毫秒)- 两次保存之间的最小时间间隔,避免频繁IO
|
||||||
|
MinSaveInterval int `json:"minSaveInterval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentConfig 定义文档配置
|
||||||
|
type DocumentConfig struct {
|
||||||
|
SaveOptions SaveOptions `json:"saveOptions"` // 详细保存选项
|
||||||
|
}
|
||||||
|
|
||||||
// EditorConfig 定义编辑器配置
|
// EditorConfig 定义编辑器配置
|
||||||
type EditorConfig struct {
|
type EditorConfig struct {
|
||||||
FontSize int `json:"fontSize"` // 字体大小
|
FontSize int `json:"fontSize"` // 字体大小
|
||||||
@@ -45,6 +60,7 @@ type PathsConfig struct {
|
|||||||
// AppConfig 应用配置 - 包含业务配置和路径配置
|
// AppConfig 应用配置 - 包含业务配置和路径配置
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Editor EditorConfig `json:"editor"` // 编辑器配置
|
Editor EditorConfig `json:"editor"` // 编辑器配置
|
||||||
|
Document DocumentConfig `json:"document"` // 文档配置
|
||||||
Paths PathsConfig `json:"paths"` // 路径配置
|
Paths PathsConfig `json:"paths"` // 路径配置
|
||||||
Metadata ConfigMetadata `json:"metadata"` // 配置元数据
|
Metadata ConfigMetadata `json:"metadata"` // 配置元数据
|
||||||
}
|
}
|
||||||
@@ -65,6 +81,7 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
|
|
||||||
// 默认路径配置
|
// 默认路径配置
|
||||||
rootDir := filepath.Join(homePath, ".voidraft")
|
rootDir := filepath.Join(homePath, ".voidraft")
|
||||||
|
dataDir := filepath.Join(rootDir, "data")
|
||||||
|
|
||||||
return &AppConfig{
|
return &AppConfig{
|
||||||
Editor: EditorConfig{
|
Editor: EditorConfig{
|
||||||
@@ -75,9 +92,16 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
Language: LangZhCN,
|
Language: LangZhCN,
|
||||||
AlwaysOnTop: false,
|
AlwaysOnTop: false,
|
||||||
},
|
},
|
||||||
|
Document: DocumentConfig{
|
||||||
|
SaveOptions: SaveOptions{
|
||||||
|
AutoSaveDelay: 5000, // 5秒后自动保存
|
||||||
|
ChangeThreshold: 500, // 500个字符变更触发保存
|
||||||
|
MinSaveInterval: 1000, // 最小间隔1000毫秒
|
||||||
|
},
|
||||||
|
},
|
||||||
Paths: PathsConfig{
|
Paths: PathsConfig{
|
||||||
LogPath: filepath.Join(rootDir, "logs"),
|
LogPath: filepath.Join(rootDir, "logs"),
|
||||||
DataPath: filepath.Join(rootDir, "data"),
|
DataPath: dataDir,
|
||||||
},
|
},
|
||||||
Metadata: ConfigMetadata{
|
Metadata: ConfigMetadata{
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
|
41
internal/models/document.go
Normal file
41
internal/models/document.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DocumentMeta 文档元数据
|
||||||
|
type DocumentMeta struct {
|
||||||
|
ID string `json:"id"` // 文档唯一标识
|
||||||
|
Title string `json:"title"` // 文档标题
|
||||||
|
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
|
||||||
|
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document 表示一个文档
|
||||||
|
type Document struct {
|
||||||
|
Meta DocumentMeta `json:"meta"` // 元数据
|
||||||
|
Content string `json:"content"` // 文档内容
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentInfo 文档信息(不包含内容,用于列表展示)
|
||||||
|
type DocumentInfo struct {
|
||||||
|
ID string `json:"id"` // 文档ID
|
||||||
|
Title string `json:"title"` // 文档标题
|
||||||
|
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
|
||||||
|
Path string `json:"path"` // 文档路径
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultDocument 创建默认文档
|
||||||
|
func NewDefaultDocument() *Document {
|
||||||
|
now := time.Now()
|
||||||
|
return &Document{
|
||||||
|
Meta: DocumentMeta{
|
||||||
|
ID: "default",
|
||||||
|
Title: "默认文档",
|
||||||
|
LastUpdated: now,
|
||||||
|
CreatedAt: now,
|
||||||
|
},
|
||||||
|
Content: "// 在此处编写文本...",
|
||||||
|
}
|
||||||
|
}
|
@@ -35,9 +35,8 @@ func (p *DefaultConfigPathProvider) GetConfigPath() string {
|
|||||||
|
|
||||||
// ConfigOption 配置服务选项
|
// ConfigOption 配置服务选项
|
||||||
type ConfigOption struct {
|
type ConfigOption struct {
|
||||||
Logger *log.LoggerService // 日志服务
|
Logger *log.LoggerService // 日志服务
|
||||||
PathProvider ConfigPath // 路径提供者
|
PathProvider ConfigPath // 路径提供者
|
||||||
AutoSaveEnabled bool // 是否启用自动保存
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigService 提供配置管理功能
|
// ConfigService 提供配置管理功能
|
||||||
@@ -98,7 +97,6 @@ func NewConfigService(opt ...ConfigOption) *ConfigService {
|
|||||||
// 创建存储
|
// 创建存储
|
||||||
store := NewStore[models.AppConfig](StoreOption{
|
store := NewStore[models.AppConfig](StoreOption{
|
||||||
FilePath: configPath,
|
FilePath: configPath,
|
||||||
AutoSave: option.AutoSaveEnabled,
|
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
306
internal/services/document_diff.go
Normal file
306
internal/services/document_diff.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
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 == '_'
|
||||||
|
}
|
592
internal/services/document_service.go
Normal file
592
internal/services/document_service.go
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"voidraft/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveTrigger 保存触发器类型
|
||||||
|
type SaveTrigger int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SaveTriggerAuto 自动保存
|
||||||
|
SaveTriggerAuto SaveTrigger = iota
|
||||||
|
// SaveTriggerManual 手动触发保存
|
||||||
|
SaveTriggerManual
|
||||||
|
// SaveTriggerThreshold 超过阈值触发保存
|
||||||
|
SaveTriggerThreshold
|
||||||
|
// SaveTriggerShutdown 程序关闭触发保存
|
||||||
|
SaveTriggerShutdown
|
||||||
|
)
|
||||||
|
|
||||||
|
// DocumentError 文档操作错误
|
||||||
|
type DocumentError struct {
|
||||||
|
Operation string // 操作名称
|
||||||
|
Err error // 原始错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 实现error接口
|
||||||
|
func (e *DocumentError) Error() string {
|
||||||
|
return fmt.Sprintf("document error during %s: %v", e.Operation, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap 获取原始错误
|
||||||
|
func (e *DocumentError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentService 提供文档管理功能
|
||||||
|
type DocumentService struct {
|
||||||
|
configService *ConfigService
|
||||||
|
logger *log.LoggerService
|
||||||
|
activeDoc *models.Document
|
||||||
|
docStore *Store[models.Document]
|
||||||
|
memoryCache *models.Document // 内存缓存,小改动只更新此缓存
|
||||||
|
lock sync.RWMutex
|
||||||
|
lastSaveTime time.Time
|
||||||
|
changeCounter int // 变更计数器,记录自上次保存后的变更数量
|
||||||
|
saveTimer *time.Timer // 自动保存定时器
|
||||||
|
pendingSave bool // 是否有等待保存的更改
|
||||||
|
saveChannel chan SaveTrigger // 保存通道,用于接收保存触发信号
|
||||||
|
shutdownChan chan struct{} // 关闭通道,用于程序退出时通知保存协程
|
||||||
|
shutdownWg sync.WaitGroup // 等待组,用于确保保存协程正常退出
|
||||||
|
onSaveCallback func(trigger SaveTrigger) // 保存回调函数
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentService 创建新的文档服务实例
|
||||||
|
func NewDocumentService(configService *ConfigService, logger *log.LoggerService) *DocumentService {
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
service := &DocumentService{
|
||||||
|
configService: configService,
|
||||||
|
logger: logger,
|
||||||
|
saveChannel: make(chan SaveTrigger, 10),
|
||||||
|
shutdownChan: make(chan struct{}),
|
||||||
|
lastSaveTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize 初始化文档服务
|
||||||
|
func (ds *DocumentService) Initialize() error {
|
||||||
|
// 确保文档目录存在
|
||||||
|
err := ds.ensureDocumentsDir()
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to ensure documents directory", "error", err)
|
||||||
|
return &DocumentError{Operation: "initialize", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化文档存储
|
||||||
|
err = ds.initDocumentStore()
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to initialize document store", "error", err)
|
||||||
|
return &DocumentError{Operation: "init_store", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载默认文档
|
||||||
|
err = ds.LoadDefaultDocument()
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to load default document", "error", err)
|
||||||
|
return &DocumentError{Operation: "load_default", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动保存处理协程
|
||||||
|
ds.startSaveProcessor()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// startSaveProcessor 启动保存处理协程
|
||||||
|
func (ds *DocumentService) startSaveProcessor() {
|
||||||
|
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()
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to get config for auto save", "error", err)
|
||||||
|
// 使用默认值2秒
|
||||||
|
ds.scheduleTimerWithDelay(2000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查配置有效性
|
||||||
|
if config == nil {
|
||||||
|
ds.logger.Error("Document: Config is nil, using default delay")
|
||||||
|
ds.scheduleTimerWithDelay(2000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印保存设置,便于调试
|
||||||
|
ds.logger.Debug("Document: Auto save settings",
|
||||||
|
"autoSaveDelay", config.Document.SaveOptions.AutoSaveDelay,
|
||||||
|
"changeThreshold", config.Document.SaveOptions.ChangeThreshold,
|
||||||
|
"minSaveInterval", config.Document.SaveOptions.MinSaveInterval)
|
||||||
|
|
||||||
|
ds.lock.Lock()
|
||||||
|
defer ds.lock.Unlock()
|
||||||
|
|
||||||
|
// 重置自动保存定时器
|
||||||
|
if ds.saveTimer != nil {
|
||||||
|
ds.saveTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的自动保存定时器
|
||||||
|
autoSaveDelay := config.Document.SaveOptions.AutoSaveDelay
|
||||||
|
ds.logger.Debug("Document: Scheduling auto save", "delay", autoSaveDelay)
|
||||||
|
ds.scheduleTimerWithDelay(autoSaveDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scheduleTimerWithDelay 使用指定延迟创建定时器
|
||||||
|
func (ds *DocumentService) scheduleTimerWithDelay(delayMs int) {
|
||||||
|
ds.saveTimer = time.AfterFunc(time.Duration(delayMs)*time.Millisecond, func() {
|
||||||
|
// 只有在有待保存的更改时才触发保存
|
||||||
|
if ds.pendingSave {
|
||||||
|
ds.saveChannel <- SaveTriggerAuto
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveToStore 保存文档到存储
|
||||||
|
func (ds *DocumentService) saveToStore(trigger SaveTrigger) {
|
||||||
|
ds.lock.Lock()
|
||||||
|
defer ds.lock.Unlock()
|
||||||
|
|
||||||
|
// 如果没有内存缓存或活动文档,直接返回
|
||||||
|
if ds.memoryCache == nil || ds.activeDoc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
config, err := ds.configService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to get config for save", "error", err)
|
||||||
|
// 继续使用默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
minInterval := 500 // 默认500毫秒
|
||||||
|
|
||||||
|
// 如果成功获取了配置,使用配置值
|
||||||
|
if err == nil && config != nil {
|
||||||
|
minInterval = config.Document.SaveOptions.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制确保保存到磁盘
|
||||||
|
err = ds.docStore.Save()
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to force save document", "trigger", trigger, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置计数器和状态
|
||||||
|
ds.changeCounter = 0
|
||||||
|
ds.pendingSave = false
|
||||||
|
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 关闭文档服务,确保所有数据保存
|
||||||
|
func (ds *DocumentService) Shutdown() {
|
||||||
|
// 发送关闭信号
|
||||||
|
close(ds.shutdownChan)
|
||||||
|
|
||||||
|
// 等待保存协程退出
|
||||||
|
ds.shutdownWg.Wait()
|
||||||
|
|
||||||
|
// 停止定时器
|
||||||
|
if ds.saveTimer != nil {
|
||||||
|
ds.saveTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.logger.Info("Document: Service shutdown completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSaveCallback 设置保存回调函数
|
||||||
|
func (ds *DocumentService) SetSaveCallback(callback func(trigger SaveTrigger)) {
|
||||||
|
ds.onSaveCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// initDocumentStore 初始化文档存储
|
||||||
|
func (ds *DocumentService) initDocumentStore() error {
|
||||||
|
docPath, err := ds.getDefaultDocumentPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.logger.Info("Document: Initializing document store", "path", docPath)
|
||||||
|
|
||||||
|
// 创建文档存储,强制保存和Service触发的保存都使用同步保存到磁盘
|
||||||
|
ds.docStore = NewStore[models.Document](StoreOption{
|
||||||
|
FilePath: docPath,
|
||||||
|
AutoSave: true, // 启用自动保存,确保Set操作直接写入磁盘
|
||||||
|
Logger: ds.logger,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDocumentsDir 确保文档目录存在
|
||||||
|
func (ds *DocumentService) ensureDocumentsDir() error {
|
||||||
|
config, err := ds.configService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文档目录
|
||||||
|
docsDir := filepath.Join(config.Paths.DataPath, "docs")
|
||||||
|
err = os.MkdirAll(docsDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDocumentsDir 获取文档目录路径
|
||||||
|
func (ds *DocumentService) getDocumentsDir() (string, error) {
|
||||||
|
config, err := ds.configService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(config.Paths.DataPath, "docs"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultDocumentPath 获取默认文档路径
|
||||||
|
func (ds *DocumentService) getDefaultDocumentPath() (string, error) {
|
||||||
|
docsDir, err := ds.getDocumentsDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(docsDir, "default.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadDefaultDocument 加载默认文档
|
||||||
|
func (ds *DocumentService) LoadDefaultDocument() error {
|
||||||
|
// 从Store加载文档
|
||||||
|
doc := ds.docStore.Get()
|
||||||
|
|
||||||
|
// 检查文档是否有效
|
||||||
|
if doc.Meta.ID == "" {
|
||||||
|
// 创建默认文档
|
||||||
|
defaultDoc := models.NewDefaultDocument()
|
||||||
|
ds.lock.Lock()
|
||||||
|
ds.activeDoc = defaultDoc
|
||||||
|
ds.memoryCache = defaultDoc // 同时更新内存缓存
|
||||||
|
ds.lock.Unlock()
|
||||||
|
|
||||||
|
// 保存默认文档
|
||||||
|
err := ds.docStore.Set(*defaultDoc)
|
||||||
|
if err != nil {
|
||||||
|
return &DocumentError{Operation: "save_default", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.logger.Info("Document: Created and saved default document")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置为活动文档
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveDocument 获取当前活动文档
|
||||||
|
func (ds *DocumentService) GetActiveDocument() (*models.Document, error) {
|
||||||
|
ds.lock.RLock()
|
||||||
|
defer ds.lock.RUnlock()
|
||||||
|
|
||||||
|
if ds.memoryCache == nil {
|
||||||
|
return nil, errors.New("no active document loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回内存缓存中的文档,确保获得最新版本
|
||||||
|
return ds.memoryCache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveDocumentContent 获取当前活动文档内容
|
||||||
|
func (ds *DocumentService) GetActiveDocumentContent() (string, error) {
|
||||||
|
ds.lock.RLock()
|
||||||
|
defer ds.lock.RUnlock()
|
||||||
|
|
||||||
|
if ds.memoryCache == nil {
|
||||||
|
return "", errors.New("no active document loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ds.memoryCache.Content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateActiveDocumentContent 更新当前活动文档内容
|
||||||
|
func (ds *DocumentService) UpdateActiveDocumentContent(content string) error {
|
||||||
|
// 获取配置
|
||||||
|
config, err := ds.configService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to get config for content update", "error", err)
|
||||||
|
// 出错时仍继续,使用默认行为
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认配置值
|
||||||
|
threshold := 100 // 默认值
|
||||||
|
|
||||||
|
// 如果成功获取了配置,使用配置值
|
||||||
|
if err == nil && config != nil {
|
||||||
|
threshold = config.Document.SaveOptions.ChangeThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.lock.Lock()
|
||||||
|
|
||||||
|
if ds.memoryCache == nil {
|
||||||
|
ds.lock.Unlock()
|
||||||
|
return errors.New("no active document loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算变更数量
|
||||||
|
oldContent := ds.memoryCache.Content
|
||||||
|
changedChars := calculateChanges(oldContent, content)
|
||||||
|
ds.changeCounter += changedChars
|
||||||
|
|
||||||
|
// 调试信息
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveDocumentSync 同步保存文档内容 (用于页面关闭前同步保存)
|
||||||
|
func (ds *DocumentService) SaveDocumentSync(content string) error {
|
||||||
|
ds.lock.Lock()
|
||||||
|
|
||||||
|
if ds.memoryCache == nil {
|
||||||
|
ds.lock.Unlock()
|
||||||
|
return errors.New("no active document loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新内存缓存
|
||||||
|
ds.memoryCache.Content = content
|
||||||
|
ds.memoryCache.Meta.LastUpdated = time.Now()
|
||||||
|
|
||||||
|
// 直接保存到存储
|
||||||
|
doc := *ds.memoryCache
|
||||||
|
ds.lock.Unlock()
|
||||||
|
|
||||||
|
err := ds.docStore.Set(doc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
ds.lock.Lock()
|
||||||
|
ds.pendingSave = false
|
||||||
|
ds.changeCounter = 0
|
||||||
|
ds.lastSaveTime = time.Now()
|
||||||
|
ds.lock.Unlock()
|
||||||
|
|
||||||
|
ds.logger.Info("Document: Synced document save completed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceSave 强制保存当前文档
|
||||||
|
func (ds *DocumentService) ForceSave() error {
|
||||||
|
ds.logger.Info("Document: Force save triggered")
|
||||||
|
|
||||||
|
// 获取当前文档内容
|
||||||
|
ds.lock.RLock()
|
||||||
|
if ds.memoryCache == nil {
|
||||||
|
ds.lock.RUnlock()
|
||||||
|
return errors.New("no active document loaded")
|
||||||
|
}
|
||||||
|
content := ds.memoryCache.Content
|
||||||
|
ds.lock.RUnlock()
|
||||||
|
|
||||||
|
// 使用同步方法直接保存到磁盘
|
||||||
|
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 计算两个字符串之间的变更数量
|
||||||
|
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.DocumentConfig, error) {
|
||||||
|
config, err := ds.configService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &DocumentError{Operation: "get_save_settings", Err: err}
|
||||||
|
}
|
||||||
|
return &config.Document, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSaveSettings 更新文档保存设置
|
||||||
|
func (ds *DocumentService) UpdateSaveSettings(docConfig models.DocumentConfig) error {
|
||||||
|
// 获取当前配置
|
||||||
|
config, err := ds.configService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return &DocumentError{Operation: "update_save_settings", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新保存设置
|
||||||
|
config.Document = docConfig
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
err = ds.configService.SaveConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return &DocumentError{Operation: "update_save_settings_save", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安排自动保存(不再需要检查保存模式)
|
||||||
|
ds.scheduleAutoSave()
|
||||||
|
|
||||||
|
ds.logger.Info("Document: Updated save settings")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceShutdown 实现应用程序关闭时的服务关闭逻辑
|
||||||
|
func (ds *DocumentService) ServiceShutdown() error {
|
||||||
|
ds.logger.Info("Document: Service is shutting down, saving document...")
|
||||||
|
|
||||||
|
// 获取当前活动文档
|
||||||
|
ds.lock.RLock()
|
||||||
|
if ds.memoryCache == nil {
|
||||||
|
ds.lock.RUnlock()
|
||||||
|
ds.logger.Info("Document: No active document to save on shutdown")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取要保存的内容
|
||||||
|
content := ds.memoryCache.Content
|
||||||
|
ds.lock.RUnlock()
|
||||||
|
|
||||||
|
// 同步保存文档内容
|
||||||
|
err := ds.SaveDocumentSync(content)
|
||||||
|
if err != nil {
|
||||||
|
ds.logger.Error("Document: Failed to save document on shutdown", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.logger.Info("Document: Document saved successfully on shutdown")
|
||||||
|
|
||||||
|
// 关闭通道以通知保存协程退出
|
||||||
|
close(ds.shutdownChan)
|
||||||
|
|
||||||
|
// 等待保存协程退出
|
||||||
|
ds.shutdownWg.Wait()
|
||||||
|
|
||||||
|
// 停止所有计时器
|
||||||
|
if ds.saveTimer != nil {
|
||||||
|
ds.saveTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -7,8 +7,9 @@ import (
|
|||||||
|
|
||||||
// ServiceManager 服务管理器,负责协调各个服务
|
// ServiceManager 服务管理器,负责协调各个服务
|
||||||
type ServiceManager struct {
|
type ServiceManager struct {
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
logger *log.LoggerService
|
documentService *DocumentService
|
||||||
|
logger *log.LoggerService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServiceManager 创建新的服务管理器实例
|
// NewServiceManager 创建新的服务管理器实例
|
||||||
@@ -18,14 +19,24 @@ func NewServiceManager() *ServiceManager {
|
|||||||
|
|
||||||
// 初始化配置服务
|
// 初始化配置服务
|
||||||
configService := NewConfigService(ConfigOption{
|
configService := NewConfigService(ConfigOption{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
PathProvider: nil,
|
PathProvider: nil,
|
||||||
AutoSaveEnabled: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 初始化文档服务
|
||||||
|
documentService := NewDocumentService(configService, logger)
|
||||||
|
|
||||||
|
// 初始化文档服务
|
||||||
|
err := documentService.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to initialize document service", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
return &ServiceManager{
|
return &ServiceManager{
|
||||||
configService: configService,
|
configService: configService,
|
||||||
logger: logger,
|
documentService: documentService,
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,10 +44,6 @@ func NewServiceManager() *ServiceManager {
|
|||||||
func (sm *ServiceManager) GetServices() []application.Service {
|
func (sm *ServiceManager) GetServices() []application.Service {
|
||||||
return []application.Service{
|
return []application.Service{
|
||||||
application.NewService(sm.configService),
|
application.NewService(sm.configService),
|
||||||
|
application.NewService(sm.documentService),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigService 获取配置服务实例
|
|
||||||
func (sm *ServiceManager) GetConfigService() *ConfigService {
|
|
||||||
return sm.configService
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user