diff --git a/src/settings.ts b/src/settings.ts index 288f9c5..1f8abe7 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,6 @@ import {App, PluginSettingTab, Setting, Notice, TextComponent} from 'obsidian'; import GitLabFilesPush from "./main"; +import {FolderSuggest} from "./ui/FolderSuggest"; // Minimal shape of Obsidian >= 1.13's SettingDefinitionItem. Declared locally so // the plugin still type-checks against older Obsidian typings (minAppVersion @@ -162,25 +163,29 @@ export class GitLabSyncSettingTab extends PluginSettingTab { new Setting(containerEl) .setName('Root path') .setDesc('Optional: subfolder in repository (e.g. "notes")') - .addText(text => text - .setPlaceholder('Enter subfolder path') - .setValue(this.plugin.settings.rootPath) - .onChange((value) => { - this.plugin.settings.rootPath = value.replace(/^\/|\/$/g, ''); - void this.plugin.saveSettings(); - this.plugin.initializeGitService(); - })); + .addText(text => { + text.setPlaceholder('Enter subfolder path') + .setValue(this.plugin.settings.rootPath) + .onChange((value) => { + this.plugin.settings.rootPath = value.replace(/^\/|\/$/g, ''); + void this.plugin.saveSettings(); + this.plugin.initializeGitService(); + }); + FolderSuggest.attach(this.app, text.inputEl); + }); new Setting(containerEl) .setName('Vault folder') .setDesc('Optional: only sync files in this vault folder (e.g. "sync" to only sync files in the sync folder)') - .addText(text => text - .setPlaceholder('Leave empty to sync all files') - .setValue(this.plugin.settings.vaultFolder) - .onChange((value) => { - this.plugin.settings.vaultFolder = value.replace(/^\/|\/$/g, ''); - void this.plugin.saveSettings(); - })); + .addText(text => { + text.setPlaceholder('Leave empty to sync all files') + .setValue(this.plugin.settings.vaultFolder) + .onChange((value) => { + this.plugin.settings.vaultFolder = value.replace(/^\/|\/$/g, ''); + void this.plugin.saveSettings(); + }); + FolderSuggest.attach(this.app, text.inputEl); + }); // "Real symlink" needs the Git Data API, which only GitHub offers. For // other providers, offer follow/skip only so the option can't mislead. diff --git a/src/ui/FolderSuggest.ts b/src/ui/FolderSuggest.ts new file mode 100644 index 0000000..b96eebc --- /dev/null +++ b/src/ui/FolderSuggest.ts @@ -0,0 +1,38 @@ +import {AbstractInputSuggest, App, TFolder} from 'obsidian'; + +/** + * Type-ahead folder suggester for a settings text input. Suggests existing + * vault folders but never forces a selection, since callers (e.g. the "Root + * path" repo setting) may need to accept a path that doesn't exist locally. + */ +export class FolderSuggest extends AbstractInputSuggest { + constructor(app: App, private readonly inputEl: HTMLInputElement) { + super(app, inputEl); + } + + protected getSuggestions(query: string): TFolder[] { + const lowerQuery = query.toLowerCase(); + return this.app.vault.getAllFolders(true) + .filter(folder => folder.path.toLowerCase().contains(lowerQuery)) + .sort((a, b) => a.path.localeCompare(b.path)); + } + + renderSuggestion(folder: TFolder, el: HTMLElement): void { + el.setText(folder.path === '/' ? '/' : folder.path); + } + + selectSuggestion(folder: TFolder): void { + const path = folder.path === '/' ? '' : folder.path; + this.setValue(path); + // TextComponent listens for the native "input" event to fire onChange, + // so dispatch one to trigger the existing save/initializeGitService flow. + this.inputEl.dispatchEvent(new Event('input')); + this.close(); + } + + /** Attaches a FolderSuggest to `inputEl`; the instance self-registers via the base class, so the caller has nothing to hold onto. */ + static attach(app: App, inputEl: HTMLInputElement): void { + // eslint-disable-next-line sonarjs/constructor-for-side-effects -- AbstractInputSuggest wires itself to inputEl in its constructor; there's nothing to assign. + new FolderSuggest(app, inputEl); + } +} diff --git a/tests/setup.ts b/tests/setup.ts index d7f73dc..597b99c 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -54,6 +54,17 @@ export const App = class { }; export const TFile = class {}; +export const TFolder = class {}; +export const AbstractInputSuggest = class { + app: unknown; + constructor(app: unknown) { + this.app = app; + } + setValue() {} + getValue() { return ''; } + close() {} + open() {} +}; export const requestUrl = vi.fn(); export const setTooltip = vi.fn(); export const setIcon = vi.fn(); @@ -68,6 +79,8 @@ vi.mock('obsidian', () => ({ Editor, App, TFile, + TFolder, + AbstractInputSuggest, requestUrl, setTooltip, setIcon,