Skip to content

Commit be635de

Browse files
committed
feat(diff): Add diff view for each files about to sync.
1 parent 7f30ee1 commit be635de

20 files changed

+2877
-278
lines changed

esbuild.config.mjs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import esbuild from "esbuild";
33
import process from "process";
44
import builtins from 'builtin-modules'
55
import cssModulesPlugin from "esbuild-css-modules-plugin"
6-
6+
import {sassPlugin} from 'esbuild-sass-plugin'
7+
import fse from 'fs-extra';
8+
import chalk from 'chalk';
79

810
const banner = `/*
911
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
@@ -81,3 +83,38 @@ esbuild
8183
},
8284
})
8385
.catch(() => process.exit(1));
86+
87+
88+
89+
esbuild
90+
.build({
91+
entryPoints: [
92+
'src/styles/styles.js'
93+
],
94+
plugins: [
95+
sassPlugin(),
96+
cssModulesPlugin({
97+
force: true,
98+
emitDeclarationFile: true,
99+
localsConvention: 'camelCaseOnly',
100+
namedExports: true,
101+
inject: false
102+
})
103+
],
104+
format: "iife",
105+
outdir: '.',
106+
watch: !prod,
107+
minify: true,
108+
bundle: true,
109+
keepNames: true, // 保留函数和变量的原始名称
110+
})
111+
.then(result => {
112+
fse.rmSync('./styles.js')
113+
if (result.errors?.length > 0) {
114+
console.log(chalk.red('bundle style failed: ', result.errors));
115+
} else {
116+
console.log(chalk.green('bundle style success'));
117+
}
118+
})
119+
120+

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@microsoft/microsoft-graph-types": "^2.19.0",
2828
"@types/chai": "^4.3.1",
2929
"@types/chai-as-promised": "^7.1.5",
30+
"@types/diff": "^5.0.8",
3031
"@types/js-beautify": "^1.13.3",
3132
"@types/jsdom": "^16.2.14",
3233
"@types/lodash": "^4.14.182",
@@ -45,6 +46,7 @@
4546
"chai-as-promised": "^7.1.1",
4647
"chalk": "^5.3.0",
4748
"cross-env": "^7.0.3",
49+
"css-minify": "^2.0.0",
4850
"dotenv": "^16.0.0",
4951
"downloadjs": "^1.4.7",
5052
"electron": "^18.3.15",
@@ -56,6 +58,7 @@
5658
"jsdom": "^19.0.0",
5759
"mocha": "^9.2.2",
5860
"prettier": "^2.6.2",
61+
"sass": "^1.69.5",
5962
"ts-loader": "^9.2.9",
6063
"ts-node": "^10.7.0",
6164
"tslib": "2.4.0",
@@ -86,6 +89,8 @@
8689
"builtin-modules": "3.3.0",
8790
"classnames": "^2.3.2",
8891
"crypto-browserify": "^3.12.0",
92+
"diff": "^5.1.0",
93+
"diff2html": "^3.4.45",
8994
"dropbox": "^10.28.0",
9095
"electron": "^18.3.15",
9196
"emoji-regex": "^10.1.0",

pnpm-lock.yaml

Lines changed: 1024 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/diff/abstract_diff_view.ts

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { createTwoFilesPatch } from 'diff';
2+
import { Diff2HtmlConfig, html } from 'diff2html';
3+
// @ts-ignore
4+
import { App, Modal, TFile, Notice, setTooltip } from 'obsidian';
5+
import type { vItem, vRecoveryItem, vSyncItem } from './interfaces';
6+
import type InvioPlugin from '../main';
7+
import type { LangType, LangTypeAndAuto, TransItemType } from "../i18n";
8+
9+
type TviewOutputFormat = `side-by-side` | `line-by-line`
10+
11+
export default abstract class DiffView extends Modal {
12+
plugin: InvioPlugin;
13+
app: App;
14+
file: TFile;
15+
leftVList: vItem[];
16+
rightVList: vItem[];
17+
leftActive: number;
18+
rightActive: number;
19+
rightContent: string;
20+
leftContent: string;
21+
leftName: string;
22+
rightName: string;
23+
syncHistoryContentContainer: HTMLElement;
24+
leftHistory: HTMLElement[];
25+
rightHistory: HTMLElement[];
26+
htmlConfig: Diff2HtmlConfig;
27+
ids: { left: number; right: number };
28+
silentClose: boolean;
29+
viewOutputFormat: TviewOutputFormat;
30+
fileChangedHook: (file: TFile) => void;
31+
cancelHook: () => void;
32+
33+
constructor(plugin: InvioPlugin, app: App, file: TFile, changedHook: (file: TFile) => void, cancelHook: () => void) {
34+
super(app);
35+
this.plugin = plugin;
36+
this.app = app;
37+
this.file = file;
38+
this.modalEl.addClasses(['mod-sync-history', 'diff']);
39+
this.leftVList = [];
40+
this.rightVList = [];
41+
this.rightActive = 0;
42+
this.leftActive = 1;
43+
this.leftName = '';
44+
this.rightName = '';
45+
this.rightContent = '';
46+
this.leftContent = '';
47+
this.ids = { left: 0, right: 0 };
48+
this.fileChangedHook = changedHook;
49+
this.cancelHook = cancelHook;
50+
this.silentClose = false;
51+
this.viewOutputFormat = 'side-by-side';
52+
53+
// @ts-ignore
54+
this.leftHistory = [null];
55+
// @ts-ignore
56+
this.rightHistory = [null];
57+
this.htmlConfig = {
58+
drawFileList: true,
59+
diffStyle: 'word',
60+
matchWordsThreshold: 0.25,
61+
outputFormat: this.viewOutputFormat,
62+
rawTemplates: {
63+
'line-by-line-file-diff': `<div id="{{fileHtmlId}}" class="d2h-file-wrapper" data-lang="{{file.language}}">
64+
<div class="d2h-file-header">
65+
{{{filePath}}}
66+
</div>
67+
<div class="d2h-file-diff">
68+
<div class="d2h-code-wrapper">
69+
<table class="d2h-diff-table">
70+
<tbody class="d2h-diff-tbody">
71+
{{{diffs}}}
72+
</tbody>
73+
</table>
74+
</div>
75+
</div>
76+
</div>`,
77+
'side-by-side-file-diff': `<div id="{{fileHtmlId}}" class="d2h-file-wrapper" data-lang="{{file.language}}">
78+
<div class="d2h-file-header">
79+
{{{filePath}}}
80+
</div>
81+
<div class="d2h-files-diff">
82+
<div class="d2h-file-side-diff">
83+
<div class="d2h-code-title">${this.leftName}</div>
84+
<div class="d2h-code-wrapper">
85+
<table class="d2h-diff-table">
86+
<tbody class="d2h-diff-tbody">
87+
{{{diffs.left}}}
88+
</tbody>
89+
</table>
90+
</div>
91+
</div>
92+
<div class="d2h-file-side-diff">
93+
<div class="d2h-code-title">${this.rightName}</div>
94+
<div class="d2h-code-wrapper">
95+
<table class="d2h-diff-table">
96+
<tbody class="d2h-diff-tbody">
97+
{{{diffs.right}}}
98+
</tbody>
99+
</table>
100+
</div>
101+
</div>
102+
</div>
103+
</div>`
104+
}
105+
};
106+
this.containerEl.addClass('diff');
107+
// @ts-ignore
108+
const contentParent = this.contentEl.createDiv({
109+
cls: ['sync-history-content-container-parent'],
110+
});
111+
112+
const topAction = contentParent.createDiv({
113+
cls: 'sync-history-content-container-top'
114+
});
115+
116+
const viewChangeBtn = topAction.createDiv({
117+
cls: ['view-action', 'btn'],
118+
text: this.t('view_change_btn')
119+
})
120+
121+
const diffResetBtn = topAction.createDiv({
122+
cls: ['view-action', 'btn'],
123+
text: this.t('diff_reset_btn')
124+
})
125+
setTooltip(diffResetBtn, 'Click to replace the file with online version', {
126+
placement: 'top',
127+
});
128+
diffResetBtn.addEventListener('click', e => {
129+
e.preventDefault();
130+
this.changeFileAndCloseModal(this.leftContent);
131+
132+
new Notice(
133+
`The ${this.file.basename} file has been overwritten with the online remote version.`
134+
);
135+
})
136+
setTooltip(viewChangeBtn, 'Click to change diff view', {
137+
placement: 'top',
138+
});
139+
viewChangeBtn.addEventListener('click', e => {
140+
e.preventDefault();
141+
this.viewOutputFormat = ('line-by-line' === this.viewOutputFormat) ? 'side-by-side' : 'line-by-line';
142+
console.log('diff styles changed to ', this.viewOutputFormat)
143+
this.reload({
144+
outputFormat: this.viewOutputFormat
145+
});
146+
})
147+
148+
this.syncHistoryContentContainer = contentParent.createDiv({
149+
cls: ['sync-history-content-container', 'diff'],
150+
})
151+
}
152+
153+
onClose(): void {
154+
if (!this.silentClose) {
155+
this.cancelHook && this.cancelHook()
156+
}
157+
}
158+
159+
reload(config?: Partial<Diff2HtmlConfig>) {
160+
this.syncHistoryContentContainer.innerHTML =
161+
this.getDiff(config) as string;
162+
}
163+
abstract getInitialVersions(): Promise<void | boolean>;
164+
165+
abstract appendVersions(): void;
166+
167+
public getDiff(config?: Diff2HtmlConfig): string {
168+
// the second type is needed for the Git view, it reimplements getDiff
169+
// get diff
170+
const uDiff = createTwoFilesPatch(
171+
this.file.basename,
172+
this.file.basename,
173+
this.leftContent,
174+
this.rightContent
175+
);
176+
177+
// create HTML from diff
178+
const diff = html(uDiff, {
179+
...this.htmlConfig,
180+
...(config || {})
181+
});
182+
return diff;
183+
}
184+
185+
public makeHistoryLists(warning: string): void {
186+
// create both history lists
187+
this.rightHistory = this.createHistory(this.contentEl);
188+
}
189+
190+
public t(x: TransItemType, vars?: any) {
191+
return this.plugin.i18n.t(x, vars);
192+
}
193+
194+
private async changeFileAndCloseModal(contents: string) {
195+
await this.app.vault.modify(this.file, contents);
196+
this.fileChangedHook && this.fileChangedHook(this.file);
197+
this.silentClose = true
198+
199+
setTimeout(() => {
200+
console.log('close modal after 500ms')
201+
this.close()
202+
}, 500)
203+
}
204+
205+
private createHistory(
206+
el: HTMLElement,
207+
): HTMLElement[] {
208+
const syncHistoryListContainer = el.createDiv({
209+
cls: ['sync-history-list-container', 'edit-list-show'],
210+
});
211+
212+
const syncHistoryList = syncHistoryListContainer.createDiv({
213+
cls: 'sync-history-list',
214+
});
215+
const title = syncHistoryList.createDiv({
216+
cls: 'sync-history-list-item title',
217+
text: this.t('diff_edit_list')
218+
});
219+
220+
const setVerBtn = syncHistoryListContainer.createEl('button', {
221+
cls: ['mod-cta', 'btn'],
222+
text: this.t('diff_reset_ver_btn')
223+
});
224+
setTooltip(setVerBtn, 'Click to replace with current selected version', {
225+
placement: 'top',
226+
});
227+
setVerBtn.addEventListener('click', (e) => {
228+
e.preventDefault();
229+
this.changeFileAndCloseModal(this.rightContent);
230+
new Notice(
231+
`The ${this.file.basename} file has been overwritten with the selected version.`
232+
);
233+
});
234+
return [syncHistoryListContainer, syncHistoryList];
235+
}
236+
237+
public basicHtml(diff: string, diffType: string): void {
238+
// set title
239+
this.titleEl.setText(diffType);
240+
// add diff to container
241+
this.syncHistoryContentContainer.innerHTML = diff;
242+
243+
// add history lists and diff to DOM
244+
// this.contentEl.appendChild(this.leftHistory[0]);
245+
this.contentEl.appendChild(this.syncHistoryContentContainer?.parentNode);
246+
this.contentEl.appendChild(this.rightHistory[0]);
247+
}
248+
249+
250+
public makeMoreGeneralHtml(): void {
251+
// highlight initial two versions
252+
this.rightVList[0].html.addClass('is-active');
253+
// keep track of highlighted versions
254+
this.rightActive = 0;
255+
this.leftActive = 1;
256+
}
257+
258+
public async generateVersionListener(
259+
div: HTMLDivElement,
260+
currentVList: vItem[],
261+
currentActive: number,
262+
): Promise<vItem> {
263+
// the exact return type depends on the type of currentVList, it is either vSyncItem or vRecoveryItem
264+
// formerly active left/right version
265+
const currentSideOldVersion = currentVList[currentActive];
266+
// get the HTML of the new version to set it active
267+
const idx = Number(div.id);
268+
const clickedEl: vItem = currentVList[idx];
269+
div.addClass('is-active');
270+
this.rightActive = idx;
271+
// make old not active
272+
currentSideOldVersion.html.classList.remove('is-active');
273+
return clickedEl;
274+
}
275+
}

src/diff/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const SYNC_WARNING =
2+
'Keep in mind that the latest Sync version shown in the diff modal is not necessarily the latest version on disk. Only replace it if you are sure that you want to overwrite this file with the displayed version.';
3+
4+
export const FILE_REC_WARNING =
5+
'The two versions at the top of each list in the diff modal were the file contents read from disk.';
6+
7+
export const GIT_WARNING = FILE_REC_WARNING;

0 commit comments

Comments
 (0)