diff --git a/.eslintrc b/.eslintrc index b127443..2ccfe2c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,6 +23,7 @@ "class-methods-use-this": "off", "no-use-before-define": "off", "no-warning-comments": 1, + "semi": [2, "always"], "@typescript-eslint/no-use-before-define": ["error"], "react/jsx-filename-extension": [ "warn", diff --git a/.prettierrc.json b/.prettierrc.json index 123daf0..3947009 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,6 +1,6 @@ { "trailingComma": "es5", "tabWidth": 4, - "semi": false, + "semi": true, "singleQuote": true } diff --git a/src/components/action_line.ts b/src/components/action_line.ts index 828eeff..db98986 100644 --- a/src/components/action_line.ts +++ b/src/components/action_line.ts @@ -1,88 +1,88 @@ -import { TFile } from 'obsidian' -import { Difference } from '../data/difference' -import { deleteLines, insertLine, replaceLine } from '../utils/string_utils' -import { ActionLineButton } from './action_line_button' -import { ActionLineDivider } from './action_line_divider' +import { TFile } from 'obsidian'; +import { Difference } from '../data/difference'; +import { deleteLines, insertLine, replaceLine } from '../utils/string_utils'; +import { ActionLineButton } from './action_line_button'; +import { ActionLineDivider } from './action_line_divider'; -type VoidCallback = () => void +type VoidCallback = () => void; export class ActionLine { constructor(args: { - difference: Difference - file1: TFile - file2: TFile - file1Content: string - file2Content: string - triggerRebuild: VoidCallback + difference: Difference; + file1: TFile; + file2: TFile; + file1Content: string; + file2Content: string; + triggerRebuild: VoidCallback; }) { - this.difference = args.difference - this.file1 = args.file1 - this.file2 = args.file2 - this.file1Content = args.file1Content - this.file2Content = args.file2Content - this.triggerRebuild = args.triggerRebuild + this.difference = args.difference; + this.file1 = args.file1; + this.file2 = args.file2; + this.file1Content = args.file1Content; + this.file2Content = args.file2Content; + this.triggerRebuild = args.triggerRebuild; } - private difference: Difference + private difference: Difference; - private file1: TFile + private file1: TFile; - private file2: TFile + private file2: TFile; - private file1Content: string + private file1Content: string; - private file2Content: string + private file2Content: string; - private triggerRebuild: VoidCallback + private triggerRebuild: VoidCallback; build(container: HTMLDivElement): void { const actionLine = container.createDiv({ cls: 'flex flex-row gap-1 py-0-5', - }) + }); - const hasMinusLines = this.difference.file1Lines.length > 0 - const hasPlusLines = this.difference.file2Lines.length > 0 + const hasMinusLines = this.difference.file1Lines.length > 0; + const hasPlusLines = this.difference.file2Lines.length > 0; if (hasPlusLines && hasMinusLines) { new ActionLineButton({ text: 'Accept Top', onClick: (e) => this.acceptTopClick(e, this.difference), - }).build(actionLine) - ActionLineDivider.build(actionLine) + }).build(actionLine); + ActionLineDivider.build(actionLine); new ActionLineButton({ text: 'Accept Bottom', onClick: (e) => this.acceptBottomClick(e, this.difference), - }).build(actionLine) - ActionLineDivider.build(actionLine) + }).build(actionLine); + ActionLineDivider.build(actionLine); new ActionLineButton({ text: 'Accept All', onClick: (e) => this.acceptAllClick(e, this.difference), - }).build(actionLine) - ActionLineDivider.build(actionLine) + }).build(actionLine); + ActionLineDivider.build(actionLine); new ActionLineButton({ text: 'Accept None', onClick: (e) => this.acceptNoneClick(e, this.difference), - }).build(actionLine) + }).build(actionLine); } else if (hasMinusLines) { new ActionLineButton({ text: `Accept from ${this.file1.name}`, onClick: (e) => this.insertFile1Difference(e, this.difference), - }).build(actionLine) - ActionLineDivider.build(actionLine) + }).build(actionLine); + ActionLineDivider.build(actionLine); new ActionLineButton({ text: 'Discard', onClick: (e) => this.discardFile1Difference(e, this.difference), - }).build(actionLine) + }).build(actionLine); } else if (hasPlusLines) { new ActionLineButton({ text: `Accept from ${this.file2.name}`, onClick: (e) => this.insertFile2Difference(e, this.difference), - }).build(actionLine) - ActionLineDivider.build(actionLine) + }).build(actionLine); + ActionLineDivider.build(actionLine); new ActionLineButton({ text: 'Discard', onClick: (e) => this.discardFile2Difference(e, this.difference), - }).build(actionLine) + }).build(actionLine); } } @@ -90,154 +90,154 @@ export class ActionLine { event: MouseEvent, difference: Difference ): Promise { - event.preventDefault() + event.preventDefault(); - const changedLines = difference.file1Lines.join('\n') + const changedLines = difference.file1Lines.join('\n'); const newContent = replaceLine({ fullText: this.file2Content, newLine: changedLines, position: difference.file2Start, linesToReplace: difference.file2Lines.length, - }) - await app.vault.modify(this.file2, newContent) + }); + await app.vault.modify(this.file2, newContent); - this.triggerRebuild() + this.triggerRebuild(); } private async acceptBottomClick( event: MouseEvent, difference: Difference ): Promise { - event.preventDefault() + event.preventDefault(); - const changedLines = difference.file2Lines.join('\n') + const changedLines = difference.file2Lines.join('\n'); const newContent = replaceLine({ fullText: this.file1Content, newLine: changedLines, position: difference.file1Start, linesToReplace: difference.file1Lines.length, - }) - await app.vault.modify(this.file1, newContent) + }); + await app.vault.modify(this.file1, newContent); - this.triggerRebuild() + this.triggerRebuild(); } private async acceptAllClick( event: MouseEvent, difference: Difference ): Promise { - event.preventDefault() + event.preventDefault(); const changedLines = [ ...difference.file1Lines, ...difference.file2Lines, - ].join('\n') + ].join('\n'); const newFile1Content = replaceLine({ fullText: this.file1Content, newLine: changedLines, position: difference.file1Start, linesToReplace: difference.file1Lines.length, - }) - await app.vault.modify(this.file1, newFile1Content) + }); + await app.vault.modify(this.file1, newFile1Content); const newFile2Content = replaceLine({ fullText: this.file2Content, newLine: changedLines, position: difference.file2Start, linesToReplace: difference.file2Lines.length, - }) - await app.vault.modify(this.file2, newFile2Content) + }); + await app.vault.modify(this.file2, newFile2Content); - this.triggerRebuild() + this.triggerRebuild(); } private async acceptNoneClick( event: MouseEvent, difference: Difference ): Promise { - event.preventDefault() + event.preventDefault(); const newFile1Content = deleteLines({ fullText: this.file1Content, position: difference.file1Start, count: difference.file1Lines.length, - }) - await app.vault.modify(this.file1, newFile1Content) + }); + await app.vault.modify(this.file1, newFile1Content); const newFile2Content = deleteLines({ fullText: this.file2Content, position: difference.file2Start, count: difference.file2Lines.length, - }) - await app.vault.modify(this.file2, newFile2Content) + }); + await app.vault.modify(this.file2, newFile2Content); - this.triggerRebuild() + this.triggerRebuild(); } private async insertFile1Difference( event: MouseEvent, difference: Difference ): Promise { - event.preventDefault() + event.preventDefault(); - const changedLines = difference.file1Lines.join('\n') + const changedLines = difference.file1Lines.join('\n'); const newContent = insertLine({ fullText: this.file2Content, newLine: changedLines, position: difference.file2Start, - }) - await app.vault.modify(this.file2, newContent) + }); + await app.vault.modify(this.file2, newContent); - this.triggerRebuild() + this.triggerRebuild(); } private async insertFile2Difference( event: MouseEvent, difference: Difference ): Promise { - event.preventDefault() + event.preventDefault(); - const changedLines = difference.file2Lines.join('\n') + const changedLines = difference.file2Lines.join('\n'); const newContent = insertLine({ fullText: this.file1Content, newLine: changedLines, position: difference.file1Start, - }) - await app.vault.modify(this.file1, newContent) + }); + await app.vault.modify(this.file1, newContent); - this.triggerRebuild() + this.triggerRebuild(); } async discardFile1Difference( event: MouseEvent, difference: Difference ): Promise { - event.preventDefault() + event.preventDefault(); const newContent = deleteLines({ fullText: this.file1Content, position: difference.file1Start, count: difference.file1Lines.length, - }) - await app.vault.modify(this.file1, newContent) + }); + await app.vault.modify(this.file1, newContent); - this.triggerRebuild() + this.triggerRebuild(); } async discardFile2Difference( event: MouseEvent, difference: Difference ): Promise { - event.preventDefault() + event.preventDefault(); const newContent = deleteLines({ fullText: this.file2Content, position: difference.file2Start, count: difference.file2Lines.length, - }) - await app.vault.modify(this.file2, newContent) + }); + await app.vault.modify(this.file2, newContent); - this.triggerRebuild() + this.triggerRebuild(); } } diff --git a/src/components/action_line_button.ts b/src/components/action_line_button.ts index 7564398..65add62 100644 --- a/src/components/action_line_button.ts +++ b/src/components/action_line_button.ts @@ -1,12 +1,12 @@ export class ActionLineButton { constructor(args: { text: string; onClick: (e: MouseEvent) => void }) { - this.text = args.text - this.onClick = args.onClick + this.text = args.text; + this.onClick = args.onClick; } - public text: string + public text: string; - public onClick: (e: MouseEvent) => void + public onClick: (e: MouseEvent) => void; build(actionLine: HTMLDivElement): void { actionLine @@ -14,6 +14,6 @@ export class ActionLineButton { text: this.text, cls: 'no-decoration text-xxs file-diff__action-line', }) - .onClickEvent(this.onClick) + .onClickEvent(this.onClick); } } diff --git a/src/components/action_line_divider.ts b/src/components/action_line_divider.ts index a21e41d..527aa5c 100644 --- a/src/components/action_line_divider.ts +++ b/src/components/action_line_divider.ts @@ -3,6 +3,6 @@ export class ActionLineDivider { actionLine.createEl('span', { text: '|', cls: 'text-xxs file-diff__action-line', - }) + }); } } diff --git a/src/components/differences_view.ts b/src/components/differences_view.ts index 249b97b..20413df 100644 --- a/src/components/differences_view.ts +++ b/src/components/differences_view.ts @@ -1,94 +1,94 @@ -import { structuredPatch } from 'diff' -import { ItemView, TFile, WorkspaceLeaf } from 'obsidian' -import { Difference } from '../data/difference' -import { FileDifferences } from '../data/file_differences' -import { preventEmptyString } from '../utils/string_utils' -import { ActionLine } from './action_line' -import { DeleteFileModal } from './modals/delete_file_modal' +import { structuredPatch } from 'diff'; +import { ItemView, TFile, WorkspaceLeaf } from 'obsidian'; +import { Difference } from '../data/difference'; +import { FileDifferences } from '../data/file_differences'; +import { preventEmptyString } from '../utils/string_utils'; +import { ActionLine } from './action_line'; +import { DeleteFileModal } from './modals/delete_file_modal'; -export const VIEW_TYPE_DIFFERENCES = 'differences-view' +export const VIEW_TYPE_DIFFERENCES = 'differences-view'; export class DifferencesView extends ItemView { constructor(args: { - leaf: WorkspaceLeaf - file1: TFile - file2: TFile - showMergeOption: boolean + leaf: WorkspaceLeaf; + file1: TFile; + file2: TFile; + showMergeOption: boolean; }) { - super(args.leaf) - this.file1 = args.file1 - this.file2 = args.file2 - this.showMergeOption = args.showMergeOption + super(args.leaf); + this.file1 = args.file1; + this.file2 = args.file2; + this.showMergeOption = args.showMergeOption; } - private file1: TFile + private file1: TFile; - private file2: TFile + private file2: TFile; - private file1Content: string + private file1Content: string; - private file2Content: string + private file2Content: string; - private showMergeOption: boolean + private showMergeOption: boolean; - private fileDifferences: FileDifferences + private fileDifferences: FileDifferences; - private file1Lines: string[] + private file1Lines: string[]; - private file2Lines: string[] + private file2Lines: string[]; - private lineCount: number + private lineCount: number; - private wasDeleteModalShown = false + private wasDeleteModalShown = false; getViewType(): string { - return VIEW_TYPE_DIFFERENCES + return VIEW_TYPE_DIFFERENCES; } getDisplayText(): string { - return `Difference between ${this.file1.name} and ${this.file2.name}` + return `Difference between ${this.file1.name} and ${this.file2.name}`; } async onload(): Promise { this.registerEvent( this.app.vault.on('modify', async (file) => { if (file === this.file1 || file === this.file2) { - await this.updateState() - this.build() + await this.updateState(); + this.build(); } }) - ) + ); } async onOpen(): Promise { - await this.updateState() - this.build() + await this.updateState(); + this.build(); } private async updateState(): Promise { - this.file1Content = await this.app.vault.read(this.file1) - this.file2Content = await this.app.vault.read(this.file2) + this.file1Content = await this.app.vault.read(this.file1); + this.file2Content = await this.app.vault.read(this.file2); this.file1Lines = this.file1Content // Add trailing new line as this removes edge cases .concat('\n') .split('\n') // Streamline empty lines at the end as this remove edge cases - .map((line) => line.trimEnd()) + .map((line) => line.trimEnd()); this.file2Lines = this.file2Content // Add trailing new line as this removes edge cases .concat('\n') .split('\n') // Streamline empty lines at the end as this remove edge cases - .map((line) => line.trimEnd()) + .map((line) => line.trimEnd()); const parsedDiff = structuredPatch( this.file1.path, this.file2.path, this.file1Lines.join('\n'), this.file2Lines.join('\n') - ) - this.fileDifferences = FileDifferences.fromParsedDiff(parsedDiff) + ); + this.fileDifferences = FileDifferences.fromParsedDiff(parsedDiff); this.lineCount = Math.max( this.file1Lines.length - @@ -101,58 +101,58 @@ export class DifferencesView extends ItemView { this.fileDifferences.differences.filter( (d) => d.file2Lines.length > 0 ).length - ) + ); } private build(): void { - this.contentEl.empty() + this.contentEl.empty(); const container = this.contentEl.createDiv({ cls: 'file-diff__container', - }) + }); - this.buildLines(container) + this.buildLines(container); - this.scrollToFirstDifference() + this.scrollToFirstDifference(); if ( this.fileDifferences.differences.length === 0 && this.showMergeOption && !this.wasDeleteModalShown ) { - this.wasDeleteModalShown = true - this.showDeleteModal() + this.wasDeleteModalShown = true; + this.showDeleteModal(); } } private buildLines(container: HTMLDivElement): void { - let lineCount1 = 0 - let lineCount2 = 0 + let lineCount1 = 0; + let lineCount2 = 0; while (lineCount1 <= this.lineCount || lineCount2 <= this.lineCount) { const difference = this.fileDifferences.differences.find( // eslint-disable-next-line no-loop-func (d) => d.file1Start === lineCount1 && d.file2Start === lineCount2 - ) + ); if (difference != null) { const differenceContainer = container.createDiv({ cls: 'difference', - }) - this.buildDifferenceVisualizer(differenceContainer, difference) - lineCount1 += difference.file1Lines.length - lineCount2 += difference.file2Lines.length + }); + this.buildDifferenceVisualizer(differenceContainer, difference); + lineCount1 += difference.file1Lines.length; + lineCount2 += difference.file2Lines.length; } else { const line = lineCount1 <= lineCount2 ? this.file1Lines[lineCount1] - : this.file2Lines[lineCount2] + : this.file2Lines[lineCount2]; container.createDiv({ // Necessary to give the line a height when it's empty. text: preventEmptyString(line), cls: 'file-diff__line', - }) - lineCount1 += 1 - lineCount2 += 1 + }); + lineCount1 += 1; + lineCount2 += 1; } } } @@ -162,9 +162,9 @@ export class DifferencesView extends ItemView { difference: Difference ): void { const triggerRebuild = async (): Promise => { - await this.updateState() - this.build() - } + await this.updateState(); + this.build(); + }; if (this.showMergeOption) { new ActionLine({ @@ -174,55 +174,55 @@ export class DifferencesView extends ItemView { file1Content: this.file1Content, file2Content: this.file2Content, triggerRebuild, - }).build(container) + }).build(container); } for (let i = 0; i < difference.file1Lines.length; i += 1) { - const line = difference.file1Lines[i] + const line = difference.file1Lines[i]; container.createDiv({ // Necessary to give the line a height when it's empty. text: preventEmptyString(line), cls: 'file-diff__line file-diff__top-line__bg', - }) + }); } for (let i = 0; i < difference.file2Lines.length; i += 1) { - const line = difference.file2Lines[i] + const line = difference.file2Lines[i]; container.createDiv({ // Necessary to give the line a height when it's empty. text: preventEmptyString(line), cls: 'file-diff__line file-diff__bottom-line__bg', - }) + }); } } private scrollToFirstDifference(): void { if (this.fileDifferences.differences.length === 0) { - return + return; } const containerRect = this.contentEl .getElementsByClassName('file-diff__container')[0] - .getBoundingClientRect() + .getBoundingClientRect(); const elementRect = this.contentEl .getElementsByClassName('difference')[0] - .getBoundingClientRect() + .getBoundingClientRect(); this.contentEl.scrollTo({ top: elementRect.top - containerRect.top - 100, behavior: 'smooth', - }) + }); } async showDeleteModal(): Promise { // Wait a moment to avoid appearing overly aggressive with the modal - await sleep(200) + await sleep(200); return new Promise((resolve, reject) => { new DeleteFileModal({ file1: this.file1, file2: this.file2, onDone: (e) => (e ? reject(e) : resolve()), - }).open() - }) + }).open(); + }); } } diff --git a/src/components/modals/delete_file_modal.ts b/src/components/modals/delete_file_modal.ts index 984a28b..c363966 100644 --- a/src/components/modals/delete_file_modal.ts +++ b/src/components/modals/delete_file_modal.ts @@ -1,64 +1,64 @@ -import { Modal, TFile } from 'obsidian' +import { Modal, TFile } from 'obsidian'; export class DeleteFileModal extends Modal { constructor(args: { - file1: TFile - file2: TFile - onDone: (error: Error | null) => void + file1: TFile; + file2: TFile; + onDone: (error: Error | null) => void; }) { - super(app) - this.file1 = args.file1 - this.file2 = args.file2 - this.onDone = args.onDone + super(app); + this.file1 = args.file1; + this.file2 = args.file2; + this.onDone = args.onDone; } - private readonly file1: TFile + private readonly file1: TFile; - private readonly file2: TFile + private readonly file2: TFile; - private readonly onDone: (error: Error | null) => void + private readonly onDone: (error: Error | null) => void; onOpen(): void { // Counteract gravity pull by moving box up for balanced composition - this.modalEl.addClass('mb-20') + this.modalEl.addClass('mb-20'); this.contentEl.createEl('h3', { text: `Delete "${this.file2.name}"?`, - }) + }); this.contentEl.createEl('p', { text: `The contents of "${this.file1.name}" and ` + `"${this.file2.name}" are identical. Would you like to ` + `delete the duplicate file? Please note that this action is ` + `irreversible.`, - }) + }); const buttonContainer = this.contentEl.createDiv( 'file-diff__button-container' - ) + ); const deleteButton = buttonContainer.createEl('button', { text: 'Delete', cls: 'mod-warning mr-2', - }) + }); const cancelButton = buttonContainer.createEl('button', { text: 'Cancel', - }) + }); - deleteButton.addEventListener('click', () => this.handleDeleteClick()) - cancelButton.addEventListener('click', () => this.close()) + deleteButton.addEventListener('click', () => this.handleDeleteClick()); + cancelButton.addEventListener('click', () => this.close()); } handleDeleteClick(): void { - this.app.vault.delete(this.file2) + this.app.vault.delete(this.file2); - this.close() + this.close(); - const leaf = this.app.workspace.getMostRecentLeaf() + const leaf = this.app.workspace.getMostRecentLeaf(); if (leaf != null) { - leaf.openFile(this.file1) + leaf.openFile(this.file1); } - this.onDone(null) + this.onDone(null); } } diff --git a/src/components/modals/risky_action_modal.ts b/src/components/modals/risky_action_modal.ts index 6f594ff..1fd6830 100644 --- a/src/components/modals/risky_action_modal.ts +++ b/src/components/modals/risky_action_modal.ts @@ -1,41 +1,41 @@ -import { Modal } from 'obsidian' +import { Modal } from 'obsidian'; export class RiskyActionModal extends Modal { constructor(args: { onAccept: (error: Error | null) => void }) { - super(app) - this.onAccept = args.onAccept + super(app); + this.onAccept = args.onAccept; } - private readonly onAccept: (error: Error | null) => void + private readonly onAccept: (error: Error | null) => void; onOpen(): void { // Counteract gravity pull by moving box up for balanced composition - this.modalEl.addClass('mb-20') + this.modalEl.addClass('mb-20'); - this.contentEl.createEl('h3', { text: `Do you accept the risk?` }) + this.contentEl.createEl('h3', { text: `Do you accept the risk?` }); this.contentEl.createEl('p', { text: `The merging options alter the files irreversibly. ` + `Proceed with caution and only if you are aware and ` + `accepting of the associated risks.`, - }) + }); const buttonContainer = this.contentEl.createDiv( 'file-diff__button-container' - ) + ); const deleteButton = buttonContainer.createEl('button', { text: 'Accept Risk', cls: 'mod-warning mr-2', - }) + }); const cancelButton = buttonContainer.createEl('button', { text: 'Cancel', - }) + }); - cancelButton.addEventListener('click', () => this.close()) + cancelButton.addEventListener('click', () => this.close()); deleteButton.addEventListener('click', () => { - this.close() - this.onAccept(null) - }) + this.close(); + this.onAccept(null); + }); } } diff --git a/src/components/modals/select_file_modal.ts b/src/components/modals/select_file_modal.ts index 46a1187..e413218 100644 --- a/src/components/modals/select_file_modal.ts +++ b/src/components/modals/select_file_modal.ts @@ -1,33 +1,33 @@ -import { SuggestModal, TFile } from 'obsidian' +import { SuggestModal, TFile } from 'obsidian'; export class SelectFileModal extends SuggestModal { constructor(args: { - selectableFiles: TFile[] - onChoose: (error: Error | null, result?: TFile) => void + selectableFiles: TFile[]; + onChoose: (error: Error | null, result?: TFile) => void; }) { - super(app) - this.selectableFiles = args.selectableFiles - this.onChoose = args.onChoose + super(app); + this.selectableFiles = args.selectableFiles; + this.onChoose = args.onChoose; } - private readonly selectableFiles: TFile[] + private readonly selectableFiles: TFile[]; - private readonly onChoose: (error: Error | null, result?: TFile) => void + private readonly onChoose: (error: Error | null, result?: TFile) => void; getSuggestions(query: string): TFile[] { return this.selectableFiles .filter((file) => { - const searchQuery = query?.toLowerCase() - return file.name?.toLowerCase().includes(searchQuery) + const searchQuery = query?.toLowerCase(); + return file.name?.toLowerCase().includes(searchQuery); }) - .sort((a, b) => (a.stat.mtime < b.stat.mtime ? 1 : -1)) + .sort((a, b) => (a.stat.mtime < b.stat.mtime ? 1 : -1)); } renderSuggestion(file: TFile, el: HTMLElement): void { - el.createEl('div', { text: file.name }) + el.createEl('div', { text: file.name }); } onChooseSuggestion(file: TFile): void { - this.onChoose(null, file) + this.onChoose(null, file); } } diff --git a/src/data/difference.ts b/src/data/difference.ts index 9b01a75..b4630db 100644 --- a/src/data/difference.ts +++ b/src/data/difference.ts @@ -1,21 +1,21 @@ export class Difference { constructor(args: { - file1Start: number - file2Start: number - file1Lines: string[] - file2Lines: string[] + file1Start: number; + file2Start: number; + file1Lines: string[]; + file2Lines: string[]; }) { - this.file1Start = args.file1Start - this.file2Start = args.file2Start - this.file1Lines = args.file1Lines - this.file2Lines = args.file2Lines + this.file1Start = args.file1Start; + this.file2Start = args.file2Start; + this.file1Lines = args.file1Lines; + this.file2Lines = args.file2Lines; } - public readonly file1Start: number + public readonly file1Start: number; - public readonly file2Start: number + public readonly file2Start: number; - public readonly file1Lines: string[] + public readonly file1Lines: string[]; - public readonly file2Lines: string[] + public readonly file2Lines: string[]; } diff --git a/src/data/file_differences.test.ts b/src/data/file_differences.test.ts index 124237d..cb32f41 100644 --- a/src/data/file_differences.test.ts +++ b/src/data/file_differences.test.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { ParsedDiff } from 'diff' -import { describe, expect, it } from 'vitest' -import { FileDifferences } from './file_differences' +import { ParsedDiff } from 'diff'; +import { describe, expect, it } from 'vitest'; +import { FileDifferences } from './file_differences'; describe('FileDifferences.fromParsedDiff', () => { it('should work with files containing one line', () => { @@ -10,13 +10,13 @@ describe('FileDifferences.fromParsedDiff', () => { {"oldStart": 1, "oldLines": 1, "newStart": 1, "newLines": 1, "lines": ["-a","+b"] }]} -`) as ParsedDiff +`) as ParsedDiff; - const fileDifferences = FileDifferences.fromParsedDiff(test) + const fileDifferences = FileDifferences.fromParsedDiff(test); expect(JSON.stringify(fileDifferences)).toBe( '{"file1Name":"1","file2Name":"2","differences":' + '[{"start":0,"lines":["-a","+b"]}]}' - ) - }) -}) + ); + }); +}); diff --git a/src/data/file_differences.ts b/src/data/file_differences.ts index 535382f..0ee71f3 100644 --- a/src/data/file_differences.ts +++ b/src/data/file_differences.ts @@ -1,25 +1,25 @@ -import { ParsedDiff } from 'diff' -import { Difference } from './difference' +import { ParsedDiff } from 'diff'; +import { Difference } from './difference'; /** * A class that contains the differences between two files. */ export class FileDifferences { private constructor(args: { - file1Name?: string - file2Name?: string - differences: Difference[] + file1Name?: string; + file2Name?: string; + differences: Difference[]; }) { - this.file1Name = args.file1Name - this.file2Name = args.file2Name - this.differences = args.differences + this.file1Name = args.file1Name; + this.file2Name = args.file2Name; + this.differences = args.differences; } - public readonly file1Name?: string + public readonly file1Name?: string; - public readonly file2Name?: string + public readonly file2Name?: string; - public readonly differences: Difference[] + public readonly differences: Difference[]; /** * Returns a FileDifferences object from the given ParsedDiff instance. @@ -39,36 +39,36 @@ export class FileDifferences { * chose the latter option as it seemed simpler. */ static fromParsedDiff(parsedDiff: ParsedDiff): FileDifferences { - const differences: Difference[] = [] + const differences: Difference[] = []; parsedDiff.hunks.forEach((hunk) => { - let line1Count = 0 - let line2Count = 0 + let line1Count = 0; + let line2Count = 0; for (let i = 0; i < hunk.lines.length; i += 1) { - const line = hunk.lines[i] + const line = hunk.lines[i]; if (line.startsWith('+') || line.startsWith('-')) { - const start = i + const start = i; // Find the end of the contiguous lines - let end = start + let end = start; while ( end < hunk.lines.length - 1 && (hunk.lines[end + 1].startsWith('+') || hunk.lines[end + 1].startsWith('-')) ) { - end += 1 + end += 1; } // Add the contiguous lines to the differences const file1Lines = hunk.lines .slice(start, end + 1) .filter((l) => l.startsWith('-')) - .map((l) => l.slice(1)) + .map((l) => l.slice(1)); const file2Lines = hunk.lines .slice(start, end + 1) .filter((l) => l.startsWith('+')) - .map((l) => l.slice(1)) + .map((l) => l.slice(1)); differences.push( new Difference({ file1Start: hunk.oldStart + start - line2Count - 1, @@ -76,19 +76,19 @@ export class FileDifferences { file1Lines, file2Lines, }) - ) + ); - line1Count += file1Lines.length - line2Count += file2Lines.length - i += end - start + line1Count += file1Lines.length; + line2Count += file2Lines.length; + i += end - start; } } - }) + }); return new this({ file1Name: parsedDiff.oldFileName, file2Name: parsedDiff.newFileName, differences, - }) + }); } } diff --git a/src/main.ts b/src/main.ts index 136a481..d652092 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,11 @@ -import { Plugin, TFile } from 'obsidian' +import { Plugin, TFile } from 'obsidian'; -import { DifferencesView } from './components/differences_view' -import { RiskyActionModal } from './components/modals/risky_action_modal' -import { SelectFileModal } from './components/modals/select_file_modal' +import { DifferencesView } from './components/differences_view'; +import { RiskyActionModal } from './components/modals/risky_action_modal'; +import { SelectFileModal } from './components/modals/select_file_modal'; export default class FileDiffPlugin extends Plugin { - fileDiffMergeWarningKey = 'file-diff-merge-warning' + fileDiffMergeWarningKey = 'file-diff-merge-warning'; onload(): void { this.addCommand({ @@ -13,19 +13,19 @@ export default class FileDiffPlugin extends Plugin { name: 'Compare', editorCallback: async () => { // Get current active file - const activeFile = this.app.workspace.getActiveFile() + const activeFile = this.app.workspace.getActiveFile(); if (activeFile == null) { - return + return; } // Get file to compare - const compareFile = await this.getFileToCompare(activeFile) + const compareFile = await this.getFileToCompare(activeFile); if (compareFile == null) { - return + return; } // Open differences view - const workspaceLeaf = this.app.workspace.getLeaf() + const workspaceLeaf = this.app.workspace.getLeaf(); await workspaceLeaf.open( new DifferencesView({ leaf: workspaceLeaf, @@ -33,9 +33,9 @@ export default class FileDiffPlugin extends Plugin { file2: compareFile, showMergeOption: false, }) - ) + ); }, - }) + }); this.addCommand({ id: 'compare-and-merge', @@ -43,26 +43,26 @@ export default class FileDiffPlugin extends Plugin { editorCallback: async () => { // Show warning when this option is selected for the first time if (!localStorage.getItem(this.fileDiffMergeWarningKey)) { - await this.showRiskyActionModal() + await this.showRiskyActionModal(); if (!localStorage.getItem(this.fileDiffMergeWarningKey)) { - return + return; } } // Get current active file - const activeFile = this.app.workspace.getActiveFile() + const activeFile = this.app.workspace.getActiveFile(); if (activeFile == null) { - return + return; } // Get file to compare - const compareFile = await this.getFileToCompare(activeFile) + const compareFile = await this.getFileToCompare(activeFile); if (compareFile == null) { - return + return; } // Open differences view - const workspaceLeaf = this.app.workspace.getMostRecentLeaf() + const workspaceLeaf = this.app.workspace.getMostRecentLeaf(); if (workspaceLeaf != null) { await workspaceLeaf.open( new DifferencesView({ @@ -71,27 +71,27 @@ export default class FileDiffPlugin extends Plugin { file2: compareFile, showMergeOption: true, }) - ) + ); } }, - }) + }); } getFileToCompare(activeFile: TFile): Promise { - const selectableFiles = this.app.vault.getFiles() - selectableFiles.remove(activeFile) - return this.showSelectOtherFileModal({ selectableFiles }) + const selectableFiles = this.app.vault.getFiles(); + selectableFiles.remove(activeFile); + return this.showSelectOtherFileModal({ selectableFiles }); } showSelectOtherFileModal(args: { - selectableFiles: TFile[] + selectableFiles: TFile[]; }): Promise { return new Promise((resolve, reject) => { new SelectFileModal({ selectableFiles: args.selectableFiles, onChoose: (e, f) => (e ? reject(e) : resolve(f)), - }).open() - }) + }).open(); + }); } showRiskyActionModal(): Promise { @@ -99,19 +99,19 @@ export default class FileDiffPlugin extends Plugin { new RiskyActionModal({ onAccept: async (e: Error | null) => { if (e) { - reject(e) + reject(e); } else { localStorage.setItem( this.fileDiffMergeWarningKey, 'true' - ) + ); // Wait for the set item dispatch event to be processed - await sleep(50) + await sleep(50); - resolve() + resolve(); } }, - }).open() - }) + }).open(); + }); } } diff --git a/src/utils/string_utils.test.ts b/src/utils/string_utils.test.ts index aecc8e8..1a3e04c 100644 --- a/src/utils/string_utils.test.ts +++ b/src/utils/string_utils.test.ts @@ -1,49 +1,49 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { describe, expect, it } from 'vitest' -import { replaceLine } from './string_utils' +import { describe, expect, it } from 'vitest'; +import { replaceLine } from './string_utils'; describe('replaceLine', () => { it('should replace fullText with newLine at given position', () => { - const fullText = 'line1\nline2\nline3' + const fullText = 'line1\nline2\nline3'; const newFullText = replaceLine({ fullText, newLine: 'newLine2', position: 1, linesToReplace: 1, - }) - expect(newFullText).toBe('line1\nnewLine2\nline3') - }) + }); + expect(newFullText).toBe('line1\nnewLine2\nline3'); + }); it('should replacing with mutliple lines correctly', () => { - const fullText = 'line1\nline2\nline3' + const fullText = 'line1\nline2\nline3'; const newFullText = replaceLine({ fullText, newLine: 'newLine2\nnewLine3', position: 1, linesToReplace: 1, - }) - expect(newFullText).toBe('line1\nnewLine2\nnewLine3\nline3') - }) + }); + expect(newFullText).toBe('line1\nnewLine2\nnewLine3\nline3'); + }); it('should remove line when new line is empty', () => { - const fullText = 'line1\nline2\nline3' + const fullText = 'line1\nline2\nline3'; const newFullText = replaceLine({ fullText, newLine: '', position: 1, linesToReplace: 1, - }) - expect(newFullText).toBe('line1\nline3') - }) + }); + expect(newFullText).toBe('line1\nline3'); + }); it('should replace two lines', () => { - const fullText = 'line1\nline2\nline3\nline4' + const fullText = 'line1\nline2\nline3\nline4'; const newFullText = replaceLine({ fullText, newLine: 'line5', position: 1, linesToReplace: 2, - }) - expect(newFullText).toBe('line1\nline5\nline4') - }) -}) + }); + expect(newFullText).toBe('line1\nline5\nline4'); + }); +}); diff --git a/src/utils/string_utils.ts b/src/utils/string_utils.ts index c427ab6..65a0603 100644 --- a/src/utils/string_utils.ts +++ b/src/utils/string_utils.ts @@ -1,42 +1,42 @@ /** Method to insert a line in a string */ export function insertLine(args: { - fullText: string - newLine: string - position: number + fullText: string; + newLine: string; + position: number; }): string { - const lines = args.fullText.split('\n') - lines.splice(args.position, 0, args.newLine) - return lines.join('\n') + const lines = args.fullText.split('\n'); + lines.splice(args.position, 0, args.newLine); + return lines.join('\n'); } /** Method to replace a line in a string */ export function replaceLine(args: { - fullText: string - newLine: string - position: number - linesToReplace: number + fullText: string; + newLine: string; + position: number; + linesToReplace: number; }): string { - const lines = args.fullText.split('\n') + const lines = args.fullText.split('\n'); if (args.newLine === '') { - lines.splice(args.position, args.linesToReplace) + lines.splice(args.position, args.linesToReplace); } else { - lines.splice(args.position, args.linesToReplace, args.newLine) + lines.splice(args.position, args.linesToReplace, args.newLine); } - return lines.join('\n') + return lines.join('\n'); } /** Method to delete a line in a string */ export function deleteLines(args: { - fullText: string - position: number - count: number + fullText: string; + position: number; + count: number; }): string { - const lines = args.fullText.split('\n') - lines.splice(args.position, args.count) - return lines.join('\n') + const lines = args.fullText.split('\n'); + lines.splice(args.position, args.count); + return lines.join('\n'); } /** Returns an invisible character when the text is empty. */ export function preventEmptyString(text: string): string { - return text !== '' ? text : '‎' + return text !== '' ? text : '‎'; }