Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions packages/lib/services/KeymapService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,24 @@ describe('services_KeymapService', () => {
keymapService.registerCommandAccelerator('some-command', null);
expect(keymapService.getAriaKeyShortcuts('some-command')).toBe(undefined);
});

describe('validateKeymap', () => {
beforeEach(() => keymapService.initialize([], 'linux'));

it('should throw when proposed accelerator conflicts with an existing command', () => {
// Ctrl+N is the default for newNote
expect(() => keymapService.validateKeymap({ accelerator: 'Ctrl+N', command: 'synchronize' })).toThrow();
});

it('should not throw when proposed accelerator is unique', () => {
expect(() => keymapService.validateKeymap({ accelerator: 'Ctrl+Shift+Alt+F12', command: 'synchronize' })).not.toThrow();
});

it('should not throw for pre-existing conflicts when checking an unrelated command', () => {
// Force a duplicate in the keymap directly, simulating a pre-existing conflict
keymapService.setAccelerator('newTodo', 'Ctrl+N'); // newNote also has Ctrl+N
// The pre-existing newNote/newTodo conflict should not block saving an unrelated shortcut
expect(() => keymapService.validateKeymap({ accelerator: 'Ctrl+Shift+Alt+F12', command: 'synchronize' })).not.toThrow();
});
});
});
31 changes: 22 additions & 9 deletions packages/lib/services/KeymapService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,25 +350,38 @@ export default class KeymapService extends BaseService {
}

public validateKeymap(proposedKeymapItem: KeymapItem = null) {
const usedAccelerators = new Set();
if (proposedKeymapItem) {
// When checking a proposed change, only throw if the proposed accelerator
// conflicts with another command. Pre-existing conflicts between other commands
// should not block the user from saving an unrelated shortcut change.
for (const item of Object.values(this.keymap)) {
if (item.command === proposedKeymapItem.command) continue;
if (item.accelerator === proposedKeymapItem.accelerator) {
throw new Error(_(
'Accelerator "%s" is used for "%s" and "%s" commands. This may lead to unexpected behaviour.',
proposedKeymapItem.accelerator,
item.command,
proposedKeymapItem.command,
));
}
}
return;
}
Comment on lines 352 to +369
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against a null/empty proposed accelerator.

If a caller (other than ShortcutRecorder, which already guards on accelerator truthiness) passes a proposedKeymapItem with accelerator === null (a disabled shortcut), this loop will falsely report a conflict against any other command whose accelerator is also null (several defaults, e.g. commands added via additionalDefaultCommandNames, are null). Consider short-circuiting when the proposed accelerator is falsy, matching the spirit of the full-keymap path which skips empty accelerators via the else if (itemAccelerator) branch.

🛡️ Proposed guard
 		if (proposedKeymapItem) {
+			// A null/empty accelerator means the shortcut is disabled — nothing to conflict with.
+			if (!proposedKeymapItem.accelerator) return;
 			// When checking a proposed change, only throw if the proposed accelerator
 			// conflicts with another command. Pre-existing conflicts between other commands
 			// should not block the user from saving an unrelated shortcut change.
 			for (const item of Object.values(this.keymap)) {
 				if (item.command === proposedKeymapItem.command) continue;
 				if (item.accelerator === proposedKeymapItem.accelerator) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public validateKeymap(proposedKeymapItem: KeymapItem = null) {
const usedAccelerators = new Set();
if (proposedKeymapItem) {
// When checking a proposed change, only throw if the proposed accelerator
// conflicts with another command. Pre-existing conflicts between other commands
// should not block the user from saving an unrelated shortcut change.
for (const item of Object.values(this.keymap)) {
if (item.command === proposedKeymapItem.command) continue;
if (item.accelerator === proposedKeymapItem.accelerator) {
throw new Error(_(
'Accelerator "%s" is used for "%s" and "%s" commands. This may lead to unexpected behaviour.',
proposedKeymapItem.accelerator,
item.command,
proposedKeymapItem.command,
));
}
}
return;
}
public validateKeymap(proposedKeymapItem: KeymapItem = null) {
if (proposedKeymapItem) {
// A null/empty accelerator means the shortcut is disabled — nothing to conflict with.
if (!proposedKeymapItem.accelerator) return;
// When checking a proposed change, only throw if the proposed accelerator
// conflicts with another command. Pre-existing conflicts between other commands
// should not block the user from saving an unrelated shortcut change.
for (const item of Object.values(this.keymap)) {
if (item.command === proposedKeymapItem.command) continue;
if (item.accelerator === proposedKeymapItem.accelerator) {
throw new Error(_(
'Accelerator "%s" is used for "%s" and "%s" commands. This may lead to unexpected behaviour.',
proposedKeymapItem.accelerator,
item.command,
proposedKeymapItem.command,
));
}
}
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/services/KeymapService.ts` around lines 352 - 369, In
validateKeymap, guard against a null/empty proposedKeymapItem.accelerator by
short-circuiting when proposedKeymapItem.accelerator is falsy so you don't treat
disabled shortcuts as conflicts; inside the proposedKeymapItem branch of
validateKeymap (the loop over this.keymap), skip any comparison if
!proposedKeymapItem.accelerator (or explicitly check truthiness) and only
compare item.accelerator === proposedKeymapItem.accelerator when
proposedKeymapItem.accelerator is truthy (mirroring the existing else if
(itemAccelerator) behavior for the full-keymap path).


// Validate as if the proposed change is already present in the current keymap
// Helpful for detecting any errors that'll occur, when the proposed change is performed on the keymap
if (proposedKeymapItem) usedAccelerators.add(proposedKeymapItem.accelerator);
// Validate the entire keymap for duplicates
const usedAccelerators = new Set();

for (const item of Object.values(this.keymap)) {
const [itemAccelerator, itemCommand] = [item.accelerator, item.command];
if (proposedKeymapItem && itemCommand === proposedKeymapItem.command) continue; // Ignore the original accelerator

if (usedAccelerators.has(itemAccelerator)) {
const originalItem = (proposedKeymapItem && proposedKeymapItem.accelerator === itemAccelerator)
? proposedKeymapItem
: Object.values(this.keymap).find(_item => _item.accelerator === itemAccelerator);
const originalItem = Object.values(this.keymap).find(_item => _item.accelerator === itemAccelerator);

throw new Error(_(
'Accelerator "%s" is used for "%s" and "%s" commands. This may lead to unexpected behaviour.',
itemAccelerator,
originalItem.command,
// originalItem must exist here — its accelerator was added to usedAccelerators in a prior iteration
originalItem!.command,
itemCommand,
));
} else if (itemAccelerator) {
Expand Down
Loading