refactor: new template

This commit is contained in:
Michael Naumov 2026-06-18 20:01:56 -06:00
parent 338744bd46
commit 6f0f2b8d4e
11 changed files with 588 additions and 671 deletions

640
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -37,6 +37,8 @@
"overrides": {
"@vitest/coverage-v8": "$@vitest/coverage-v8",
"eslint": "$eslint",
"js-yaml": "^4.2.0",
"markdown-it": "^14.2.0",
"type-fest": "^5.6.0",
"typescript": "^6.0.3"
},
@ -44,23 +46,23 @@
"@commitlint/cli": "^21.0.2",
"@commitlint/config-conventional": "^21.0.2",
"@commitlint/types": "^21.0.1",
"@obsidian-typings/obsidian-public-latest": "^6.14.0",
"@obsidian-typings/obsidian-public-latest": "^6.15.0",
"@total-typescript/ts-reset": "^0.6.1",
"@tsconfig/strictest": "^2.0.8",
"@types/node": "^25.9.3",
"@vitest/coverage-v8": "^4.1.8",
"@vitest/coverage-v8": "^4.1.9",
"better-typescript-lib": "^2.12.0",
"czg": "^1.13.1",
"eslint": "^10.4.1",
"eslint": "^10.5.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"jsdom": "^29.1.1",
"nano-staged": "^1.0.2",
"obsidian": "^1.13.1",
"obsidian-dev-utils": "^70.2.0",
"obsidian-integration-testing": "^4.2.3",
"obsidian-test-mocks": "^3.1.0",
"obsidian-dev-utils": "^72.0.0",
"obsidian-integration-testing": "^4.2.6",
"obsidian-test-mocks": "^3.1.1",
"sass-embedded": "^1.100.0",
"vitest": "^4.1.8"
"vitest": "^4.1.9"
}
}

View file

@ -0,0 +1,201 @@
import type { BacklinkView } from '@obsidian-typings/obsidian-public-latest';
import type {
App,
TFile
} from 'obsidian';
import {
InternalPluginName,
ViewType
} from '@obsidian-typings/obsidian-public-latest/implementations';
import {
MarkdownView,
setTooltip
} from 'obsidian';
import { invokeAsyncSafely } from 'obsidian-dev-utils/async';
import { getPrototypeOf } from 'obsidian-dev-utils/object-utils';
import { LayoutReadyComponent } from 'obsidian-dev-utils/obsidian/components/layout-ready-component';
import { MonkeyAroundComponent } from 'obsidian-dev-utils/obsidian/components/monkey-around-component';
import { PluginSettingsComponent } from './plugin-settings-component.ts';
interface BacklinkFullPathComponentConstructorParams {
readonly app: App;
readonly pluginSettingsComponent: PluginSettingsComponent;
}
export class BacklinkFullPathComponent extends LayoutReadyComponent {
private readonly pluginSettingsComponent: PluginSettingsComponent;
public constructor(params: BacklinkFullPathComponentConstructorParams) {
super(params.app);
this.pluginSettingsComponent = params.pluginSettingsComponent;
}
protected override async onLayoutReady(): Promise<void> {
this.pluginSettingsComponent.on('saveSettings', async () => {
await this.refreshBacklinkPanels();
});
const backlinksCorePlugin = this.app.internalPlugins.getPluginById(InternalPluginName.Backlink);
if (!backlinksCorePlugin) {
return;
}
const patch = this.addChild(new MonkeyAroundComponent());
patch.registerMethodPatch({
methodName: 'onUserEnable',
obj: getPrototypeOf(backlinksCorePlugin.instance),
patchHandler: ({
fallback
}) => {
fallback();
this.onBacklinksCorePluginEnable();
}
});
if (backlinksCorePlugin.enabled) {
await this.patchBacklinksPane();
await this.refreshBacklinkPanels();
}
this.register(() => {
invokeAsyncSafely(async () => {
await this.refreshBacklinkPanels();
});
});
}
private generateBacklinkTitle(file: TFile): HTMLDivElement {
const fileNamePart = this.pluginSettingsComponent.settings.shouldIncludeExtension ? file.name : file.basename;
let parentPathParts = file.path.split('/').slice(0, -1);
for (let length = parentPathParts.length; length >= 1; length--) {
const rootPath = parentPathParts.slice(0, length).join('/');
if (this.pluginSettingsComponent.settings.rootPaths.includes(rootPath)) {
parentPathParts = parentPathParts.slice(length);
break;
}
}
if (this.pluginSettingsComponent.settings.pathDepth > 0) {
const partsToSkipCount = Math.max(0, parentPathParts.length - this.pluginSettingsComponent.settings.pathDepth + 1);
if (partsToSkipCount > 0) {
parentPathParts.splice(0, partsToSkipCount);
if (this.pluginSettingsComponent.settings.shouldShowEllipsisForSkippedPathParts) {
parentPathParts.unshift('...');
}
}
}
if (this.pluginSettingsComponent.settings.shouldReversePathParts) {
parentPathParts.reverse();
}
const pathSeparator = this.pluginSettingsComponent.settings.shouldReversePathParts ? ' \u2190 ' : '/';
const parentStr = parentPathParts.join(pathSeparator);
const container = createDiv({
cls: ['backlink-full-path', 'backlink-control']
});
container.dataset['shouldHighlightFileName'] = this.pluginSettingsComponent.settings.shouldHighlightFileName.toString();
container.dataset['shouldDisplayParentPathOnSeparateLine'] = this.pluginSettingsComponent.settings.shouldDisplayParentPathOnSeparateLine.toString();
container.createSpan({
cls: 'full-path',
text: file.path
});
const shadowRoot = container.attachShadow({ mode: 'open' });
setTooltip(container, file.path);
shadowRoot.createSpan({
attr: {
part: 'file-name'
},
text: fileNamePart
});
if (parentStr) {
let text = parentStr;
if (!this.pluginSettingsComponent.settings.shouldDisplayParentPathOnSeparateLine) {
text = this.pluginSettingsComponent.settings.shouldReversePathParts ? pathSeparator + text : text + pathSeparator;
}
shadowRoot.createSpan({
attr: {
part: 'parent-path'
},
prepend: !this.pluginSettingsComponent.settings.shouldReversePathParts && !this.pluginSettingsComponent.settings.shouldDisplayParentPathOnSeparateLine,
text
});
}
return container;
}
private async getBacklinkView(): Promise<BacklinkView | null> {
const backlinksLeaf = this.app.workspace.getLeavesOfType(ViewType.Backlink)[0];
if (!backlinksLeaf) {
return null;
}
await backlinksLeaf.loadIfDeferred();
return backlinksLeaf.view as BacklinkView;
}
private onBacklinksCorePluginEnable(): void {
invokeAsyncSafely(() => this.patchBacklinksPane());
}
private async patchBacklinksPane(): Promise<void> {
const backlinkView = await this.getBacklinkView();
if (!backlinkView) {
return;
}
const patch = this.addChild(new MonkeyAroundComponent());
patch.registerMethodPatch({
methodName: 'addResult',
obj: getPrototypeOf(backlinkView.backlink.backlinkDom),
patchHandler: ({
fallback,
originalArgs: [file]
}) => {
const resultDomItem = fallback();
const fileNameCaptionEl = resultDomItem.el.querySelector('.tree-item-inner');
if (fileNameCaptionEl) {
fileNameCaptionEl.empty();
fileNameCaptionEl.appendChild(this.generateBacklinkTitle(file));
}
return resultDomItem;
}
});
}
private async refreshBacklinkPanels(): Promise<void> {
await this.reloadBacklinksView();
for (const leaf of this.app.workspace.getLeavesOfType(ViewType.Markdown)) {
if (!(leaf.view instanceof MarkdownView)) {
continue;
}
if (!leaf.view.backlinks) {
continue;
}
leaf.view.backlinks.recomputeBacklink(leaf.view.backlinks.file);
}
}
private async reloadBacklinksView(): Promise<void> {
const backlinkView = await this.getBacklinkView();
if (!backlinkView) {
return;
}
if (backlinkView.file) {
backlinkView.backlink.recomputeBacklink(backlinkView.file);
}
}
}

View file

@ -1,9 +1,3 @@
/**
* @file
*
* Tests for the main module default export.
*/
import {
describe,
expect,

View file

@ -1,9 +1,3 @@
/**
* @file
*
* Settings component that manages plugin settings persistence.
*/
import type { DataHandler } from 'obsidian-dev-utils/obsidian/data-handler';
import type { PluginEventSource } from 'obsidian-dev-utils/obsidian/plugin/plugin-event-source';

View file

@ -1,9 +1,3 @@
/**
* @file
*
* Tests for the PluginSettingsTab class.
*/
import type { PluginManifest } from 'obsidian';
import {
@ -21,15 +15,10 @@ import { PluginSettingsTab } from './plugin-settings-tab.ts';
import { Plugin } from './plugin.ts';
interface DisplayedTabResult {
names: string[];
tab: PluginSettingsTab;
readonly names: string[];
readonly tab: PluginSettingsTab;
}
/**
* Creates a PluginSettingsTab attached to a freshly created Plugin and calls display().
*
* @returns An object with the tab and the extracted setting names.
*/
function createDisplayedTab(): DisplayedTabResult {
const plugin = createPlugin();
const pluginSettingsComponent = Reflect.get(plugin, 'pluginSettingsComponent') as InstanceType<
@ -41,17 +30,11 @@ function createDisplayedTab(): DisplayedTabResult {
pluginSettingsComponent
});
// eslint-disable-next-line @typescript-eslint/no-deprecated -- Not ready to migrate `display()`.
tab.display();
tab.displayLegacy();
const names = getSettingNames(tab.containerEl);
return { names, tab };
}
/**
* Creates a Plugin instance with mocked dependencies.
*
* @returns The created Plugin.
*/
function createPlugin(): Plugin {
const app = App.createConfigured__();
const manifest: PluginManifest = {
@ -65,15 +48,6 @@ function createPlugin(): Plugin {
return new Plugin(app.asOriginalType__(), manifest);
}
/**
* Extracts setting names from the tab's containerEl after calling display().
*
* Each Setting mock creates a settingEl > [controlEl, infoEl > [nameEl, descEl]].
* The nameEl is the second-level div that contains the setting name text.
*
* @param containerEl - The container element.
* @returns The array of setting name strings.
*/
function getSettingNames(containerEl: HTMLElement): string[] {
const names: string[] = [];
for (const settingEl of containerEl.children) {
@ -90,10 +64,6 @@ function getSettingNames(containerEl: HTMLElement): string[] {
return names;
}
/**
* Patches missing mock properties on obsidian-test-mocks components
* that obsidian-dev-utils's `bind()` method probes via strict proxy.
*/
function patchMissingMockProperties(): void {
// Obsidian-dev-utils checks for setPlaceholderValue to detect text-based components.
if (!('setPlaceholderValue' in ToggleComponent.prototype)) {

View file

@ -6,9 +6,8 @@ import { SettingEx } from 'obsidian-dev-utils/obsidian/setting-ex';
import type { PluginSettings } from './plugin-settings.ts';
export class PluginSettingsTab extends PluginSettingsTabBase<PluginSettings> {
public override display(): void {
// eslint-disable-next-line @typescript-eslint/no-deprecated -- Not ready to migrate `display()`.
super.display();
public override displayLegacy(): void {
super.displayLegacy();
new Setting(this.containerEl)
.setName('Include extension')

View file

@ -1,9 +1,3 @@
/**
* @file
*
* Tests for PluginSettings default values.
*/
import {
describe,
expect,

View file

@ -1,45 +1,15 @@
/**
* @file
*
* Plugin settings data class with default values.
*/
/**
* Settings for the Backlink Full Path plugin.
*/
export class PluginSettings {
/**
* The depth of the path to include in backlinks (0 for unlimited).
*/
public pathDepth = 0;
/**
* The paths to be treated as root paths.
*/
public rootPaths: string[] = [];
/**
* Whether to display the parent path on a separate line.
*/
public shouldDisplayParentPathOnSeparateLine = false;
/**
* Whether to highlight the file name.
*/
public shouldHighlightFileName = true;
/**
* Whether to include the file extension in backlinks.
*/
public shouldIncludeExtension = true;
/**
* Whether to reverse the path parts.
*/
public shouldReversePathParts = false;
/**
* Whether to show ellipsis for skipped path parts.
*/
public shouldShowEllipsisForSkippedPathParts = true;
}

View file

@ -1,9 +1,3 @@
/**
* @file
*
* Tests for the Plugin class.
*/
import type {
BacklinkView,
ResultDom,
@ -48,16 +42,10 @@ interface SettingsComponentProxy {
}
interface TestPluginResult {
app: ReturnType<typeof App.createConfigured__>;
plugin: Plugin;
readonly app: ReturnType<typeof App.createConfigured__>;
readonly plugin: Plugin;
}
/**
* Creates a mock TFile for testing.
*
* @param path - The file path.
* @returns A mock TFile.
*/
function createMockFile(path: string): TFile {
const parts = path.split('/');
const name = parts.at(-1) ?? '';
@ -72,12 +60,6 @@ function createMockFile(path: string): TFile {
});
}
/**
* Creates a mock ResultDomItem with an element that has .tree-item-inner.
*
* @param hasTreeItemInner - Whether the result element should have a .tree-item-inner child.
* @returns The mock ResultDomItem.
*/
function createMockResultDomItem(hasTreeItemInner: boolean): ResultDomItem {
const el = activeDocument.createElement('div');
if (hasTreeItemInner) {
@ -89,11 +71,6 @@ function createMockResultDomItem(hasTreeItemInner: boolean): ResultDomItem {
return mockProxy<ResultDomItem>({ el });
}
/**
* Creates a minimal PluginManifest for testing.
*
* @returns A test PluginManifest.
*/
function createTestManifest(): PluginManifest {
return {
author: 'test',
@ -105,11 +82,6 @@ function createTestManifest(): PluginManifest {
};
}
/**
* Creates a test Plugin instance with a configured App.
*
* @returns The plugin and app.
*/
function createTestPlugin(): TestPluginResult {
const app = App.createConfigured__();
const manifest = createTestManifest();
@ -117,49 +89,22 @@ function createTestPlugin(): TestPluginResult {
return { app, plugin };
}
/**
* Gets the plugin's private pluginSettingsComponent via Reflect.
*
* @param plugin - The plugin instance.
* @returns The pluginSettingsComponent.
*/
function getSettingsComponent(plugin: Plugin): SettingsComponentProxy {
return Reflect.get(plugin, 'pluginSettingsComponent') as SettingsComponentProxy;
}
/**
* Marks the plugin and its internal wrapperComponent as loaded
* without going through the full load lifecycle.
*
* @param plugin - The plugin to mark as loaded.
*/
function markPluginLoaded(plugin: Plugin): void {
Reflect.set(bypassStrictProxy(plugin), '_loaded', true);
const wrapperComponent = Reflect.get(bypassStrictProxy(plugin), 'wrapperComponent') as object;
Reflect.set(bypassStrictProxy(wrapperComponent), '_loaded', true);
}
/**
* Sets up internalPlugins mock on the plugin's app.
*
* @param plugin - The plugin.
* @param returnValue - The value to return from getPluginById.
* @returns The mocked getPluginById function.
*/
function mockInternalPlugins(plugin: Plugin, returnValue: unknown): ReturnType<typeof vi.fn> {
const getPluginById = vi.fn().mockReturnValue(returnValue);
Reflect.set(bypassStrictProxy(plugin.app), 'internalPlugins', strictProxy({ getPluginById }));
return getPluginById;
}
/**
* Mocks `obsidianDevUtilsState` on both the plugin's app and the
* global `app` so that `super.onload()` can complete without errors.
* The library accesses state via the passed app instance and via the
* global `getApp()` fallback (`globalThis.app`).
*
* @param plugin - The plugin whose app needs the mock.
*/
function mockObsidianDevUtilsState(plugin: Plugin): void {
Reflect.set(bypassStrictProxy(plugin.app), 'obsidianDevUtilsState', {});
const globalApp = Reflect.get(window, 'app') as unknown;
@ -168,25 +113,11 @@ function mockObsidianDevUtilsState(plugin: Plugin): void {
}
}
/**
* Creates a strict proxy, working around `PartialDeep<T>` incompatibility
* with complex DOM types by casting the partial before passing to strictProxy.
*
* @param partial - A partial object with the mocked members.
* @returns A strict proxy typed as T.
*/
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is used in the body for strictProxy<T>.
function mockProxy<T>(partial: object): T {
return strictProxy<T>(partial as StrictProxyPartial<T>);
}
/**
* Calls a private/protected method on an object via Reflect and binds it.
*
* @param obj - The object.
* @param name - The method name.
* @returns The bound method.
*/
function privateMethod(obj: object, name: string): (...args: never[]) => unknown {
const fn = Reflect.get(obj, name) as (...args: never[]) => unknown;
return fn.bind(obj);
@ -404,12 +335,6 @@ describe('Plugin', () => {
settings = getSettingsComponent(plugin).settings;
});
/**
* Calls the private generateBacklinkTitle method.
*
* @param file - The TFile to generate a title for.
* @returns The generated HTMLDivElement.
*/
function callGenerateBacklinkTitle(file: TFile): HTMLDivElement {
const fn = privateMethod(plugin, 'generateBacklinkTitle');
return (fn as (...args: unknown[]) => unknown)(file) as HTMLDivElement;

View file

@ -1,62 +1,15 @@
/**
* @file
*
* Main plugin class for the Backlink Full Path plugin.
*/
import type {
BacklinkPlugin,
BacklinkView,
ResultDom,
ResultDomItem,
ResultDomResult
} from '@obsidian-typings/obsidian-public-latest';
import type {
App,
PluginManifest,
TFile
} from 'obsidian';
import {
InternalPluginName,
ViewType
} from '@obsidian-typings/obsidian-public-latest/implementations';
import {
MarkdownView,
setTooltip
} from 'obsidian';
import { invokeAsyncSafely } from 'obsidian-dev-utils/async';
import { getPrototypeOf } from 'obsidian-dev-utils/object-utils';
import { MonkeyAroundComponent } from 'obsidian-dev-utils/obsidian/components/monkey-around-component';
import { PluginSettingsTabComponent } from 'obsidian-dev-utils/obsidian/components/plugin-settings-tab-component';
import { PluginDataHandler } from 'obsidian-dev-utils/obsidian/data-handler';
import { PluginBase } from 'obsidian-dev-utils/obsidian/plugin/plugin';
import { PluginEventSourceImpl } from 'obsidian-dev-utils/obsidian/plugin/plugin-event-source';
import { BacklinkFullPathComponent } from './backlink-full-path-component.ts';
import { PluginSettingsComponent } from './plugin-settings-component.ts';
import { PluginSettingsTab } from './plugin-settings-tab.ts';
/**
* Type alias for the `addResult` method on `ResultDom`.
*/
type AddResultFn = ResultDom['addResult'];
/**
* The Backlink Full Path plugin. Replaces backlink titles with full paths.
*/
export class Plugin extends PluginBase {
private readonly pluginSettingsComponent: PluginSettingsComponent;
/**
* Creates a new Backlink Full Path plugin.
*
* @param app - The Obsidian app instance.
* @param manifest - The plugin manifest.
*/
public constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
this.pluginSettingsComponent = this.addChild(
protected override onloadImpl(): void {
const pluginSettingsComponent = this.addChild(
new PluginSettingsComponent({
dataHandler: new PluginDataHandler(this),
pluginEventSource: new PluginEventSourceImpl(this)
@ -68,183 +21,16 @@ export class Plugin extends PluginBase {
plugin: this,
pluginSettingsTab: new PluginSettingsTab({
plugin: this,
pluginSettingsComponent: this.pluginSettingsComponent
pluginSettingsComponent
})
})
);
}
/**
* Called after pre-loaded components are initialized.
*/
public override async onload(): Promise<void> {
await super.onload();
this.pluginSettingsComponent.on('saveSettings', async () => {
await this.refreshBacklinkPanels();
});
}
/**
* Called when the workspace layout is ready.
*/
protected async onLayoutReady(): Promise<void> {
const backlinksCorePlugin = this.app.internalPlugins.getPluginById(InternalPluginName.Backlink);
if (!backlinksCorePlugin) {
return;
}
const that = this;
const patch = this.addChild(new MonkeyAroundComponent());
patch.registerPatch(getPrototypeOf(backlinksCorePlugin.instance), {
onUserEnable: (next: () => void) => {
return function onUserEnablePatched(this: BacklinkPlugin): void {
next.call(this);
that.onBacklinksCorePluginEnable();
};
}
});
if (backlinksCorePlugin.enabled) {
await this.patchBacklinksPane();
await this.refreshBacklinkPanels();
}
this.register(() => {
invokeAsyncSafely(async () => {
await this.refreshBacklinkPanels();
});
});
}
private addResult(next: AddResultFn, resultDom: ResultDom, file: TFile, result: ResultDomResult, content: string, shouldShowTitle?: boolean): ResultDomItem {
const resultDomItem = next.call(resultDom, file, result, content, shouldShowTitle);
const fileNameCaptionEl = resultDomItem.el.querySelector('.tree-item-inner');
if (fileNameCaptionEl) {
fileNameCaptionEl.empty();
fileNameCaptionEl.appendChild(this.generateBacklinkTitle(file));
}
return resultDomItem;
}
private generateBacklinkTitle(file: TFile): HTMLDivElement {
const fileNamePart = this.pluginSettingsComponent.settings.shouldIncludeExtension ? file.name : file.basename;
let parentPathParts = file.path.split('/').slice(0, -1);
for (let length = parentPathParts.length; length >= 1; length--) {
const rootPath = parentPathParts.slice(0, length).join('/');
if (this.pluginSettingsComponent.settings.rootPaths.includes(rootPath)) {
parentPathParts = parentPathParts.slice(length);
break;
}
}
if (this.pluginSettingsComponent.settings.pathDepth > 0) {
const partsToSkipCount = Math.max(0, parentPathParts.length - this.pluginSettingsComponent.settings.pathDepth + 1);
if (partsToSkipCount > 0) {
parentPathParts.splice(0, partsToSkipCount);
if (this.pluginSettingsComponent.settings.shouldShowEllipsisForSkippedPathParts) {
parentPathParts.unshift('...');
}
}
}
if (this.pluginSettingsComponent.settings.shouldReversePathParts) {
parentPathParts.reverse();
}
const pathSeparator = this.pluginSettingsComponent.settings.shouldReversePathParts ? ' \u2190 ' : '/';
const parentStr = parentPathParts.join(pathSeparator);
const container = createDiv({
cls: ['backlink-full-path', 'backlink-control']
});
container.dataset['shouldHighlightFileName'] = this.pluginSettingsComponent.settings.shouldHighlightFileName.toString();
container.dataset['shouldDisplayParentPathOnSeparateLine'] = this.pluginSettingsComponent.settings.shouldDisplayParentPathOnSeparateLine.toString();
container.createSpan({
cls: 'full-path',
text: file.path
});
const shadowRoot = container.attachShadow({ mode: 'open' });
setTooltip(container, file.path);
shadowRoot.createSpan({
attr: {
part: 'file-name'
},
text: fileNamePart
});
if (parentStr) {
let text = parentStr;
if (!this.pluginSettingsComponent.settings.shouldDisplayParentPathOnSeparateLine) {
text = this.pluginSettingsComponent.settings.shouldReversePathParts ? pathSeparator + text : text + pathSeparator;
}
shadowRoot.createSpan({
attr: {
part: 'parent-path'
},
prepend: !this.pluginSettingsComponent.settings.shouldReversePathParts && !this.pluginSettingsComponent.settings.shouldDisplayParentPathOnSeparateLine,
text
});
}
return container;
}
private async getBacklinkView(): Promise<BacklinkView | null> {
const backlinksLeaf = this.app.workspace.getLeavesOfType(ViewType.Backlink)[0];
if (!backlinksLeaf) {
return null;
}
await backlinksLeaf.loadIfDeferred();
return backlinksLeaf.view as BacklinkView;
}
private onBacklinksCorePluginEnable(): void {
invokeAsyncSafely(() => this.patchBacklinksPane());
}
private async patchBacklinksPane(): Promise<void> {
const backlinkView = await this.getBacklinkView();
if (!backlinkView) {
return;
}
const that = this;
const patch = this.addChild(new MonkeyAroundComponent());
patch.registerPatch(getPrototypeOf(backlinkView.backlink.backlinkDom), {
addResult: (next: AddResultFn): AddResultFn => {
return function addResultPatched(this: ResultDom, file: TFile, result: ResultDomResult, content: string, shouldShowTitle?: boolean): ResultDomItem {
return that.addResult(next, this, file, result, content, shouldShowTitle);
};
}
});
}
private async refreshBacklinkPanels(): Promise<void> {
await this.reloadBacklinksView();
for (const leaf of this.app.workspace.getLeavesOfType(ViewType.Markdown)) {
if (!(leaf.view instanceof MarkdownView)) {
continue;
}
if (!leaf.view.backlinks) {
continue;
}
leaf.view.backlinks.recomputeBacklink(leaf.view.backlinks.file);
}
}
private async reloadBacklinksView(): Promise<void> {
const backlinkView = await this.getBacklinkView();
if (!backlinkView) {
return;
}
if (backlinkView.file) {
backlinkView.backlink.recomputeBacklink(backlinkView.file);
}
this.addChild(
new BacklinkFullPathComponent({
app: this.app,
pluginSettingsComponent
})
);
}
}