- Internal documents are now ignored.
- Merge dialogue now respond immediately to button pressing.
- Periodic processing now works fine
- The checking interval of detecting conflicted has got shorter
- Replication is now cancelled while cleaning up
- The database locking by the cleaning up is now carefully unlocked
- Missing chunks message is correctly reported

New feature:
- Suspend database reflecting has been implemented
- Now fetch suspends the reflecting database and storage changes temporarily to improve the performance.
- We can choose the action when the remote database has been cleaned
- Merge dialogue now show `↲` before the new line.

Improved:
- Now progress is reported while the cleaning up and fetch process
- Cancelled replication is now detected
This commit is contained in:
vorotamoroz
2023-07-25 19:16:39 +09:00
parent 0a2caea3c7
commit db9d428ab4
9 changed files with 182 additions and 45 deletions

View File

@@ -4,7 +4,7 @@ import { Notice, type PluginManifest, parseYaml } from "./deps";
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types"; import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types";
import { LOG_LEVEL } from "./lib/src/types"; import { LOG_LEVEL } from "./lib/src/types";
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types"; import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
import { Parallels, delay, getDocData } from "./lib/src/utils"; import { delay, getDocData } from "./lib/src/utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { WrappedNotice } from "./lib/src/wrapper"; import { WrappedNotice } from "./lib/src/wrapper";
import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin"; import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin";

View File

@@ -8,6 +8,7 @@ import { LiveSyncCommands } from "./LiveSyncCommands";
import { delay } from "./lib/src/utils"; import { delay } from "./lib/src/utils";
import { confirmWithMessage } from "./dialogs"; import { confirmWithMessage } from "./dialogs";
import { Platform } from "./deps"; import { Platform } from "./deps";
import { fetchAllUsedChunks } from "./lib/src/utils_couchdb";
export class SetupLiveSync extends LiveSyncCommands { export class SetupLiveSync extends LiveSyncCommands {
onunload() { } onunload() { }
@@ -284,6 +285,26 @@ Of course, we are able to disable these features.`
this.plugin.settings.syncAfterMerge = false; this.plugin.settings.syncAfterMerge = false;
//this.suspendExtraSync(); //this.suspendExtraSync();
} }
async suspendReflectingDatabase() {
if (this.plugin.settings.doNotSuspendOnFetching) return;
Logger(`Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`, LOG_LEVEL.NOTICE);
this.plugin.settings.suspendParseReplicationResult = true;
this.plugin.settings.suspendFileWatching = true;
await this.plugin.saveSettings();
}
async resumeReflectingDatabase() {
if (this.plugin.settings.doNotSuspendOnFetching) return;
Logger(`Database and storage reflection has been resumed!`, LOG_LEVEL.NOTICE);
this.plugin.settings.suspendParseReplicationResult = false;
this.plugin.settings.suspendFileWatching = false;
await this.plugin.saveSettings();
if (this.plugin.settings.readChunksOnline) {
await this.plugin.syncAllFiles(true);
await this.plugin.loadQueuedFiles();
// Start processing
this.plugin.procQueuedFiles();
}
}
async askUseNewAdapter() { async askUseNewAdapter() {
if (!this.plugin.settings.useIndexedDBAdapter) { if (!this.plugin.settings.useIndexedDBAdapter) {
const message = `Now this plugin has been configured to use the old database adapter for keeping compatibility. Do you want to deactivate it?`; const message = `Now this plugin has been configured to use the old database adapter for keeping compatibility. Do you want to deactivate it?`;
@@ -297,9 +318,22 @@ Of course, we are able to disable these features.`
} }
} }
} }
async fetchRemoteChunks() {
if (!this.plugin.settings.doNotSuspendOnFetching && this.plugin.settings.readChunksOnline) {
Logger(`Fetching chunks`, LOG_LEVEL.NOTICE);
const remoteDB = await this.plugin.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.plugin.getIsMobile(), true);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL.NOTICE);
} else {
await fetchAllUsedChunks(this.localDatabase.localDatabase, remoteDB.db);
}
Logger(`Fetching chunks done`, LOG_LEVEL.NOTICE);
}
}
async fetchLocal() { async fetchLocal() {
this.suspendExtraSync(); this.suspendExtraSync();
this.askUseNewAdapter(); this.askUseNewAdapter();
await this.suspendReflectingDatabase();
await this.plugin.realizeSettingSyncMode(); await this.plugin.realizeSettingSyncMode();
await this.plugin.resetLocalDatabase(); await this.plugin.resetLocalDatabase();
await delay(1000); await delay(1000);
@@ -310,6 +344,8 @@ Of course, we are able to disable these features.`
await this.plugin.replicateAllFromServer(true); await this.plugin.replicateAllFromServer(true);
await delay(1000); await delay(1000);
await this.plugin.replicateAllFromServer(true); await this.plugin.replicateAllFromServer(true);
await this.fetchRemoteChunks();
await this.resumeReflectingDatabase();
await this.askHiddenFileConfiguration({ enableFetch: true }); await this.askHiddenFileConfiguration({ enableFetch: true });
} }
async rebuildRemote() { async rebuildRemote() {

View File

@@ -30,11 +30,11 @@ export class ConflictResolveModal extends Modal {
const x1 = v[0]; const x1 = v[0];
const x2 = v[1]; const x2 = v[1];
if (x1 == DIFF_DELETE) { if (x1 == DIFF_DELETE) {
diff += "<span class='deleted'>" + escapeStringToHTML(x2) + "</span>"; diff += "<span class='deleted'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
} else if (x1 == DIFF_EQUAL) { } else if (x1 == DIFF_EQUAL) {
diff += "<span class='normal'>" + escapeStringToHTML(x2) + "</span>"; diff += "<span class='normal'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
} else if (x1 == DIFF_INSERT) { } else if (x1 == DIFF_INSERT) {
diff += "<span class='added'>" + escapeStringToHTML(x2) + "</span>"; diff += "<span class='added'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
} }
} }
@@ -48,23 +48,26 @@ export class ConflictResolveModal extends Modal {
`; `;
contentEl.createEl("button", { text: "Keep A" }, (e) => { contentEl.createEl("button", { text: "Keep A" }, (e) => {
e.addEventListener("click", async () => { e.addEventListener("click", async () => {
await this.callback(this.result.right.rev); const callback = this.callback;
this.callback = null; this.callback = null;
this.close(); this.close();
await callback(this.result.right.rev);
}); });
}); });
contentEl.createEl("button", { text: "Keep B" }, (e) => { contentEl.createEl("button", { text: "Keep B" }, (e) => {
e.addEventListener("click", async () => { e.addEventListener("click", async () => {
await this.callback(this.result.left.rev); const callback = this.callback;
this.callback = null; this.callback = null;
this.close(); this.close();
await callback(this.result.left.rev);
}); });
}); });
contentEl.createEl("button", { text: "Concat both" }, (e) => { contentEl.createEl("button", { text: "Concat both" }, (e) => {
e.addEventListener("click", async () => { e.addEventListener("click", async () => {
await this.callback(""); const callback = this.callback;
this.callback = null; this.callback = null;
this.close(); this.close();
await callback("");
}); });
}); });
contentEl.createEl("button", { text: "Not now" }, (e) => { contentEl.createEl("button", { text: "Not now" }, (e) => {

View File

@@ -7,7 +7,7 @@ import { Logger } from "./lib/src/logger";
import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js"; import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js";
import { testCrypt } from "./lib/src/e2ee_v2"; import { testCrypt } from "./lib/src/e2ee_v2";
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
import { performRebuildDB, requestToCouchDB } from "./utils"; import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
export class ObsidianLiveSyncSettingTab extends PluginSettingTab { export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
@@ -1610,6 +1610,27 @@ ${stringifyYaml(pluginConfig)}`;
toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => { toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => {
this.plugin.settings.suspendFileWatching = value; this.plugin.settings.suspendFileWatching = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
scheduleTask("configReload", 250, async () => {
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
// @ts-ignore
this.app.commands.executeCommandById("app:reload")
}
})
})
);
new Setting(containerHatchEl)
.setName("Suspend database reflecting")
.setDesc("Stop reflecting database changes to storage files.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.suspendParseReplicationResult).onChange(async (value) => {
this.plugin.settings.suspendParseReplicationResult = value;
await this.plugin.saveSettings();
scheduleTask("configReload", 250, async () => {
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
// @ts-ignore
this.app.commands.executeCommandById("app:reload")
}
})
}) })
); );
new Setting(containerHatchEl) new Setting(containerHatchEl)
@@ -1731,6 +1752,16 @@ ${stringifyYaml(pluginConfig)}`;
) )
.setClass("wizardHidden"); .setClass("wizardHidden");
new Setting(containerHatchEl)
.setName("Fetch database with previous behaviour")
.setDesc("")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.doNotSuspendOnFetching).onChange(async (value) => {
this.plugin.settings.doNotSuspendOnFetching = value;
await this.plugin.saveSettings();
})
);
addScreenElement("50", containerHatchEl); addScreenElement("50", containerHatchEl);

View File

@@ -183,7 +183,7 @@ export class MessageBox extends Modal {
}) })
contentEl.createEl("h1", { text: this.title }); contentEl.createEl("h1", { text: this.title });
const div = contentEl.createDiv(); const div = contentEl.createDiv();
MarkdownRenderer.renderMarkdown(this.contentMd, div, "/", null); MarkdownRenderer.renderMarkdown(this.contentMd, div, "/", this.plugin);
const buttonSetting = new Setting(contentEl); const buttonSetting = new Setting(contentEl);
for (const button of this.buttons) { for (const button of this.buttons) {
buttonSetting.addButton((btn) => { buttonSetting.addButton((btn) => {

Submodule src/lib updated: 642efefaf1...ca61c5a64b

View File

@@ -18,7 +18,7 @@ import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/s
import { setNoticeClass } from "./lib/src/wrapper"; import { setNoticeClass } from "./lib/src/wrapper";
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin"; import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
import { addPrefix, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path"; import { addPrefix, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
import { runWithLock } from "./lib/src/lock"; import { isLockAcquired, runWithLock } from "./lib/src/lock";
import { Semaphore } from "./lib/src/semaphore"; import { Semaphore } from "./lib/src/semaphore";
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager"; import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB"; import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
@@ -240,6 +240,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
createPouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> { createPouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
if (this.settings.useIndexedDBAdapter) { if (this.settings.useIndexedDBAdapter) {
options.adapter = "indexeddb"; options.adapter = "indexeddb";
//@ts-ignore :missing def
options.purged_infos_limit = 1; options.purged_infos_limit = 1;
return new PouchDB(name + "-indexeddb", options); return new PouchDB(name + "-indexeddb", options);
} }
@@ -421,11 +422,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin
Logger(`${FLAGMD_REDFLAG3} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, LOG_LEVEL.NOTICE); Logger(`${FLAGMD_REDFLAG3} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, LOG_LEVEL.NOTICE);
await this.addOnSetup.fetchLocal(); await this.addOnSetup.fetchLocal();
await this.deleteRedFlag3(); await this.deleteRedFlag3();
if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") { if (this.settings.suspendFileWatching) {
this.settings.suspendFileWatching = false; if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") {
await this.saveSettings(); this.settings.suspendFileWatching = false;
// @ts-ignore await this.saveSettings();
this.app.commands.executeCommandById("app:reload") // @ts-ignore
this.app.commands.executeCommandById("app:reload")
}
} }
} else { } else {
this.settings.writeLogToTheFile = true; this.settings.writeLogToTheFile = true;
@@ -438,6 +441,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (this.settings.suspendFileWatching) { if (this.settings.suspendFileWatching) {
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE); Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE);
} }
if (this.settings.suspendParseReplicationResult) {
Logger("'Suspend database reflecting' turned on. Are you sure this is what you intended? Every replicated change will be postponed until disabling this option.", LOG_LEVEL.NOTICE);
}
const isInitialized = await this.initializeDatabase(false, false); const isInitialized = await this.initializeDatabase(false, false);
if (!isInitialized) { if (!isInitialized) {
//TODO:stop all sync. //TODO:stop all sync.
@@ -1317,12 +1323,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin
localStorage.setItem(lsKey, saveData); localStorage.setItem(lsKey, saveData);
} }
async loadQueuedFiles() { async loadQueuedFiles() {
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName(); if (!this.settings.suspendParseReplicationResult) {
const ids = JSON.parse(localStorage.getItem(lsKey) || "[]") as string[]; const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({ keys: ids, include_docs: true }); const ids = [...new Set(JSON.parse(localStorage.getItem(lsKey) || "[]"))] as string[];
for (const doc of ret.rows) { const ret = await this.localDatabase.allDocsRaw<EntryDoc>({ keys: ids, include_docs: true });
if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) { for (const doc of ret.rows) {
await this.parseIncomingDoc(doc.doc as PouchDB.Core.ExistingDocument<EntryBody & PouchDB.Core.AllDocsMeta>); if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) {
await this.parseIncomingDoc(doc.doc as PouchDB.Core.ExistingDocument<EntryBody & PouchDB.Core.AllDocsMeta>);
}
} }
} }
} }
@@ -1384,6 +1392,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
// It is better for your own safety, not to handle the following files // It is better for your own safety, not to handle the following files
const ignoreFiles = [ const ignoreFiles = [
"_design/replicate", "_design/replicate",
"_design/chunks",
FLAGMD_REDFLAG, FLAGMD_REDFLAG,
FLAGMD_REDFLAG2, FLAGMD_REDFLAG2,
FLAGMD_REDFLAG3 FLAGMD_REDFLAG3
@@ -1432,21 +1441,41 @@ export default class ObsidianLiveSyncPlugin extends Plugin
L1: L1:
for (const change of docsSorted) { for (const change of docsSorted) {
if (isChunk(change._id)) { if (isChunk(change._id)) {
await this.parseIncomingChunk(change); if (!this.settings.suspendParseReplicationResult) {
await this.parseIncomingChunk(change);
}
continue; continue;
} }
for (const proc of this.addOns) { if (!this.settings.suspendParseReplicationResult) {
if (await proc.parseReplicationResultItem(change)) { for (const proc of this.addOns) {
continue L1; if (await proc.parseReplicationResultItem(change)) {
continue L1;
}
} }
} }
if (change._id == SYNCINFO_ID) { if (change._id == SYNCINFO_ID) {
continue; continue;
} }
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") { if (change._id.startsWith("_design")) {
await this.parseIncomingDoc(change);
continue; continue;
} }
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
if (this.settings.suspendParseReplicationResult) {
const newQueue = {
entry: change,
missingChildren: [] as string[],
timeout: 0,
};
Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL.INFO);
this.queuedFiles.push(newQueue);
this.saveQueuedFiles();
continue;
} else {
await this.parseIncomingDoc(change);
continue;
}
}
if (change.type == "versioninfo") { if (change.type == "versioninfo") {
if (change.version > VER) { if (change.version > VER) {
this.replicator.closeReplication(); this.replicator.closeReplication();
@@ -1589,8 +1618,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin
async replicate(showMessage?: boolean) { async replicate(showMessage?: boolean) {
if (!this.isReady) return; if (!this.isReady) return;
if (isLockAcquired("cleanup")) {
Logger("Database cleaning up is in process. replication has been cancelled", LOG_LEVEL.NOTICE);
return;
}
if (this.settings.versionUpFlash != "") { if (this.settings.versionUpFlash != "") {
Logger("Open settings and check message, please.", LOG_LEVEL.NOTICE); Logger("Open settings and check message, please. replication has been cancelled.", LOG_LEVEL.NOTICE);
return; return;
} }
await this.applyBatchChange(); await this.applyBatchChange();
@@ -1600,19 +1633,41 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (!ret) { if (!ret) {
if (this.replicator.remoteLockedAndDeviceNotAccepted) { if (this.replicator.remoteLockedAndDeviceNotAccepted) {
if (this.replicator.remoteCleaned) { if (this.replicator.remoteCleaned) {
Logger(`The remote database has been cleaned up. The local database of this device also should be done.`, showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO); Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
const remoteDB = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.getIsMobile(), true);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL.NOTICE);
return false;
}
// TODO Check actually sent.
await runWithLock("cleanup", true, async () => { await runWithLock("cleanup", true, async () => {
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db); const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false); const message = `The remote database has been cleaned up.
await this.getReplicator().markRemoteResolved(this.settings); To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
However, If there are many chunks to be deleted, maybe fetching again is faster.
We will lose the history of this device if we fetch the remote database again.
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`
const CHOICE_FETCH = "Fetch again";
const CHOICE_CLEAN = "Cleanup";
const CHOICE_DISMISS = "Dismiss";
const ret = await confirmWithMessage(this, "Cleaned", message, [CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS], CHOICE_DISMISS, 30);
if (ret == CHOICE_FETCH) {
await performRebuildDB(this, "localOnly");
}
if (ret == CHOICE_CLEAN) {
const remoteDB = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.getIsMobile(), true);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL.NOTICE);
return false;
}
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
// Perform the synchronisation once.
if (await this.replicator.openReplication(this.settings, false, showMessage, true)) {
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
await this.getReplicator().markRemoteResolved(this.settings);
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO)
} else {
Logger("Replication has been cancelled. Please try it again.", showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO)
}
}
}); });
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO)
} else { } else {
const message = ` const message = `
The remote database has been rebuilt. The remote database has been rebuilt.
@@ -2164,7 +2219,7 @@ Or if you are sure know what had been happened, we can unlock the database from
setTimeout(() => { setTimeout(() => {
//resolved, check again. //resolved, check again.
this.showIfConflicted(filename); this.showIfConflicted(filename);
}, 500); }, 50);
} else if (toDelete == null) { } else if (toDelete == null) {
Logger("Leave it still conflicted"); Logger("Leave it still conflicted");
} else { } else {
@@ -2177,7 +2232,7 @@ Or if you are sure know what had been happened, we can unlock the database from
setTimeout(() => { setTimeout(() => {
//resolved, check again. //resolved, check again.
this.showIfConflicted(filename); this.showIfConflicted(filename);
}, 500); }, 50);
} }
return res(true); return res(true);
@@ -2221,7 +2276,7 @@ Or if you are sure know what had been happened, we can unlock the database from
Logger("conflict:Automatically merged, but we have to check it again"); Logger("conflict:Automatically merged, but we have to check it again");
setTimeout(() => { setTimeout(() => {
this.showIfConflicted(filename); this.showIfConflicted(filename);
}, 500); }, 50);
return; return;
} }
//there conflicts, and have to resolve ; //there conflicts, and have to resolve ;

View File

@@ -430,11 +430,12 @@ export class PeriodicProcessor {
enable(interval: number) { enable(interval: number) {
this.disable(); this.disable();
if (interval == 0) return; if (interval == 0) return;
this._timer = window.setInterval(() => this._process().then(() => { }), interval); this._timer = window.setInterval(() => this.process().then(() => { }), interval);
this._plugin.registerInterval(this._timer); this._plugin.registerInterval(this._timer);
} }
disable() { disable() {
if (this._timer) clearInterval(this._timer); if (this._timer !== undefined) window.clearInterval(this._timer);
this._timer = undefined;
} }
} }

View File

@@ -260,3 +260,14 @@ div.sls-setting-menu-btn {
.password-input > .setting-item-control >input { .password-input > .setting-item-control >input {
-webkit-text-security: disc; -webkit-text-security: disc;
} }
span.ls-mark-cr::after {
user-select: none;
content: "↲";
color: var(--text-muted);
font-size: 0.8em;
}
.deleted span.ls-mark-cr::after {
color: var(--text-on-accent);
}