Skip to content

Commit

Permalink
feat: hotkeys for quickly opening top nth suggestions #124
Browse files Browse the repository at this point in the history
  • Loading branch information
darlal committed Sep 14, 2024
1 parent 6b2428e commit 7199325
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 11 deletions.
10 changes: 10 additions & 0 deletions src/settings/__tests__/switcherPlusSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ function getDefaultSettingsData(): SettingsData {
renderHeadings: false,
toggleContentRenderingKeys: { modifiers: ['Shift', 'Ctrl'], key: 'm' },
},
quickOpen: {
isEnabled: true,
modifiers: ['Alt'],
keyList: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
},
};

return data;
Expand Down Expand Up @@ -291,6 +296,11 @@ function getTransientSettingsData(): SettingsData {
renderHeadings: false,
toggleContentRenderingKeys: { modifiers: ['Shift', 'Ctrl'], key: 'm' },
},
quickOpen: {
isEnabled: true,
modifiers: chance.pickset(['Alt', 'Ctrl'], 1),
keyList: [chance.letter()],
},
};

return data;
Expand Down
14 changes: 14 additions & 0 deletions src/settings/switcherPlusSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Mode,
NavigationKeysConfig,
PathDisplayFormat,
QuickOpenConfig,
RelationType,
RenderMarkdownContentConfig,
SettingsData,
Expand Down Expand Up @@ -147,6 +148,11 @@ export class SwitcherPlusSettings {
renderHeadings: false,
toggleContentRenderingKeys: { modifiers: ['Shift', 'Ctrl'], key: 'm' },
},
quickOpen: {
isEnabled: true,
modifiers: ['Alt'],
keyList: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
},
};
}

Expand Down Expand Up @@ -678,6 +684,14 @@ export class SwitcherPlusSettings {
this.data.renderMarkdownContentInSuggestions = value;
}

get quickOpen(): QuickOpenConfig {
return this.data.quickOpen;
}

set quickOpen(value: QuickOpenConfig) {
this.data.quickOpen = value;
}

constructor(private plugin: SwitcherPlusPlugin) {
this.data = SwitcherPlusSettings.defaults;
}
Expand Down
93 changes: 84 additions & 9 deletions src/switcherPlus/__tests__/switcherPlusKeymap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
CommandSuggestion,
SuggestionType,
SymbolType,
QuickOpenConfig,
} from 'src/types';
import { makeHeading, makeHeadingSuggestion, makeSymbolSuggestion } from '@fixtures';

Expand All @@ -58,8 +59,9 @@ describe('SwitcherPlusKeymap', () => {
const selector = '.prompt-instructions';
const mockScope = mock<Scope>({ keys: [] });
const mockChooser = mock<Chooser<AnySuggestion>>();
const mockConfig = mock<SwitcherPlusSettings>({ closeWhenEmptyKeys: [] });
const mockWorkspace = mock<Workspace>();
const config = new SwitcherPlusSettings(null);
const mockConfig = mock<SwitcherPlusSettings>({ closeWhenEmptyKeys: [] });

// The .prompt-instruction wrapper element
const mockInstructionEl = mock<HTMLDivElement>();
Expand Down Expand Up @@ -1223,6 +1225,85 @@ describe('SwitcherPlusKeymap', () => {
});
});

describe('QuickOpen Hotkeys', () => {
let sut: SwitcherPlusKeymap;
let mockChooserLocal: MockProxy<Chooser<CommandSuggestion>>;
let mockQuickOpenConfig: QuickOpenConfig;

beforeAll(() => {
mockChooserLocal = mock<Chooser<CommandSuggestion>>();
mockQuickOpenConfig = mock<QuickOpenConfig>({
isEnabled: true,
modifiers: ['Alt'],
keyList: ['1'],
});

sut = new SwitcherPlusKeymap(
mockApp,
mockScope,
mockChooserLocal,
mockModal,
mockConfig,
);

mockConfig.quickOpen = mockQuickOpenConfig;
mockReset(mockScope);
});

afterAll(() => {
mockConfig.quickOpen = null;
});

afterEach(() => {
mockReset(mockScope);
});

it('should register the Quick Open hotkeys', () => {
sut.registerQuickOpenBindings(mockScope, mockConfig);

expect(mockScope.register).toHaveBeenCalledWith(
mockQuickOpenConfig.modifiers,
mockQuickOpenConfig.keyList[0],
expect.any(Function),
);
});

test('.quickOpenByIndex() should return false to prevent default', () => {
const result = sut.quickOpenByIndex(null, mock<KeymapContext>());
expect(result).toBe(false);
});

test('.quickOpenByIndex() should open the item at the index number associated with the key pressed', () => {
const mockEvt = mock<KeyboardEvent>();
mockChooserLocal.values = [mock<CommandSuggestion>()];

// Use the same key from the config keyList
const vkey = mockQuickOpenConfig.keyList[0];
const mockCtx = mock<KeymapContext>({ vkey });

sut.quickOpenByIndex(mockEvt, mockCtx);

expect(mockChooserLocal.setSelectedItem).toHaveBeenCalledWith(0, mockEvt);
expect(mockChooserLocal.useSelectedItem).toHaveBeenCalledWith(mockEvt);
});

test('.renderQuickOpenFlairIcons() should add flair icons to existing suggestion div elements', () => {
const mockFlairContainer = mock<HTMLDivElement>();

const mockSuggEl = mock<HTMLDivElement>();
mockSuggEl.createDiv.mockReturnValueOnce(mockFlairContainer);

sut.renderQuickOpenFlairIcons([mockSuggEl], mockConfig);

expect(mockFlairContainer.createEl).toHaveBeenCalledWith(
'kbd',
expect.objectContaining({
cls: ['suggestion-hotkey', 'qsp-quick-open-hotkey'],
}),
);
});
});

describe('Toggle pinned state for Commands', () => {
const commandId = 'testCommandId';
let sut: SwitcherPlusKeymap;
Expand Down Expand Up @@ -1262,7 +1343,7 @@ describe('SwitcherPlusKeymap', () => {
mockScope,
mockChooserLocal,
mockModal,
mockConfig,
config,
);
});

Expand Down Expand Up @@ -1336,13 +1417,7 @@ describe('SwitcherPlusKeymap', () => {

beforeAll(() => {
file = new TFile();
sut = new SwitcherPlusKeymap(
mockApp,
mockScope,
mockChooser,
mockModal,
mockConfig,
);
sut = new SwitcherPlusKeymap(mockApp, mockScope, mockChooser, mockModal, config);

mockTitleEl = mock<HTMLElement>();
mockSuggParentEl = mock<HTMLDivElement>({
Expand Down
1 change: 1 addition & 0 deletions src/switcherPlus/modeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ export class ModeHandler {
if (suggs?.length) {
chooser.setSuggestions(suggs);
ModeHandler.setActiveSuggestion(mode, chooser);
this.exKeymap?.renderQuickOpenFlairIcons(chooser.suggestions, this.settings);
} else {
if (
this.noResultActionModes.includes(mode) &&
Expand Down
88 changes: 87 additions & 1 deletion src/switcherPlus/switcherPlusKeymap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export class SwitcherPlusKeymap {
this.registerNavigationBindings(scope, config.navigationKeys);
this.registerEditorTabBindings(scope);
this.registerCloseWhenEmptyBindings(scope, config);
this.registerQuickOpenBindings(scope, config);
this.renderModeTriggerInstructions(modal.modalEl, config);

this.standardInstructionsEl =
Expand Down Expand Up @@ -251,6 +252,75 @@ export class SwitcherPlusKeymap {
});
}

/**
* Registers the hotkeys for selecting a suggestion and opening it using its index number in the array.
*
* @param {Scope} scope
* @param {SwitcherPlusSettings} config
*/
registerQuickOpenBindings(scope: Scope, config: SwitcherPlusSettings): void {
const { isEnabled, modifiers, keyList } = config.quickOpen;

if (isEnabled) {
keyList?.forEach((key) => {
scope.register(modifiers, key, this.quickOpenByIndex.bind(this));
});
}
}

/**
* Uses the vkey from KeymapContext and the QuickOpenConfig keyList to identify the
* index of a suggestion in the chooser to open.
*
* @param {KeyboardEvent} evt
* @param {KeymapContext} ctx
* @returns {false}
*/
quickOpenByIndex(evt: KeyboardEvent, ctx: KeymapContext): false {
// The index of the key in the keyList array will be used to identify the
// suggestion to open from the values array in the chooser.
const index = this.config.quickOpen.keyList.indexOf(ctx.vkey);

if (index !== -1) {
const { chooser } = this;

if (chooser.values.length > index) {
chooser.setSelectedItem(index, evt);
this.useSelectedItem(evt, ctx);
}
}

return false; // Return false to prevent default.
}

/**
* Adds a flair icon element to the first nth suggestionElements displaying the hotkey
* that can be used to select and open that suggestion.
*
* @param {HTMLDivElement[]} suggestionElements Array of already rendered Suggestion elements.
* @param {SwitcherPlusSettings} config
*/
renderQuickOpenFlairIcons(
suggestionElements: HTMLDivElement[],
config: SwitcherPlusSettings,
): void {
const { isEnabled, modifiers, keyList } = config.quickOpen;

if (isEnabled) {
for (let i = 0; i < keyList.length && i < suggestionElements.length; i++) {
const key = keyList[i];
const parentEl = suggestionElements[i];
const containerEl = parentEl.createDiv({ cls: 'qsp-quick-open-aux' });

// Create the hotkey flair element.
containerEl?.createEl('kbd', {
cls: ['suggestion-hotkey', 'qsp-quick-open-hotkey'],
text: SwitcherPlusKeymap.commandDisplayStr(modifiers, key),
});
}
}
}

updateInsertIntoEditorCommand(
mode: Mode,
activeEditor: WorkspaceLeaf,
Expand Down Expand Up @@ -799,8 +869,9 @@ export class SwitcherPlusKeymap {
return false;
}

useSelectedItem(evt: KeyboardEvent, _ctx: KeymapContext): boolean | void {
useSelectedItem(evt: KeyboardEvent, _ctx: KeymapContext): false {
this.chooser.useSelectedItem(evt);
return false; // Return false to prevent default.
}

insertIntoEditorAsLink(
Expand Down Expand Up @@ -951,6 +1022,21 @@ export class SwitcherPlusKeymap {
Mode.SymbolList,
];

// Display instructions for opening top nth suggestions using a dedicated hotkey
const { quickOpen } = this.config;
const openKeyList = quickOpen?.keyList;
if (openKeyList?.length) {
const quickOpenKeyStr = `${openKeyList[0]}~${openKeyList[openKeyList.length - 1]}`;
this.createCustomKeymap(
'open nth item',
[Mode.CommandList, Mode.VaultList, Mode.WorkspaceList, ...customFileBasedModes],
{ modifiers: quickOpen.modifiers, key: quickOpenKeyStr },
null,
quickOpen.isEnabled,
true,
);
}

// Builtin keymap to open file in a new tab.
this.createCustomKeymap(
'open in new tab',
Expand Down
19 changes: 19 additions & 0 deletions src/types/sharedTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,21 @@ export type RenderMarkdownContentConfig = {
toggleContentRenderingKeys: Hotkey;
};

export type QuickOpenConfig = {
/**
* Whether or not the feature is turned on.
*/
isEnabled: boolean;
/**
* The modifiers to use for the trigger hotkey.
*/
modifiers: Modifier[];
/**
* Array of single characters to be used along with modifiers for the trigger hotkey.
*/
keyList: string[];
};

export interface SettingsData {
version: string;
onOpenPreferNewTab: boolean;
Expand Down Expand Up @@ -452,6 +467,10 @@ export interface SettingsData {
* Configuration for how markdown content found in suggestions should be displayed
*/
renderMarkdownContentInSuggestions: RenderMarkdownContentConfig;
/**
* Configuration for mapping hotkeys to select nth items from the suggestion list
*/
quickOpen: QuickOpenConfig;
}

export type SessionOpts = {
Expand Down
13 changes: 12 additions & 1 deletion styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@
/* flair icon for suggestions that represent an alias */
.qsp-alias-indicator {}

/* Quick Open indicator container element */
.qsp-quick-open-aux {
display: flex;
align-items: center;
align-self: center;
flex-shrink: 0;
}

/* Quick Open hotkey indicator element */
.qsp-quick-open-hotkey {}

/* headings level */
.qsp-headings-l1 {}
.qsp-headings-l2 {}
Expand All @@ -198,5 +209,5 @@
/* Prompt instructions element for facets in custom modes */
.qsp-prompt-instructions-facets {}

/* Prompt instructions element for mode triggers*/
/* Prompt instructions element for mode triggers */
.qsp-prompt-instructions-modes {}

0 comments on commit 7199325

Please sign in to comment.