feat: Implement visual conflict resolution with CodeMirror 6

This commit is contained in:
Gustavo Carreiro 2026-02-25 20:12:01 -03:00
parent 48d770f6d9
commit 5cf2ccc23a
8 changed files with 505 additions and 160 deletions

101
package-lock.json generated
View file

@ -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",

View file

@ -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"
}
}

View file

@ -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 fora! Lembrem-se de passar protetor solar se vierem de bicicleta. Não esqueçam de bater o ponto no sistema novo (Aquele , 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();

View file

@ -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)",

View file

@ -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)",

View file

@ -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: "Текущий файл (Оригинал)",

View file

@ -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();
}
}

View file

@ -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 {