mirror of
https://github.com/gustjose/obsidian-syncthing-manager.git
synced 2026-07-22 06:40:35 +00:00
feat: Implement visual conflict resolution with CodeMirror 6
This commit is contained in:
parent
48d770f6d9
commit
5cf2ccc23a
8 changed files with 505 additions and 160 deletions
101
package-lock.json
generated
101
package-lock.json
generated
|
|
@ -8,6 +8,11 @@
|
|||
"name": "syncthing-manager",
|
||||
"version": "1.3.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/merge": "^6.12.0",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/view": "^6.39.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"builtin-modules": "3.3.0",
|
||||
|
|
@ -77,6 +82,54 @@
|
|||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
|
||||
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/merge": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.12.0.tgz",
|
||||
"integrity": "sha512-o+36bbapcEHf4Ux75pZ4CKjMBUd14parA0uozvWVlacaT+uxaA3DDefEvWYjngsKU+qsrDe/HOOfsw0Q72pLjA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"style-mod": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
|
||||
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.15",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz",
|
||||
"integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||
|
|
@ -885,6 +938,36 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
|
||||
"integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
|
||||
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@microsoft/eslint-plugin-sdl": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/eslint-plugin-sdl/-/eslint-plugin-sdl-1.1.0.tgz",
|
||||
|
|
@ -1912,6 +1995,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -5281,6 +5370,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
|
@ -5808,6 +5903,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -37,5 +37,10 @@
|
|||
"tslib": "2.4.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/merge": "^6.12.0",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/view": "^6.39.15"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,68 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// --- CONFIGURAÇÃO ---
|
||||
const VAULT_PATH = 'C:/Users/Gustavo/Android/dev-obsidian';
|
||||
const VAULT_PATH = "C:/Users/Gustavo/Android/dev-obsidian";
|
||||
|
||||
const BASE_FILENAME = 'TesteConflito.md';
|
||||
const BASE_FILENAME = "TesteConflito.md";
|
||||
|
||||
function generateTimestamp() {
|
||||
const now = new Date();
|
||||
// Formata para YYYYMMDD-HHMMSS (Padrão do Syncthing)
|
||||
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const time = now.toTimeString().slice(0, 8).replace(/:/g, '');
|
||||
return `${date}-${time}`;
|
||||
const now = new Date();
|
||||
// Formata para YYYYMMDD-HHMMSS (Padrão do Syncthing)
|
||||
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
||||
const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
|
||||
return `${date}-${time}`;
|
||||
}
|
||||
|
||||
function createConflict() {
|
||||
if (!fs.existsSync(VAULT_PATH)) {
|
||||
console.error(`❌ Erro: O caminho do cofre não existe: ${VAULT_PATH}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!fs.existsSync(VAULT_PATH)) {
|
||||
console.error(`❌ Erro: O caminho do cofre não existe: ${VAULT_PATH}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 1. Cria o arquivo Original (se não existir ou sobrescreve)
|
||||
const originalPath = path.join(VAULT_PATH, BASE_FILENAME);
|
||||
const originalContent = `# Nota Original\n\nEsta é a versão que estava no disco.\n\n- Item A\n- Item B\n- Item C`;
|
||||
|
||||
fs.writeFileSync(originalPath, originalContent);
|
||||
console.log(`✅ Arquivo original criado: ${BASE_FILENAME}`);
|
||||
// 1. Cria o arquivo Original (se não existir ou sobrescreve)
|
||||
const originalPath = path.join(VAULT_PATH, BASE_FILENAME);
|
||||
const originalContent = `# Planejamento de Reunião Diária
|
||||
|
||||
// 2. Cria o arquivo de Conflito
|
||||
const timestamp = generateTimestamp();
|
||||
const conflictName = `TesteConflito.sync-conflict-${timestamp}-ScriptDev.md`;
|
||||
const conflictPath = path.join(VAULT_PATH, conflictName);
|
||||
|
||||
const conflictContent = `# Nota em Conflito\n\nEsta é a versão que veio de outro dispositivo (Conflito).\n\n- Item A\n- Item B (Modificado)\n- Item D (Novo)`;
|
||||
## Tópicos do Dia (Versão Computador)
|
||||
1. Discutir as metas da Sprint 42.
|
||||
2. Analisar os bugs críticos relatados pelos clientes no final de semana.
|
||||
3. Revisar a arquitetura do novo módulo de autenticação.
|
||||
|
||||
fs.writeFileSync(conflictPath, conflictContent);
|
||||
console.log(`⚠️ Arquivo de conflito criado: ${conflictName}`);
|
||||
|
||||
console.log('\n👉 Agora abra o Obsidian e verifique o painel lateral do plugin!');
|
||||
## Observações Gerais
|
||||
Hoje o dia está bastante chuvoso. Recomendo que a equipe faça home office se possível. Não esqueçam de bater o ponto no sistema novo.
|
||||
|
||||
---
|
||||
*Gerado via Syncthing Manager Tests*`;
|
||||
|
||||
fs.writeFileSync(originalPath, originalContent);
|
||||
console.log(`✅ Arquivo original criado: ${BASE_FILENAME}`);
|
||||
|
||||
// 2. Cria o arquivo de Conflito
|
||||
const timestamp = generateTimestamp();
|
||||
const conflictName = `TesteConflito.sync-conflict-${timestamp}-ScriptDev.md`;
|
||||
const conflictPath = path.join(VAULT_PATH, conflictName);
|
||||
|
||||
const conflictContent = `# Planejamento de Reunião Diária
|
||||
|
||||
## Tópicos do Dia (Versão Celular)
|
||||
1. Discutir as metas da Sprint 42.
|
||||
2. URGE: Adicionar pauta sobre a queda do servidor de banco de dados hoje cedo!
|
||||
3. Revisar a arquitetura do novo módulo de autenticação.
|
||||
4. Alinhar folgas do mês que vem.
|
||||
|
||||
## Observações Gerais
|
||||
O Sol está raiando lá fora! Lembrem-se de passar protetor solar se vierem de bicicleta. Não esqueçam de bater o ponto no sistema novo (Aquele lá, o portal azul).
|
||||
|
||||
---
|
||||
*Gerado via Celular (Conflito)*`;
|
||||
|
||||
fs.writeFileSync(conflictPath, conflictContent);
|
||||
console.log(`⚠️ Arquivo de conflito criado: ${conflictName}`);
|
||||
|
||||
console.log(
|
||||
"\n👉 Agora abra o Obsidian e verifique o painel lateral do plugin!",
|
||||
);
|
||||
}
|
||||
|
||||
createConflict();
|
||||
createConflict();
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ export default {
|
|||
btn_keep_conflict: "Use conflict version",
|
||||
tooltip_keep_conflict:
|
||||
"Overwrites your local file with the conflict version (right side).",
|
||||
btn_use_this_version: "Use this version",
|
||||
diff_instructions:
|
||||
"Use the arrows in the middle to transfer changes to the original file. To keep one version entirely, click 'Use this version' above it.",
|
||||
btn_save_merge: "Save merge",
|
||||
|
||||
// Diff View
|
||||
diff_original_header: "Current file (original)",
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ export default {
|
|||
btn_keep_conflict: "Usar Versão do Conflito",
|
||||
tooltip_keep_conflict:
|
||||
"Substitui o seu arquivo local pela versão do conflito (da direita).",
|
||||
btn_use_this_version: "Usar esta versão",
|
||||
diff_instructions:
|
||||
"Use as setas no meio para transferir alterações para o arquivo original. Para manter uma das versões integralmente, clique em 'Usar esta versão' acima dela.",
|
||||
btn_save_merge: "Salvar Merge",
|
||||
|
||||
// Visualização de Diferença
|
||||
diff_original_header: "Arquivo Atual (Original)",
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ export default {
|
|||
btn_keep_conflict: "Использовать конфликтную",
|
||||
tooltip_keep_conflict:
|
||||
"Заменяет Ваши локальные файлы на конфликтные (правая сторона).",
|
||||
btn_use_this_version: "Использовать эту версию",
|
||||
diff_instructions:
|
||||
"Используйте стрелки посередине для переноса изменений в исходный файл. Чтобы полностью сохранить одну из версий, нажмите 'Использовать эту версию' над ней.",
|
||||
btn_save_merge: "Сохранить слияние",
|
||||
|
||||
// Diff View
|
||||
diff_original_header: "Текущий файл (Оригинал)",
|
||||
|
|
|
|||
|
|
@ -1,125 +1,221 @@
|
|||
import { App, Modal, TFile, ButtonComponent } from 'obsidian';
|
||||
import { ConflictManager, ConflictFile } from '../services/conflict-manager';
|
||||
import { t } from '../lang/lang';
|
||||
import { App, Modal, TFile, ButtonComponent, Notice } from "obsidian";
|
||||
import { ConflictManager, ConflictFile } from "../services/conflict-manager";
|
||||
import { t } from "../lang/lang";
|
||||
import { MergeView } from "@codemirror/merge";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
||||
export class ConflictModal extends Modal {
|
||||
manager: ConflictManager;
|
||||
conflicts: ConflictFile[];
|
||||
onCloseCallback: () => void;
|
||||
manager: ConflictManager;
|
||||
conflicts: ConflictFile[];
|
||||
onCloseCallback: () => void;
|
||||
|
||||
constructor(app: App, manager: ConflictManager, onCloseCallback: () => void) {
|
||||
super(app);
|
||||
this.manager = manager;
|
||||
this.onCloseCallback = onCloseCallback;
|
||||
this.conflicts = manager.getConflicts();
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
manager: ConflictManager,
|
||||
onCloseCallback: () => void,
|
||||
) {
|
||||
super(app);
|
||||
this.manager = manager;
|
||||
this.onCloseCallback = onCloseCallback;
|
||||
this.conflicts = manager.getConflicts();
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
this.modalEl.addClass('st-modal-wide');
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl('h2', { text: `${t('modal_conflict_title')} (${this.conflicts.length})` });
|
||||
this.modalEl.addClass("st-modal-wide");
|
||||
|
||||
if (this.conflicts.length === 0) {
|
||||
contentEl.createEl('p', { text: t('modal_conflict_empty') });
|
||||
return;
|
||||
}
|
||||
contentEl.createEl("h2", {
|
||||
text: `${t("modal_conflict_title")} (${this.conflicts.length})`,
|
||||
});
|
||||
|
||||
contentEl.createEl('p', { text: t('modal_conflict_desc') });
|
||||
if (this.conflicts.length === 0) {
|
||||
contentEl.createEl("p", { text: t("modal_conflict_empty") });
|
||||
return;
|
||||
}
|
||||
|
||||
this.conflicts.forEach(conflict => {
|
||||
const container = contentEl.createDiv({ cls: 'conflict-item-box' });
|
||||
|
||||
container.createEl('h4', {
|
||||
text: conflict.baseName,
|
||||
cls: 'st-conflict-title'
|
||||
});
|
||||
|
||||
container.createEl('div', { text: `Data: ${conflict.date}`, cls: 'conflict-meta' });
|
||||
container.createEl('div', { text: conflict.path, cls: 'conflict-meta-path' });
|
||||
contentEl.createEl("p", { text: t("modal_conflict_desc") });
|
||||
|
||||
const previewContainer = container.createDiv();
|
||||
const actionsContainer = container.createDiv({ cls: 'conflict-actions' });
|
||||
this.conflicts.forEach((conflict) => {
|
||||
const container = contentEl.createDiv({ cls: "conflict-item-box" });
|
||||
|
||||
new ButtonComponent(actionsContainer)
|
||||
.setButtonText(t('btn_compare'))
|
||||
.setIcon('eye')
|
||||
.onClick(async () => {
|
||||
if (previewContainer.hasChildNodes()) {
|
||||
previewContainer.empty();
|
||||
return;
|
||||
}
|
||||
await this.renderContentPreview(previewContainer, conflict);
|
||||
});
|
||||
container.createEl("h4", {
|
||||
text: conflict.baseName,
|
||||
cls: "st-conflict-title",
|
||||
});
|
||||
|
||||
actionsContainer.createSpan({ cls: 'st-flex-grow' });
|
||||
container.createEl("div", {
|
||||
text: `Data: ${conflict.date}`,
|
||||
cls: "conflict-meta",
|
||||
});
|
||||
container.createEl("div", {
|
||||
text: conflict.path,
|
||||
cls: "conflict-meta-path",
|
||||
});
|
||||
|
||||
new ButtonComponent(actionsContainer)
|
||||
.setButtonText(t('btn_keep_original'))
|
||||
.setTooltip(t('tooltip_keep_original'))
|
||||
.onClick(() => {
|
||||
this.manager.deleteConflict(conflict)
|
||||
.then(() => this.refresh())
|
||||
.catch(console.error);
|
||||
});
|
||||
const previewContainer = container.createDiv();
|
||||
const actionsContainer = container.createDiv({
|
||||
cls: "conflict-actions",
|
||||
});
|
||||
|
||||
new ButtonComponent(actionsContainer)
|
||||
.setButtonText(t('btn_keep_conflict'))
|
||||
.setCta()
|
||||
.setTooltip(t('tooltip_keep_conflict'))
|
||||
.onClick(() => {
|
||||
this.manager.acceptConflict(conflict)
|
||||
.then(() => this.refresh())
|
||||
.catch(console.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
new ButtonComponent(actionsContainer)
|
||||
.setButtonText(t("btn_compare"))
|
||||
.setIcon("eye")
|
||||
.onClick(async () => {
|
||||
if (previewContainer.hasChildNodes()) {
|
||||
previewContainer.empty();
|
||||
return;
|
||||
}
|
||||
await this.renderContentPreview(previewContainer, conflict);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async renderContentPreview(container: HTMLElement, conflict: ConflictFile) {
|
||||
container.empty();
|
||||
const diffWrapper = container.createDiv({ cls: 'st-diff-container' });
|
||||
async renderContentPreview(container: HTMLElement, conflict: ConflictFile) {
|
||||
container.empty();
|
||||
|
||||
const leftBox = diffWrapper.createDiv({ cls: 'st-diff-box' });
|
||||
leftBox.createDiv({ cls: 'st-diff-header', text: t('diff_original_header') });
|
||||
const leftContent = leftBox.createDiv({ cls: 'st-diff-content st-diff-original' });
|
||||
leftContent.setText(t('diff_loading'));
|
||||
const loadingEl = container.createEl("div", {
|
||||
text: t("diff_loading"),
|
||||
cls: "st-diff-loading",
|
||||
});
|
||||
let originalText = "";
|
||||
let conflictText = "";
|
||||
|
||||
const rightBox = diffWrapper.createDiv({ cls: 'st-diff-box' });
|
||||
rightBox.createDiv({ cls: 'st-diff-header', text: `${t('diff_conflict_header')} (${conflict.date})` });
|
||||
const rightContent = rightBox.createDiv({ cls: 'st-diff-content st-diff-conflict' });
|
||||
rightContent.setText(t('diff_loading'));
|
||||
try {
|
||||
conflictText = await this.app.vault.read(conflict.file);
|
||||
|
||||
try {
|
||||
const conflictText = await this.app.vault.read(conflict.file);
|
||||
rightContent.setText(conflictText);
|
||||
const originalPath = conflict.path.replace(
|
||||
conflict.file.name,
|
||||
conflict.baseName,
|
||||
);
|
||||
const originalFile =
|
||||
this.app.vault.getAbstractFileByPath(originalPath);
|
||||
|
||||
const originalPath = conflict.path.replace(conflict.file.name, conflict.baseName);
|
||||
const originalFile = this.app.vault.getAbstractFileByPath(originalPath);
|
||||
if (originalFile instanceof TFile) {
|
||||
originalText = await this.app.vault.read(originalFile);
|
||||
} else {
|
||||
originalText = t("diff_original_missing");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
loadingEl.setText(t("diff_read_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (originalFile instanceof TFile) {
|
||||
const originalText = await this.app.vault.read(originalFile);
|
||||
leftContent.setText(originalText);
|
||||
} else {
|
||||
leftContent.setText(t('diff_original_missing'));
|
||||
}
|
||||
loadingEl.remove();
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
leftContent.setText(t('diff_read_error'));
|
||||
rightContent.setText(t('diff_read_error'));
|
||||
}
|
||||
}
|
||||
// 1. Instructions
|
||||
container.createEl("p", {
|
||||
text: t("diff_instructions"),
|
||||
cls: "st-diff-instructions",
|
||||
});
|
||||
|
||||
refresh() {
|
||||
this.conflicts = this.manager.getConflicts();
|
||||
this.onOpen();
|
||||
}
|
||||
// 2. Headers and Keep Buttons
|
||||
const headersContainer = container.createDiv({
|
||||
cls: "st-diff-headers",
|
||||
});
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.onCloseCallback();
|
||||
}
|
||||
}
|
||||
const leftHeader = headersContainer.createDiv({
|
||||
cls: "st-diff-header-box",
|
||||
});
|
||||
const leftBtn = new ButtonComponent(leftHeader)
|
||||
.setButtonText(t("btn_use_this_version"))
|
||||
.setTooltip(t("tooltip_keep_original"))
|
||||
.onClick(() => {
|
||||
this.manager
|
||||
.deleteConflict(conflict)
|
||||
.then(() => this.refresh())
|
||||
.catch(console.error);
|
||||
});
|
||||
leftBtn.buttonEl.addClass("st-btn-accent");
|
||||
|
||||
const rightHeader = headersContainer.createDiv({
|
||||
cls: "st-diff-header-box",
|
||||
});
|
||||
const rightBtn = new ButtonComponent(rightHeader)
|
||||
.setButtonText(t("btn_use_this_version"))
|
||||
.setTooltip(t("tooltip_keep_conflict"))
|
||||
.onClick(() => {
|
||||
this.manager
|
||||
.acceptConflict(conflict)
|
||||
.then(() => this.refresh())
|
||||
.catch(console.error);
|
||||
});
|
||||
rightBtn.buttonEl.addClass("st-btn-accent");
|
||||
|
||||
// 3. Diff View Wrapper
|
||||
const diffWrapper = container.createDiv({
|
||||
cls: "st-diff-view-wrapper",
|
||||
});
|
||||
|
||||
const mergeView = new MergeView({
|
||||
a: {
|
||||
doc: originalText,
|
||||
extensions: [EditorView.lineWrapping],
|
||||
},
|
||||
b: {
|
||||
doc: conflictText,
|
||||
extensions: [
|
||||
EditorView.lineWrapping,
|
||||
EditorState.readOnly.of(true),
|
||||
],
|
||||
},
|
||||
parent: diffWrapper,
|
||||
revertControls: "b-to-a", // Permite puxar alterações do Conflito para o Original
|
||||
});
|
||||
|
||||
// 4. Save Merge Control
|
||||
const diffControlsContainer = container.createDiv({
|
||||
cls: "st-diff-controls",
|
||||
});
|
||||
|
||||
diffControlsContainer.createEl("span", {
|
||||
text: "Original (A) ↔ Conflito (B)",
|
||||
cls: "st-diff-legend",
|
||||
});
|
||||
|
||||
diffControlsContainer.createSpan({ cls: "st-flex-grow" });
|
||||
|
||||
const saveMergeBtn = new ButtonComponent(diffControlsContainer)
|
||||
.setButtonText(t("btn_save_merge"))
|
||||
.setTooltip(
|
||||
"Sobrescrever original com estado atual da esquerda e remover o conflito",
|
||||
)
|
||||
.setCta();
|
||||
|
||||
saveMergeBtn.onClick(async () => {
|
||||
const mergedText = mergeView.a.state.doc.toString();
|
||||
const originalPath = conflict.path.replace(
|
||||
conflict.file.name,
|
||||
conflict.baseName,
|
||||
);
|
||||
const originalFile =
|
||||
this.app.vault.getAbstractFileByPath(originalPath);
|
||||
|
||||
try {
|
||||
if (originalFile instanceof TFile) {
|
||||
await this.app.vault.modify(originalFile, mergedText);
|
||||
await this.manager.deleteConflict(conflict);
|
||||
new Notice(`Merge salvo: ${conflict.baseName}`);
|
||||
this.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
new Notice("Erro ao salvar arquivo merged.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.conflicts = this.manager.getConflicts();
|
||||
this.onOpen();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.onCloseCallback();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
160
styles.css
160
styles.css
|
|
@ -255,45 +255,151 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Diff View */
|
||||
.st-diff-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
/* Diff View & Merge View */
|
||||
.st-diff-instructions {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
padding-top: 10px;
|
||||
}
|
||||
.st-diff-box {
|
||||
.st-diff-headers {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.st-diff-header-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.st-diff-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
.st-btn-accent {
|
||||
color: var(--text-on-accent) !important;
|
||||
}
|
||||
.st-diff-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.cm-changeGutter {
|
||||
background-color: var(--background-modifier-border) !important;
|
||||
}
|
||||
|
||||
.st-diff-legend {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8em;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
.st-diff-content {
|
||||
background-color: var(--background-primary-alt);
|
||||
.st-diff-loading {
|
||||
margin: 15px 0;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.st-diff-view-wrapper {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--background-primary-alt);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* CodeMirror Merge Styles overrides to match Obsidian Theme */
|
||||
.cm-mergeView {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.8em;
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
}
|
||||
.st-diff-original {
|
||||
border-left: 3px solid var(--text-success);
|
||||
.cm-mergeView .cm-editor {
|
||||
background-color: var(--background-primary) !important;
|
||||
color: var(--text-normal) !important;
|
||||
font-family: var(--font-monospace) !important;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.st-diff-conflict {
|
||||
border-left: 3px solid var(--text-warning);
|
||||
|
||||
.cm-merge-a .cm-content {
|
||||
border-right: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* Highlight lines */
|
||||
.cm-merge-deleted {
|
||||
background-color: rgba(var(--color-red-rgb), 0.15) !important;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.cm-merge-inserted {
|
||||
background-color: rgba(var(--color-green-rgb), 0.15) !important;
|
||||
}
|
||||
|
||||
/* Background chunks */
|
||||
.cm-mergeView .cm-deletedChunk {
|
||||
background-color: rgba(var(--color-red-rgb), 0.08) !important;
|
||||
}
|
||||
.cm-mergeView .cm-insertedChunk {
|
||||
background-color: rgba(var(--color-green-rgb), 0.08) !important;
|
||||
}
|
||||
|
||||
/* Revert buttons spacer */
|
||||
.cm-merge-spacer {
|
||||
background-color: var(--background-secondary) !important;
|
||||
}
|
||||
|
||||
.cm-merge-revert {
|
||||
background-color: var(--background-modifier-form-field) !important;
|
||||
border: 1px solid var(--background-modifier-border) !important;
|
||||
color: var(--text-muted) !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cm-merge-revert button {
|
||||
height: 2.8vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cm-mergeView {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Mobile Responsiveness (Celulares) */
|
||||
@media (max-width: 600px) {
|
||||
.st-diff-instructions {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.st-diff-header-box {
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.st-diff-header-box button {
|
||||
font-size: 0.8em;
|
||||
padding: 4px 8px;
|
||||
height: auto;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.st-diff-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.st-diff-legend {
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.st-diff-controls button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Ajuste forçado para displays muito pequenos do CodeMirror MergeView */
|
||||
.cm-mergeView .cm-editor {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.st-history-container {
|
||||
|
|
|
|||
Loading…
Reference in a new issue