Skip to content

Desktop: Fix shortcut editor Save button blocked by pre-existing conflicts (#11670)#15185

Open
samkaminski wants to merge 1 commit intolaurent22:devfrom
samkaminski:fix/11670-shortcut-conflict-blocks-save
Open

Desktop: Fix shortcut editor Save button blocked by pre-existing conflicts (#11670)#15185
samkaminski wants to merge 1 commit intolaurent22:devfrom
samkaminski:fix/11670-shortcut-conflict-blocks-save

Conversation

@samkaminski
Copy link
Copy Markdown

Summary

Fixes #11670.

In Joplin's desktop keyboard shortcut settings dialog, the Save button in the ShortcutRecorder component was disabled whenever any shortcut conflict existed in the keymap — even pre-existing conflicts that the user had nothing to do with. This made it impossible to save a change to command C if commands A and B happened to share an accelerator, regardless of what C was being set to.

Root cause: KeymapService.validateKeymap(proposedKeymapItem) accumulated all seen accelerators in a Set as it iterated the full keymap. When two pre-existing commands shared an accelerator, the second occurrence triggered a throw — even when proposedKeymapItem was for a completely unrelated command with a completely different accelerator.

Fix: When proposedKeymapItem is provided (the "check a proposed change" path), only verify that the proposed accelerator doesn't directly appear in another command. Pre-existing conflicts between other commands are not the user's responsibility and should not block saving. The no-argument path (called from overrideKeymap for full keymap validation) is unchanged.

Changes

  • packages/lib/services/KeymapService.ts — split validateKeymap into two code paths: a targeted check when a proposedKeymapItem is given, and the existing full duplicate scan when called with no arguments.
  • packages/lib/services/KeymapService.test.js — added three unit tests covering: conflict detection still works, no conflict passes, and pre-existing conflicts between other commands no longer block an unrelated save.

Test plan

  • Open keyboard shortcut settings when pre-existing conflicts exist — Save button should now be enabled for unrelated shortcuts
  • Setting a shortcut to an accelerator already used by another command should still block Save
  • yarn test packages/lib/services/KeymapService.test.js passes (15/15 tests)

🤖 Generated with Claude Code

…licts

When validateKeymap was called with a proposedKeymapItem, it accumulated
all seen accelerators in a Set and threw on any duplicate — including
pre-existing conflicts between other commands unrelated to the proposed
change. This caused the Save button in ShortcutRecorder to be disabled
whenever any pre-existing shortcut conflict existed in the keymap, even
if the user's edit introduced no new conflict.

Fix: when proposedKeymapItem is provided, only check if that accelerator
directly conflicts with another command's accelerator. The full duplicate
scan (no proposedKeymapItem) used by overrideKeymap is unchanged.

Fixes laurent22#11670

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@coderabbitai coderabbitai Bot added bug It's a bug desktop All desktop platforms labels Apr 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/lib/services/KeymapService.test.js (1)

349-367: LGTM — good targeted coverage for the fix.

The three cases map cleanly to the new two-path behaviour: proposed conflict throws, proposed unique passes, and pre-existing unrelated conflict no longer blocks a save. Using setAccelerator to seed the pre-existing duplicate is a nice way to simulate the bug scenario from #11670.

One optional addition worth considering: an assertion that proposing an accelerator which matches the pre-existing duplicate (e.g. proposing Ctrl+N for synchronize after newTodo was set to Ctrl+N) still throws — this locks in that the fix only relaxes unrelated conflicts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/services/KeymapService.test.js` around lines 349 - 367, Add a
new assertion in the validateKeymap test to ensure that if you seed a
pre-existing duplicate via keymapService.setAccelerator('newTodo','Ctrl+N') and
then propose the same accelerator for a different command,
keymapService.validateKeymap({ accelerator: 'Ctrl+N', command: 'synchronize' })
still throws; this confirms validateKeymap enforces conflicts for the proposed
shortcut even when the duplicate was pre-existing (use the existing
describe('validateKeymap') setup and the setAccelerator helper to seed the
state).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/lib/services/KeymapService.ts`:
- Around line 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).

---

Nitpick comments:
In `@packages/lib/services/KeymapService.test.js`:
- Around line 349-367: Add a new assertion in the validateKeymap test to ensure
that if you seed a pre-existing duplicate via
keymapService.setAccelerator('newTodo','Ctrl+N') and then propose the same
accelerator for a different command, keymapService.validateKeymap({ accelerator:
'Ctrl+N', command: 'synchronize' }) still throws; this confirms validateKeymap
enforces conflicts for the proposed shortcut even when the duplicate was
pre-existing (use the existing describe('validateKeymap') setup and the
setAccelerator helper to seed the state).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 60fd0d0b-19c5-4d1e-b3c2-bc48f8bef399

📥 Commits

Reviewing files that changed from the base of the PR and between b13827d and b53a67d.

📒 Files selected for processing (2)
  • packages/lib/services/KeymapService.test.js
  • packages/lib/services/KeymapService.ts

Comment on lines 352 to +369
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;
}
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).

@github-actions github-actions Bot deleted a comment from coderabbitai Bot Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug It's a bug desktop All desktop platforms

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shortcut editor: Can't save changes if there are existing shortcut conflicts

1 participant