Follow guide https://bit.ly/3JIZGTE to improve Prettier + ESLint

This commit is contained in:
friebetill 2023-02-02 22:36:11 +08:00
parent 8d864b7d8f
commit a01e69a66f
20 changed files with 2065 additions and 444 deletions

View file

@ -1,3 +1,6 @@
node_modules/
main.js
*.css
*.svg

View file

@ -1,23 +1,63 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended",
"airbnb",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint", "react-hooks"],
"rules": {
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["error"],
"react/jsx-filename-extension": [
"warn",
{
"extensions": [".tsx"]
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never"
}
],
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true
}
],
"max-len": [
"warn",
{
"code": 80
}
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"import/prefer-default-export": "off",
"react/prop-types": "off"
},
"settings": {
"import/resolver": {
"typescript": {}
}
}
}

4
.prettierignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
# Ignore artifacts:
build
coverage

6
.prettierrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}

View file

@ -10,7 +10,6 @@
"format": "prettier --write \"**/*.{ts,tsx}\"",
"coverage": "vitest run --coverage",
"release": "./scripts/release.sh"
},
"keywords": [
"obsidian"
@ -19,7 +18,8 @@
"authorUrl": "https://friebetill.github.io/",
"license": "MIT",
"dependencies": {
"diff": "^5.1.0"
"diff": "^5.1.0",
"obsidian": "latest"
},
"devDependencies": {
"@types/diff": "^5.0.2",
@ -28,7 +28,15 @@
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react": "^7.32.2",
"eslint": "^8.33.0",
"prettier": "^2.8.3",
"tslib": "2.4.0",
"typescript": "4.7.4",

View file

@ -1,177 +0,0 @@
import { TFile } from "obsidian";
import { Difference } from "./data/difference";
import { replaceLine } from "./utils/string_utils";
type VoidCallback = () => void;
export class ActionLine {
constructor(args: {
difference: Difference;
file1: TFile;
file2: TFile;
triggerRebuild: VoidCallback;
}) {
this.difference = args.difference;
this.file1 = args.file1;
this.file2 = args.file2;
this.triggerRebuild = args.triggerRebuild;
}
private difference: Difference;
private file1: TFile;
private file2: TFile;
private triggerRebuild: VoidCallback;
build(container: HTMLDivElement) {
const actionLine = container.createDiv({ cls: "flex-row gap-2 py-2" });
const hasPlusLines = this.difference.lines.some((l) =>
l.startsWith("+")
);
const hasMinusLines = this.difference.lines.some((l) =>
l.startsWith("-")
);
if (hasPlusLines && hasMinusLines) {
this.buildActionLineButton(actionLine, "Accept Top", (e) =>
this.handleAcceptTopClick(e, this.difference)
);
this.buildActionLineDivider(actionLine);
this.buildActionLineButton(actionLine, "Accept Bottom", (e) =>
this.handleAcceptBottomClick(e, this.difference)
);
this.buildActionLineDivider(actionLine);
this.buildActionLineButton(actionLine, "Accept All", (e) =>
this.handleAcceptAllClick(e, this.difference)
);
} else if (hasMinusLines) {
this.buildActionLineButton(
actionLine,
`Accept from ${this.file1.name}`,
(e) => this.handleAcceptTopClick(e, this.difference)
);
this.buildActionLineDivider(actionLine);
this.buildActionLineButton(actionLine, `Discard`, (e) =>
this.handleDiscardClick(e, this.difference)
);
} else if (hasPlusLines) {
this.buildActionLineButton(
actionLine,
`Accept from ${this.file2.name}`,
(e) => this.handleAcceptTopClick(e, this.difference)
);
this.buildActionLineDivider(actionLine);
this.buildActionLineButton(actionLine, `Discard`, (e) =>
this.handleDiscardClick(e, this.difference)
);
}
}
private buildActionLineButton(
actionLine: HTMLDivElement,
text: string,
onClick: (event: MouseEvent) => void
) {
actionLine
.createEl("a", { text, cls: "no-decoration text-xxs text-gray" })
.onClickEvent(onClick);
}
private buildActionLineDivider(actionLine: HTMLDivElement) {
actionLine.createEl("span", { text: "|", cls: "text-xxs text-gray" });
}
private async handleAcceptTopClick(
event: MouseEvent,
difference: Difference
): Promise<void> {
event.preventDefault();
const file1Content = await app.vault.read(this.file1);
const changedLines = difference.lines
.filter((line) => line.startsWith("-"))
.map((line) => line.slice(1, line.length))
.join("\n");
const minusPlusLinesCount = difference.lines.findIndex(
(line) => line.startsWith("-") || line.startsWith("+")
);
const newContent = replaceLine(
file1Content,
difference.start - 1 + minusPlusLinesCount,
changedLines
);
await app.vault.modify(this.file1, newContent);
this.triggerRebuild();
}
private async handleAcceptBottomClick(
event: MouseEvent,
difference: Difference
): Promise<void> {
event.preventDefault();
const file1Content = await app.vault.read(this.file1);
const changedLines = difference.lines
.filter((line) => line.startsWith("+"))
.map((line) => line.slice(1, line.length))
.join("\n");
const minusPlusLinesCount = difference.lines.findIndex(
(line) => line.startsWith("-") || line.startsWith("+")
);
const newContent = replaceLine(
file1Content,
difference.start - 1 + minusPlusLinesCount,
changedLines
);
await app.vault.modify(this.file1, newContent);
this.triggerRebuild();
}
private async handleAcceptAllClick(
event: MouseEvent,
difference: Difference
): Promise<void> {
event.preventDefault();
const file1Content = await app.vault.read(this.file1);
const changedLines = difference.lines
.filter((line) => line.startsWith("-") || line.startsWith("+"))
.map((line) => line.slice(1, line.length))
.join("\n");
const minusPlusLinesCount = difference.lines.findIndex(
(line) => line.startsWith("-") || line.startsWith("+")
);
const newContent = replaceLine(
file1Content,
difference.start - 1 + minusPlusLinesCount,
changedLines
);
await app.vault.modify(this.file1, newContent);
this.triggerRebuild();
}
async handleDiscardClick(
event: MouseEvent,
difference: Difference
): Promise<void> {
event.preventDefault();
const file1Content = await app.vault.read(this.file1);
const minusPlusLinesCount = difference.lines.findIndex(
(line) => line.startsWith("-") || line.startsWith("+")
);
const newContent = replaceLine(
file1Content,
difference.start - 1 + minusPlusLinesCount,
""
);
await app.vault.modify(this.file1, newContent);
this.triggerRebuild();
}
}

View file

@ -0,0 +1,172 @@
import { TFile } from 'obsidian'
import { Difference } from '../data/difference'
import { replaceLine } from '../utils/string_utils'
import { ActionLineButton } from './action_line_button'
import { ActionLineDivider } from './action_line_divider'
type VoidCallback = () => void
export class ActionLine {
constructor(args: {
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
}
private difference: Difference
private file1: TFile
private file2: TFile
private file1Content: string
private file2Content: string
private triggerRebuild: VoidCallback
build(container: HTMLDivElement): void {
const actionLine = container.createDiv({ cls: 'flex-row gap-2 py-2' })
const hasPlusLines = this.difference.lines.some((l) =>
l.startsWith('+')
)
const hasMinusLines = this.difference.lines.some((l) =>
l.startsWith('-')
)
if (hasPlusLines && hasMinusLines) {
new ActionLineButton({
text: 'Accept Top',
onClick: (e) => this.handleAcceptTopClick(e, this.difference),
}).build(actionLine)
ActionLineDivider.build(actionLine)
new ActionLineButton({
text: 'Accept Bottom',
onClick: (e) =>
this.handleAcceptBottomClick(e, this.difference),
}).build(actionLine)
ActionLineDivider.build(actionLine)
new ActionLineButton({
text: 'Accept All',
onClick: (e) => this.handleAcceptAllClick(e, this.difference),
}).build(actionLine)
} else if (hasMinusLines) {
new ActionLineButton({
text: `Accept from ${this.file1.name}`,
onClick: (e) => this.handleAcceptTopClick(e, this.difference),
}).build(actionLine)
ActionLineDivider.build(actionLine)
new ActionLineButton({
text: 'Discard',
onClick: (e) => this.handleDiscardClick(e, this.difference),
}).build(actionLine)
} else if (hasPlusLines) {
new ActionLineButton({
text: `Accept from ${this.file2.name}`,
onClick: (e) => this.handleAcceptTopClick(e, this.difference),
}).build(actionLine)
ActionLineDivider.build(actionLine)
new ActionLineButton({
text: 'Discard',
onClick: (e) => this.handleDiscardClick(e, this.difference),
}).build(actionLine)
}
}
private async handleAcceptTopClick(
event: MouseEvent,
difference: Difference
): Promise<void> {
event.preventDefault()
const changedLines = difference.lines
.filter((line) => line.startsWith('-'))
.map((line) => line.slice(1, line.length))
.join('\n')
const minusPlusLinesCount = difference.lines.findIndex(
(line) => line.startsWith('-') || line.startsWith('+')
)
const newContent = replaceLine({
fullText: this.file1Content,
position: difference.start - 1 + minusPlusLinesCount,
newLine: changedLines,
})
await app.vault.modify(this.file1, newContent)
this.triggerRebuild()
}
private async handleAcceptBottomClick(
event: MouseEvent,
difference: Difference
): Promise<void> {
event.preventDefault()
const changedLines = difference.lines
.filter((line) => line.startsWith('+'))
.map((line) => line.slice(1, line.length))
.join('\n')
const newContent = replaceLine({
fullText: this.file1Content,
position: difference.start - 1,
newLine: changedLines,
})
await app.vault.modify(this.file1, newContent)
this.triggerRebuild()
}
private async handleAcceptAllClick(
event: MouseEvent,
difference: Difference
): Promise<void> {
event.preventDefault()
const changedLines = difference.lines
.filter((line) => line.startsWith('-') || line.startsWith('+'))
.map((line) => line.slice(1, line.length))
.join('\n')
const minusPlusLinesCount = difference.lines.findIndex(
(line) => line.startsWith('-') || line.startsWith('+')
)
const newContent = replaceLine({
fullText: this.file1Content,
position: difference.start - 1 + minusPlusLinesCount,
newLine: changedLines,
})
await app.vault.modify(this.file1, newContent)
this.triggerRebuild()
}
async handleDiscardClick(
event: MouseEvent,
difference: Difference
): Promise<void> {
event.preventDefault()
const minusPlusLinesCount = difference.lines.findIndex(
(line) => line.startsWith('-') || line.startsWith('+')
)
const newContent = replaceLine({
fullText: this.file1Content,
position: difference.start - 1 + minusPlusLinesCount,
newLine: '',
})
await app.vault.modify(this.file1, newContent)
this.triggerRebuild()
}
}

View file

@ -0,0 +1,19 @@
export class ActionLineButton {
constructor(args: { text: string; onClick: (e: MouseEvent) => void }) {
this.text = args.text
this.onClick = args.onClick
}
public text: string
public onClick: (e: MouseEvent) => void
build(actionLine: HTMLDivElement): void {
actionLine
.createEl('a', {
text: this.text,
cls: 'no-decoration text-xxs text-gray',
})
.onClickEvent(this.onClick)
}
}

View file

@ -0,0 +1,5 @@
export class ActionLineDivider {
static build(actionLine: HTMLDivElement): void {
actionLine.createEl('span', { text: '|', cls: 'text-xxs text-gray' })
}
}

View file

@ -0,0 +1,140 @@
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'
export const VIEW_TYPE_PATCH = 'patch-view'
export class DifferencesView extends ItemView {
constructor(args: {
leaf: WorkspaceLeaf
file1: TFile
file2: TFile
showMergeOption: boolean
}) {
super(args.leaf)
this.file1 = args.file1
this.file2 = args.file2
this.showMergeOption = args.showMergeOption
}
private file1: TFile
private file2: TFile
private file1Content: string
private file2Content: string
private showMergeOption: boolean
private fileDifferences: FileDifferences
private file1Lines: string[]
private lineCount: number
getViewType(): string {
return VIEW_TYPE_PATCH
}
getDisplayText(): string {
return `Difference between ${this.file1.name} and ${this.file2.name}`
}
async onOpen(): Promise<void> {
await this.updateState()
this.build()
}
private async updateState(): Promise<void> {
// TODO(tillf): Find way to refresh state when one of the files changes
this.file1Content = await this.app.vault.read(this.file1)
this.file1Lines = this.file1Content.split('\n')
this.file2Content = await this.app.vault.read(this.file2)
this.fileDifferences = FileDifferences.fromParsedDiff(
structuredPatch(
this.file1.path,
this.file2.path,
this.file1Content,
this.file2Content
)
)
// Find the highest line number we need to go through. This can be the
// highest number in the differences, because the second file can have
// more lines than the first file.
this.lineCount = Math.max(
this.file1Lines.length,
...this.fileDifferences.differences.map((d) => d.start)
)
}
private build(): void {
this.contentEl.empty()
const container = this.contentEl.createDiv({ cls: 'container' })
for (let i = 0; i <= this.lineCount; i++) {
const line = i in this.file1Lines ? this.file1Lines[i] : null
const difference = this.fileDifferences.differences.find(
(d) => d.start === i
)
if (difference != null) {
this.buildDifferenceVisualizer(container, difference)
}
if (
line != null &&
(difference == null || !difference.hasChangesFromFile1())
) {
container.createDiv({
// Necessary to give the line a height when it's empty.
text: preventEmptyString(line),
cls: 'line',
})
}
}
// TODO(tillf): Show delete second file modal if showMergeOption is
// enabled and there are no more differences
}
private buildDifferenceVisualizer(
container: HTMLDivElement,
difference: Difference
): void {
const triggerRebuild = (): Promise<void> => this.onOpen()
if (this.showMergeOption) {
new ActionLine({
difference,
file1: this.file1,
file2: this.file2,
file1Content: this.file1Content,
file2Content: this.file2Content,
triggerRebuild,
}).build(container)
}
difference.lines.forEach((line) => {
if (line.startsWith('+')) {
container.createDiv({
// Necessary to give the line a height when it's empty.
text: preventEmptyString(line.slice(1, line.length)),
cls: 'line bg-turquoise-light',
})
} else if (line.startsWith('-')) {
container.createDiv({
// Necessary to give the line a height when it's empty.
text: preventEmptyString(line.slice(1, line.length)),
cls: 'line bg-blue-light',
})
}
})
}
}

View file

@ -0,0 +1,26 @@
import { App, SuggestModal, TFile } from 'obsidian'
export class SelectFileModal extends SuggestModal<TFile> {
constructor(
app: App,
private readonly selectableFiles: TFile[],
private onChoose: (error: Error | null, result?: TFile) => void
) {
super(app)
}
getSuggestions(query: string): TFile[] {
return this.selectableFiles.filter((file) => {
const searchQuery = query?.toLowerCase()
return file.name?.toLowerCase().includes(searchQuery)
})
}
renderSuggestion(file: TFile, el: HTMLElement): void {
el.createEl('div', { text: file.name })
}
onChooseSuggestion(file: TFile): void {
this.onChoose(null, file)
}
}

View file

@ -1,13 +1,14 @@
export class Difference {
constructor(args: { start: number; lines: string[] }) {
this.start = args.start;
this.lines = args.lines;
this.start = args.start
this.lines = args.lines
}
public readonly start: number;
public readonly lines: string[];
public readonly start: number
public readonly lines: string[]
hasChangesFromFile1(): boolean {
return this.lines.some((l) => l.startsWith("-"));
return this.lines.some((l) => l.startsWith('-'))
}
}

View file

@ -1,17 +1,17 @@
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", () => {
describe('FileDifferences.fromParsedDiff', () => {
it('should work with files containing one line', () => {
const test = JSON.parse(
'{"oldFileName":"Hallo.md","newFileName":"Test.md","hunks":[{"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":"Hallo.md","file2Name":"Test.md","differences":[{"start":0,"lines":["-a","+b"]}]}'
);
});
});
)
})
})

View file

@ -1,5 +1,5 @@
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.
@ -29,23 +29,23 @@ 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) => {
for (let i = 0; i < hunk.lines.length; i++) {
const line = hunk.lines[i];
const line = hunk.lines[i]
if (line.startsWith("+") || line.startsWith("-")) {
let start = i;
if (line.startsWith('+') || line.startsWith('-')) {
const start = i
// Find the end of the contiguous lines
let end = i;
let end = i
while (
end < hunk.lines.length - 1 &&
(hunk.lines[end + 1].startsWith("+") ||
hunk.lines[end + 1].startsWith("-"))
(hunk.lines[end + 1].startsWith('+') ||
hunk.lines[end + 1].startsWith('-'))
) {
end++;
end++
}
// Add the contiguous lines to the differences
@ -54,16 +54,16 @@ export class FileDifferences {
start: hunk.oldStart + start - 1,
lines: hunk.lines.slice(start, end + 1),
})
);
i += end - start;
)
i += end - start
}
}
});
})
return new this(
parsedDiff.oldFileName!,
parsedDiff.newFileName!,
differences
);
)
}
}

View file

@ -1,125 +0,0 @@
import { structuredPatch } from "diff";
import { ItemView, TFile, WorkspaceLeaf } from "obsidian";
import { ActionLine } from "./action_line";
import { Difference } from "./data/difference";
import { FileDifferences } from "./data/file_differences";
import { preventEmptyString } from "./utils/string_utils";
export const VIEW_TYPE_PATCH = "patch-view";
export class DifferencesView extends ItemView {
constructor(args: {
leaf: WorkspaceLeaf;
file1: TFile;
file2: TFile;
showMergeOption: boolean;
}) {
super(args.leaf);
this.file1 = args.file1;
this.file2 = args.file2;
this.showMergeOption = args.showMergeOption;
}
private file1: TFile;
private file2: TFile;
private showMergeOption: boolean;
private fileDifferences: FileDifferences;
private file1Lines: string[];
private lineCount: number;
getViewType() {
return VIEW_TYPE_PATCH;
}
getDisplayText() {
return `Difference between ${this.file1.name} and ${this.file2.name}`;
}
async onOpen() {
await this.updateState();
this.build();
}
private async updateState() {
const file1Content = await this.app.vault.read(this.file1);
this.file1Lines = file1Content.split("\n");
const file2Content = await this.app.vault.read(this.file2);
this.fileDifferences = FileDifferences.fromParsedDiff(
structuredPatch(
this.file1.path,
this.file2.path,
file1Content,
file2Content
)
);
// Find the highest line number we need to go through. This can be the
// highest number in the differences, because the second file can have
// more lines than the first file.
this.lineCount = Math.max(
this.file1Lines.length,
...this.fileDifferences.differences.map((d) => d.start)
);
}
private build() {
this.contentEl.empty();
const container = this.contentEl.createDiv({ cls: "container" });
for (let i = 0; i <= this.lineCount; i++) {
let line = i in this.file1Lines ? this.file1Lines[i] : null;
const difference = this.fileDifferences.differences.find(
(d) => d.start === i
);
if (difference != null) {
this.buildDifferenceVisualizer(container, difference);
}
if (
line != null &&
(difference == null || !difference.hasChangesFromFile1())
) {
container.createDiv({
// Necessary to give the line a height when it's empty.
text: preventEmptyString(line),
cls: "line",
});
}
}
}
private buildDifferenceVisualizer(
container: HTMLDivElement,
difference: Difference
) {
const triggerRebuild = () => this.onOpen();
if (this.showMergeOption) {
new ActionLine({
difference: difference,
file1: this.file1,
file2: this.file2,
triggerRebuild: triggerRebuild,
}).build(container);
}
difference.lines.forEach((line) => {
if (line.startsWith("+")) {
container.createDiv({
// Necessary to give the line a height when it's empty.
text: preventEmptyString(line.slice(1, line.length)),
cls: "line bg-turquoise-light",
});
} else if (line.startsWith("-")) {
container.createDiv({
// Necessary to give the line a height when it's empty.
text: preventEmptyString(line.slice(1, line.length)),
cls: "line bg-blue-light",
});
}
});
}
}

View file

@ -1,25 +1,28 @@
import { Editor, Plugin, TFile } from "obsidian";
import { Plugin, TFile } from 'obsidian'
import { DifferencesView } from "./differences_view";
import { SelectFileModal } from "./select_file_modal";
import { DifferencesView } from './components/differences_view'
import { SelectFileModal } from './components/select_file_modal'
export default class FileDiffPlugin extends Plugin {
async onload() {
onload(): void {
this.addCommand({
id: "file-diff",
name: "Show differences to another file",
editorCallback: async (_: Editor) => {
id: 'file-diff',
name: 'Show differences to another file',
editorCallback: async () => {
// Get current active file
const activeFile = this.app.workspace.getActiveFile()!;
const activeFile = this.app.workspace.getActiveFile()
if (activeFile == null) {
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,
@ -27,28 +30,31 @@ export default class FileDiffPlugin extends Plugin {
file2: compareFile,
showMergeOption: false,
})
);
)
},
});
})
this.addCommand({
id: "file-diff-merge",
name: "Show differences and merge options to another file",
editorCallback: async (_: Editor) => {
id: 'file-diff-merge',
name: 'Show differences and merge options to another file',
editorCallback: async () => {
// TODO(tillf): Show warning when the user selects this option
// for the first time
// Get current active file
const activeFile = this.app.workspace.getActiveFile()!;
const activeFile = this.app.workspace.getActiveFile()
if (activeFile == null) {
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,
@ -56,29 +62,29 @@ export default class FileDiffPlugin extends Plugin {
file2: compareFile,
showMergeOption: true,
})
);
)
},
});
})
}
async getFileToCompare(activeFile: TFile): Promise<TFile | undefined> {
const selectableFiles = this.app.vault.getFiles();
selectableFiles.remove(activeFile);
getFileToCompare(activeFile: TFile): Promise<TFile | undefined> {
const selectableFiles = this.app.vault.getFiles()
selectableFiles.remove(activeFile)
return this.showSelectOtherFileModal({
selectableFiles: selectableFiles,
});
selectableFiles,
})
}
async showSelectOtherFileModal(args: {
selectableFiles: TFile[];
showSelectOtherFileModal(args: {
selectableFiles: TFile[]
}): Promise<TFile | undefined> {
return new Promise((resolve, reject) => {
return new SelectFileModal(
new SelectFileModal(
this.app,
args.selectableFiles,
(error, selectedFile) =>
error ? reject(error) : resolve(selectedFile)
).open();
});
).open()
})
}
}

View file

@ -1,26 +0,0 @@
import { App, SuggestModal, TFile } from "obsidian";
export class SelectFileModal extends SuggestModal<TFile> {
constructor(
app: App,
private readonly selectableFiles: TFile[],
private onChoose: (error: Error | null, result?: TFile) => void
) {
super(app);
}
getSuggestions(query: string): TFile[] {
return this.selectableFiles.filter((file) => {
const searchQuery = query?.toLowerCase();
return file.name?.toLowerCase().includes(searchQuery);
});
}
renderSuggestion(file: TFile, el: HTMLElement) {
el.createEl("div", { text: file.name });
}
onChooseSuggestion(file: TFile) {
this.onChoose(null, file);
}
}

View file

@ -1,16 +1,24 @@
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 the line at the given line number with the new content", () => {
const content = "line1\nline2\nline3";
const newContent = replaceLine(content, 1, "newLine2");
expect(newContent).toBe("line1\nnewLine2\nline3");
});
describe('replaceLine', () => {
it('should replace the line at the given line number with the new content', () => {
const content = 'line1\nline2\nline3'
const newContent = replaceLine({
fullText: content,
position: 1,
newLine: 'newLine2',
})
expect(newContent).toBe('line1\nnewLine2\nline3')
})
it("should replace the line at the given line number with multiple lines", () => {
const content = "line1\nline2\nline3";
const newContent = replaceLine(content, 1, "newLine2\nnewLine3");
expect(newContent).toBe("line1\nnewLine2\nnewLine3\nline3");
});
});
it('should replace the line at the given line number with multiple lines', () => {
const content = 'line1\nline2\nline3'
const newContent = replaceLine({
fullText: content,
position: 1,
newLine: 'newLine2\nnewLine3',
})
expect(newContent).toBe('line1\nnewLine2\nnewLine3\nline3')
})
})

View file

@ -1,15 +1,15 @@
/** Method to replace line in a string */
export function replaceLine(
text: string,
lineNumber: number,
contentToInsert: string
): string {
const lines = text.split("\n");
lines[lineNumber] = contentToInsert;
return lines.join("\n");
export function replaceLine(args: {
fullText: string
position: number
newLine: string
}): string {
const lines = this.fullText.split('\n')
lines[this.position] = this.newLine
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 : ''
}

1525
yarn.lock

File diff suppressed because it is too large Load diff