mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-10 16:30:15 +00:00
Merge remote-tracking branch 'origin/main' into feat/history-search
# Conflicts: # src/lib # src/modules/features/DocumentHistory/DocumentHistoryModal.ts
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../../../deps.ts";
|
||||
import { getPathFromTFile, isValidPath } from "../../../common/utils.ts";
|
||||
import { decodeBinary, escapeStringToHTML, readString } from "../../../lib/src/string_and_binary/convert.ts";
|
||||
import { decodeBinary, readString } from "../../../lib/src/string_and_binary/convert.ts";
|
||||
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||
import {
|
||||
type DocumentID,
|
||||
@@ -96,7 +96,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (!file && id) {
|
||||
this.file = this.services.path.id2path(id);
|
||||
}
|
||||
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
|
||||
if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") {
|
||||
this.showDiff = true;
|
||||
}
|
||||
}
|
||||
@@ -153,22 +153,87 @@ export class DocumentHistoryModal extends Modal {
|
||||
return v;
|
||||
}
|
||||
|
||||
prepareContentView(usePreformatted = true) {
|
||||
this.contentView.empty();
|
||||
this.contentView.toggleClass("op-pre", usePreformatted);
|
||||
}
|
||||
|
||||
appendTextDiff(diff: [number, string][]) {
|
||||
for (const [operation, text] of diff) {
|
||||
if (operation == DIFF_DELETE) {
|
||||
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-deleted" }), text);
|
||||
} else if (operation == DIFF_EQUAL) {
|
||||
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-normal" }), text);
|
||||
} else if (operation == DIFF_INSERT) {
|
||||
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-added" }), text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendSearchHighlightedText(container: HTMLElement, text: string) {
|
||||
if (!this.searchKeyword) {
|
||||
container.appendText(text);
|
||||
return;
|
||||
}
|
||||
const escapedKeyword = this.searchKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(escapedKeyword, "gi");
|
||||
let lastIndex = 0;
|
||||
for (const match of text.matchAll(regex)) {
|
||||
const index = match.index ?? 0;
|
||||
if (index > lastIndex) {
|
||||
container.appendText(text.slice(lastIndex, index));
|
||||
}
|
||||
container.createEl("mark", { text: match[0] });
|
||||
lastIndex = index + match[0].length;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
container.appendText(text.slice(lastIndex));
|
||||
}
|
||||
}
|
||||
|
||||
appendImageDiff(baseSrc: string, overlaySrc?: string) {
|
||||
const wrap = this.contentView.createDiv({ cls: "ls-imgdiff-wrap" });
|
||||
const overlay = wrap.createDiv({ cls: "overlay" });
|
||||
overlay.createEl("img", { cls: "img-base" }, (img) => {
|
||||
img.src = baseSrc;
|
||||
});
|
||||
if (overlaySrc) {
|
||||
overlay.createEl("img", { cls: "img-overlay" }, (img) => {
|
||||
img.src = overlaySrc;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
appendDeletedNotice(usePreformatted = true) {
|
||||
const notice = "(At this revision, the file has been deleted)";
|
||||
if (usePreformatted) {
|
||||
this.contentView.appendText(`${notice}\n`);
|
||||
} else {
|
||||
this.contentView.createDiv({ text: notice });
|
||||
}
|
||||
}
|
||||
|
||||
async showExactRev(rev: string) {
|
||||
const db = this.core.localDatabase;
|
||||
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||
this.currentText = "";
|
||||
this.currentDeleted = false;
|
||||
this.prepareContentView();
|
||||
if (w === false) {
|
||||
this.currentDeleted = true;
|
||||
this.info.innerHTML = "";
|
||||
this.contentView.innerHTML = `Could not read this revision<br>(${rev})`;
|
||||
this.info.empty();
|
||||
this.contentView.appendText("Could not read this revision");
|
||||
this.contentView.createEl("br");
|
||||
this.contentView.appendText(`(${rev})`);
|
||||
} else {
|
||||
this.currentDoc = w;
|
||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||
let result = undefined;
|
||||
this.info.setText(`Modified:${new Date(w.mtime).toLocaleString()}`);
|
||||
const w1data = readDocument(w);
|
||||
this.currentDeleted = !!w.deleted;
|
||||
// this.currentText = w1data;
|
||||
if (typeof w1data == "string") {
|
||||
this.currentText = w1data;
|
||||
}
|
||||
let rendered = false;
|
||||
if (this.showDiff) {
|
||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||
@@ -176,72 +241,55 @@ export class DocumentHistoryModal extends Modal {
|
||||
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
||||
if (w2 != false) {
|
||||
if (typeof w1data == "string") {
|
||||
result = "";
|
||||
const dmp = new diff_match_patch();
|
||||
const w2data = readDocument(w2) as string;
|
||||
const diff = dmp.diff_main(w2data, w1data);
|
||||
dmp.diff_cleanupSemantic(diff);
|
||||
for (const v of diff) {
|
||||
const x1 = v[0];
|
||||
const x2 = v[1];
|
||||
let text = escapeStringToHTML(x2);
|
||||
if (this.searchKeyword) {
|
||||
const regex = new RegExp(
|
||||
`(${this.searchKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
|
||||
"gi"
|
||||
);
|
||||
text = text.replace(regex, "<mark>$1</mark>");
|
||||
}
|
||||
if (x1 == DIFF_DELETE) {
|
||||
result += "<span class='history-deleted'>" + text + "</span>";
|
||||
} else if (x1 == DIFF_EQUAL) {
|
||||
result += "<span class='history-normal'>" + text + "</span>";
|
||||
} else if (x1 == DIFF_INSERT) {
|
||||
result += "<span class='history-added'>" + text + "</span>";
|
||||
const w2data = readDocument(w2);
|
||||
if (typeof w2data == "string") {
|
||||
const dmp = new diff_match_patch();
|
||||
const diff = dmp.diff_main(w2data, w1data);
|
||||
dmp.diff_cleanupSemantic(diff);
|
||||
if (this.currentDeleted) {
|
||||
this.appendDeletedNotice();
|
||||
}
|
||||
this.appendTextDiff(diff);
|
||||
rendered = true;
|
||||
}
|
||||
result = result.replace(/\n/g, "<br>");
|
||||
} else if (isImage(this.file)) {
|
||||
const src = this.generateBlobURL("base", w1data);
|
||||
const overlay = this.generateBlobURL(
|
||||
"overlay",
|
||||
readDocument(w2) as Uint8Array<ArrayBuffer>
|
||||
);
|
||||
result = `<div class='ls-imgdiff-wrap'>
|
||||
<div class='overlay'>
|
||||
<img class='img-base' src="${src}">
|
||||
<img class='img-overlay' src='${overlay}'>
|
||||
</div>
|
||||
</div>`;
|
||||
this.contentView.removeClass("op-pre");
|
||||
this.prepareContentView(false);
|
||||
if (this.currentDeleted) {
|
||||
this.appendDeletedNotice(false);
|
||||
}
|
||||
this.appendImageDiff(src, overlay);
|
||||
rendered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == undefined) {
|
||||
if (!rendered) {
|
||||
if (typeof w1data != "string") {
|
||||
if (isImage(this.file)) {
|
||||
const src = this.generateBlobURL("base", w1data);
|
||||
result = `<div class='ls-imgdiff-wrap'>
|
||||
<div class='overlay'>
|
||||
<img class='img-base' src="${src}">
|
||||
</div>
|
||||
</div>`;
|
||||
this.contentView.removeClass("op-pre");
|
||||
this.prepareContentView(false);
|
||||
if (this.currentDeleted) {
|
||||
this.appendDeletedNotice(false);
|
||||
}
|
||||
this.appendImageDiff(src);
|
||||
} else {
|
||||
if (this.currentDeleted) {
|
||||
this.appendDeletedNotice();
|
||||
}
|
||||
this.contentView.appendText("Binary file");
|
||||
}
|
||||
} else {
|
||||
result = escapeStringToHTML(w1data);
|
||||
if (this.currentDeleted) {
|
||||
this.appendDeletedNotice();
|
||||
}
|
||||
this.appendSearchHighlightedText(this.contentView, w1data);
|
||||
}
|
||||
}
|
||||
if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file";
|
||||
|
||||
if (this.searchKeyword && typeof result == "string" && !this.showDiff) {
|
||||
const regex = new RegExp(`(${this.searchKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
|
||||
result = result.replace(regex, "<mark>$1</mark>");
|
||||
}
|
||||
|
||||
this.contentView.innerHTML =
|
||||
(this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
|
||||
}
|
||||
// Reset diff navigation after content changes
|
||||
this.resetDiffNavigation();
|
||||
@@ -272,15 +320,14 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (direction === "next") {
|
||||
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
|
||||
} else {
|
||||
this.currentDiffIndex =
|
||||
this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
|
||||
this.currentDiffIndex = this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
|
||||
}
|
||||
|
||||
const target = diffElements[this.currentDiffIndex];
|
||||
target.classList.add("diff-focused");
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
|
||||
this.diffNavIndicator.textContent = `${this.currentDiffIndex + 1}/${diffElements.length}`;
|
||||
this.diffNavIndicator.setText(`${this.currentDiffIndex + 1}/${diffElements.length}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,9 +338,9 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (this.diffNavIndicator) {
|
||||
if (this.showDiff) {
|
||||
const diffElements = this.contentView.querySelectorAll(".history-added, .history-deleted");
|
||||
this.diffNavIndicator.textContent = diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014";
|
||||
this.diffNavIndicator.setText(diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014");
|
||||
} else {
|
||||
this.diffNavIndicator.textContent = "\u2014";
|
||||
this.diffNavIndicator.setText("\u2014");
|
||||
}
|
||||
}
|
||||
this.updateDiffNavVisibility();
|
||||
@@ -317,8 +364,8 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.currentSearchIndex = -1;
|
||||
|
||||
if (!keyword) {
|
||||
this.searchResultIndicator.textContent = "";
|
||||
this.searchProgressIndicator.textContent = "";
|
||||
this.searchResultIndicator.setText("");
|
||||
this.searchProgressIndicator.setText("");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,7 +374,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
const totalRevs = this.revs_info.length;
|
||||
const end = Math.min(totalRevs, limit);
|
||||
|
||||
this.searchProgressIndicator.textContent = "Searching...";
|
||||
this.searchProgressIndicator.setText("Searching...");
|
||||
|
||||
const dmp = new diff_match_patch();
|
||||
|
||||
@@ -336,7 +383,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
const revInfo = this.revs_info[i];
|
||||
const rev = revInfo.rev;
|
||||
|
||||
this.searchProgressIndicator.textContent = `Searching ${i + 1}/${end}...`;
|
||||
this.searchProgressIndicator.setText(`Searching ${i + 1}/${end}...`);
|
||||
|
||||
const doc = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||
if (doc === false) continue;
|
||||
@@ -364,8 +411,10 @@ export class DocumentHistoryModal extends Modal {
|
||||
const diffs = dmp.diff_main(olderContent, content);
|
||||
let foundInDiff = false;
|
||||
for (const d of diffs) {
|
||||
if ((d[0] === DIFF_INSERT || d[0] === DIFF_DELETE) &&
|
||||
d[1].toLocaleLowerCase().includes(keywordLower)) {
|
||||
if (
|
||||
(d[0] === DIFF_INSERT || d[0] === DIFF_DELETE) &&
|
||||
d[1].toLocaleLowerCase().includes(keywordLower)
|
||||
) {
|
||||
foundInDiff = true;
|
||||
break;
|
||||
}
|
||||
@@ -379,16 +428,16 @@ export class DocumentHistoryModal extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
this.searchProgressIndicator.textContent = "Done";
|
||||
this.searchProgressIndicator.setText("Done");
|
||||
this.updateSearchUI();
|
||||
}
|
||||
|
||||
updateSearchUI() {
|
||||
if (this.searchResults.length === 0) {
|
||||
this.searchResultIndicator.textContent = this.searchKeyword ? "No matches found" : "";
|
||||
this.searchResultIndicator.setText(this.searchKeyword ? "No matches found" : "");
|
||||
} else {
|
||||
const current = this.currentSearchIndex >= 0 ? this.currentSearchIndex + 1 : 0;
|
||||
this.searchResultIndicator.textContent = `${current}/${this.searchResults.length} matches`;
|
||||
this.searchResultIndicator.setText(`${current}/${this.searchResults.length} matches`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +455,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.range.value = `${this.revs_info.length - 1 - match.index}`;
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
this.updateSearchUI();
|
||||
|
||||
|
||||
// If it's a diff match, make sure Highlight diff is on
|
||||
if (match.matchType === "Diff" && !this.showDiff) {
|
||||
// We could auto-enable it, but maybe just notify the user?
|
||||
@@ -425,13 +474,10 @@ export class DocumentHistoryModal extends Modal {
|
||||
const searchRow = contentEl.createDiv("");
|
||||
searchRow.addClass("op-info");
|
||||
searchRow.addClass("search-row");
|
||||
searchRow.style.display = "flex";
|
||||
searchRow.style.gap = "5px";
|
||||
searchRow.style.alignItems = "center";
|
||||
searchRow.style.marginBottom = "10px";
|
||||
searchRow.addClass("history-search-row");
|
||||
|
||||
const searchInput = searchRow.createEl("input", { type: "text", placeholder: "Search in history (last 100)..." });
|
||||
searchInput.style.flexGrow = "1";
|
||||
searchInput.addClass("history-search-input");
|
||||
searchInput.addEventListener("input", () => {
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
@@ -451,12 +497,10 @@ export class DocumentHistoryModal extends Modal {
|
||||
});
|
||||
|
||||
this.searchResultIndicator = searchRow.createEl("span", { text: "" });
|
||||
this.searchResultIndicator.style.fontSize = "0.8em";
|
||||
this.searchResultIndicator.style.minWidth = "80px";
|
||||
this.searchResultIndicator.addClass("history-search-result-indicator");
|
||||
|
||||
this.searchProgressIndicator = searchRow.createEl("span", { text: "" });
|
||||
this.searchProgressIndicator.style.fontSize = "0.8em";
|
||||
this.searchProgressIndicator.style.color = "var(--text-muted)";
|
||||
this.searchProgressIndicator.addClass("history-search-progress-indicator");
|
||||
|
||||
const divView = contentEl.createDiv("");
|
||||
divView.addClass("op-flex");
|
||||
@@ -473,31 +517,24 @@ export class DocumentHistoryModal extends Modal {
|
||||
const diffOptionsRow = contentEl.createDiv("");
|
||||
diffOptionsRow.addClass("op-info");
|
||||
diffOptionsRow.addClass("diff-options-row");
|
||||
diffOptionsRow.style.display = "flex";
|
||||
diffOptionsRow.style.justifyContent = "space-between";
|
||||
diffOptionsRow.style.alignItems = "center";
|
||||
diffOptionsRow.addClass("history-diff-options-row");
|
||||
|
||||
const highlightDiffContainer = diffOptionsRow.createDiv("");
|
||||
highlightDiffContainer.style.display = "flex";
|
||||
highlightDiffContainer.style.alignItems = "center";
|
||||
highlightDiffContainer.addClass("history-highlight-diff-container");
|
||||
|
||||
highlightDiffContainer.createEl("label", {}, (label) => {
|
||||
label.style.display = "flex";
|
||||
label.style.alignItems = "center";
|
||||
label.style.gap = "4px";
|
||||
label.appendChild(
|
||||
createEl("input", { type: "checkbox" }, (checkbox) => {
|
||||
if (this.showDiff) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
this.showDiff = checkbox.checked;
|
||||
localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : "");
|
||||
this.updateDiffNavVisibility();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
})
|
||||
);
|
||||
label.addClass("history-highlight-diff-label");
|
||||
label.createEl("input", { type: "checkbox" }, (checkbox) => {
|
||||
if (this.showDiff) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
this.showDiff = checkbox.checked;
|
||||
this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null);
|
||||
this.updateDiffNavVisibility();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
});
|
||||
label.appendText("Highlight diff");
|
||||
});
|
||||
|
||||
@@ -505,7 +542,6 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.diffNavContainer = diffOptionsRow.createDiv("");
|
||||
this.diffNavContainer.addClass("diff-nav");
|
||||
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
|
||||
this.diffNavContainer.style.marginLeft = "auto";
|
||||
|
||||
this.diffNavContainer.createEl("button", { text: "\u25B2 Prev" }, (e) => {
|
||||
e.addClass("diff-nav-btn");
|
||||
|
||||
Reference in New Issue
Block a user