Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/gui/GenericSuggester/genericSuggester.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { beforeEach, describe, expect, it } from "vitest";
import { App } from "obsidian";
import GenericSuggester from "./genericSuggester";

describe("GenericSuggester", () => {
let app: App;

beforeEach(() => {
app = new App();
});

it("normalizes non-string display items for fuzzy matching", () => {
const items = [{ id: 1 }, { id: 2 }];
const displayItems = items as unknown as string[];

const suggester = new GenericSuggester(app, displayItems, items);

expect(() => suggester.getSuggestions("1")).not.toThrow();
});

});
43 changes: 39 additions & 4 deletions src/gui/GenericSuggester/genericSuggester.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { FuzzySuggestModal } from "obsidian";
import type { FuzzyMatch, App } from "obsidian";
import { log, toError } from "src/logger/logManager";
import { normalizeDisplayItem } from "../suggesters/utils";

type SuggestRender<T> = (value: T, el: HTMLElement) => void;

Expand All @@ -10,6 +12,9 @@ export default class GenericSuggester<T> extends FuzzySuggestModal<T> {
private resolved: boolean;

private renderItem?: SuggestRender<T>;
private displayItems: string[];
private items: T[];
private warnedOnEmptyDisplay = false;


public static Suggest<T>(
Expand All @@ -26,13 +31,15 @@ export default class GenericSuggester<T> extends FuzzySuggestModal<T> {

public constructor(
app: App,
private displayItems: string[],
private items: T[],
displayItems: string[],
items: T[],
renderItem?: SuggestRender<T>,
) {
super(app);

this.renderItem = renderItem;
this.items = items;
this.displayItems = displayItems.map((value) => normalizeDisplayItem(value));

this.promise = new Promise<T>((resolve, reject) => {
this.resolvePromise = resolve;
Expand All @@ -58,11 +65,21 @@ export default class GenericSuggester<T> extends FuzzySuggestModal<T> {
this.inputEl.value = values[selectedItem].item ?? value;
});

if (this.displayItems.length !== this.items.length) {
this.displayItems = this.items.map((item, index) => {
const displayItem = this.displayItems[index];
return normalizeDisplayItem(displayItem ?? item);
});
}

this.warnIfEmptyDisplay();
this.open();
}

getItemText(item: T): string {
return this.displayItems[this.items.indexOf(item)];
const index = this.items.indexOf(item);
const displayItem = index >= 0 ? this.displayItems[index] : undefined;
return normalizeDisplayItem(displayItem ?? item);
}

getItems(): T[] {
Expand All @@ -87,8 +104,11 @@ export default class GenericSuggester<T> extends FuzzySuggestModal<T> {
try {
el.empty();
this.renderItem(value.item, el);
} catch {
} catch (error) {
// Fallback to default rendering if custom render throws
const err = toError(error);
err.message = `Custom renderItem threw an error; falling back to default rendering. ${err.message}`;
log.logWarning(err);
el.empty();
super.renderSuggestion(value, el);
}
Expand All @@ -104,4 +124,19 @@ export default class GenericSuggester<T> extends FuzzySuggestModal<T> {

if (!this.resolved) this.rejectPromise("no input given.");
}

private warnIfEmptyDisplay(): void {
if (this.warnedOnEmptyDisplay) return;

const hasEmptyDisplay = this.displayItems.some(
(displayItem) => displayItem.length === 0,
);

if (hasEmptyDisplay) {
this.warnedOnEmptyDisplay = true;
log.logWarning(
"QuickAdd suggester received empty display values. Check your displayItems mapping.",
);
}
}
}
48 changes: 43 additions & 5 deletions src/gui/InputSuggester/inputSuggester.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { FuzzySuggestModal } from "obsidian";
import type { FuzzyMatch, App } from "obsidian";
import { log, toError } from "src/logger/logManager";
import { normalizeDisplayItem } from "../suggesters/utils";

type SuggestRender<T> = (value: T, el: HTMLElement) => void;

Expand All @@ -24,6 +26,9 @@ export default class InputSuggester extends FuzzySuggestModal<string> {
private resolved: boolean;

private renderItem?: SuggestRender<string>;
private displayItems: string[];
private items: string[];
private warnedOnEmptyDisplay = false;

public static Suggest(
app: App,
Expand All @@ -42,12 +47,15 @@ export default class InputSuggester extends FuzzySuggestModal<string> {

public constructor(
app: App,
private displayItems: string[],
private items: string[],
displayItems: string[],
items: string[],
options: Partial<Options> = {}
) {
super(app);

this.items = items;
this.displayItems = displayItems.map((value) => normalizeDisplayItem(value));

this.promise = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
this.rejectPromise = reject;
Expand Down Expand Up @@ -75,17 +83,29 @@ export default class InputSuggester extends FuzzySuggestModal<string> {
});

if (options.placeholder) this.setPlaceholder(options.placeholder);
if (options.limit) this.limit = options.limit;
if (typeof options.limit === "number") {
this.limit = options.limit;
}
if (options.emptyStateText)
this.emptyStateText = options.emptyStateText;

if (this.displayItems.length !== this.items.length) {
this.displayItems = this.items.map((item, index) => {
const displayItem = this.displayItems[index];
return normalizeDisplayItem(displayItem ?? item);
});
}
Comment thread
chhoumann marked this conversation as resolved.

this.warnIfEmptyDisplay();
this.open();
}

getItemText(item: string): string {
if (item === this.inputEl.value) return item;

return this.displayItems[this.items.indexOf(item)];
const index = this.items.indexOf(item);
const displayItem = index >= 0 ? this.displayItems[index] : undefined;
return normalizeDisplayItem(displayItem ?? item);
}

getItems(): string[] {
Expand Down Expand Up @@ -138,7 +158,10 @@ export default class InputSuggester extends FuzzySuggestModal<string> {
try {
el.empty();
this.renderItem(value.item, el);
} catch {
} catch (error) {
const err = toError(error);
err.message = `Custom renderItem threw an error; falling back to default rendering. ${err.message}`;
log.logWarning(err);
el.empty();
super.renderSuggestion(value, el);
}
Expand All @@ -154,4 +177,19 @@ export default class InputSuggester extends FuzzySuggestModal<string> {

if (!this.resolved) this.rejectPromise("no input given.");
}

private warnIfEmptyDisplay(): void {
if (this.warnedOnEmptyDisplay) return;

const hasEmptyDisplay = this.displayItems.some(
(displayItem) => displayItem.length === 0,
);

if (hasEmptyDisplay) {
this.warnedOnEmptyDisplay = true;
log.logWarning(
"QuickAdd suggester received empty display values. Check your displayItems mapping.",
);
}
}
}
6 changes: 6 additions & 0 deletions src/gui/suggesters/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
* Utility functions for text manipulation in suggesters
*/

export function normalizeDisplayItem(value: unknown): string {
if (typeof value === "string") return value;
if (value == null) return "";
return String(value);
}

/**
* Insert text at cursor position
*/
Expand Down