-
Notifications
You must be signed in to change notification settings - Fork 11
Fix BLOCKMENTIONSDATED parsing, hotkey add behavior, and FORM focus handling #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
salmonumbrella
wants to merge
8
commits into
RoamJS:main
Choose a base branch
from
salmonumbrella:fix/blockmentionsdated-date-parsing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
e2aced1
Fix BLOCKMENTIONSDATED parsing and hotkey/form issues
salmonumbrella 8bc4ed4
chore: remove research.md from repository
salmonumbrella 5a6100d
docs: add issue references to enforceFocus setting
salmonumbrella 9e35e59
test: add edge case tests for splitSmartBlockArgs and coalescing
salmonumbrella 5b17ebe
docs: add explanatory comments to coalesceBlockMentionsDatedDates
salmonumbrella b8d7262
fix: throw on exhausted hotkey combos, document suffix ordering
salmonumbrella a94d986
refactor: remove stream output feature from bugfix branch
salmonumbrella 841f5dc
test: add edge cases for malformed page references in splitSmartBlock…
salmonumbrella File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| const HOTKEY_MODIFIERS = ["control", "alt", "shift", "meta"]; | ||
|
|
||
| // Ordered by ergonomic preference: right-hand home row first (o, p, k, l), | ||
| // then surrounding keys, then left hand, then digits. | ||
| const HOTKEY_SUFFIXES = "opklijuyhgfdsawertqzxcvbnm1234567890".split(""); | ||
|
|
||
| const getNextAvailableHotKey = (keys: Record<string, string>) => { | ||
| for (const modifier of HOTKEY_MODIFIERS) { | ||
| for (const suffix of HOTKEY_SUFFIXES) { | ||
| const candidate = `${modifier}+${suffix}`; | ||
| if (!keys[candidate]) { | ||
| return candidate; | ||
| } | ||
| } | ||
| } | ||
| throw new Error( | ||
| "All hotkey combinations are in use. Remove an existing hotkey first." | ||
| ); | ||
| }; | ||
|
|
||
| export default getNextAvailableHotKey; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| const MONTH_DAY_REGEX = | ||
| /^(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?$/i; | ||
| const YEAR_REGEX = /^\d{4}$/; | ||
|
|
||
| /** | ||
| * Re-joins "Month Day, Year" date tokens that were split on commas. | ||
| * | ||
| * BLOCKMENTIONSDATED signature: (limit, title, startDate, endDate, sort, format, ...search) | ||
| * Positions 0-1 (limit, title) are passed through unchanged. | ||
| * Starting at position 2, up to 2 date tokens are coalesced (startDate + endDate). | ||
| * The `mergedDates < 2` guard stops coalescing after both date slots are filled. | ||
| */ | ||
| const coalesceBlockMentionsDatedDates = (args: string[]) => { | ||
| const merged = args.slice(0, 2); | ||
| let i = 2; | ||
| let mergedDates = 0; | ||
| while (i < args.length) { | ||
| const current = args[i] || ""; | ||
| const next = args[i + 1]; | ||
| if ( | ||
| mergedDates < 2 && | ||
| typeof next === "string" && | ||
| MONTH_DAY_REGEX.test(current.trim()) && | ||
| YEAR_REGEX.test(next.trim()) | ||
| ) { | ||
| merged.push(`${current.trimEnd()}, ${next.trimStart()}`); | ||
| i += 2; | ||
| mergedDates += 1; | ||
| } else { | ||
| merged.push(current); | ||
| i += 1; | ||
| } | ||
| } | ||
| return merged; | ||
| }; | ||
|
|
||
| const splitSmartBlockArgs = (cmd: string, afterColon: string) => { | ||
| let commandStack = 0; | ||
| let pageRefStack = 0; | ||
| const args = [] as string[]; | ||
| for (let i = 0; i < afterColon.length; i += 1) { | ||
| const cur = afterColon[i]; | ||
| const prev = afterColon[i - 1]; | ||
| const next = afterColon[i + 1]; | ||
| if (cur === "%" && prev === "<") { | ||
| commandStack += 1; | ||
| } else if (cur === "%" && next === ">" && commandStack) { | ||
| commandStack -= 1; | ||
| } else if (cur === "[" && next === "[") { | ||
| pageRefStack += 1; | ||
| } else if (cur === "]" && prev === "]" && pageRefStack) { | ||
| pageRefStack -= 1; | ||
| } | ||
| if (cur === "," && !commandStack && !pageRefStack && prev !== "\\") { | ||
| args.push(""); | ||
| continue; | ||
| } else if (cur === "\\" && next === ",") { | ||
| continue; | ||
| } | ||
| const current = args[args.length - 1] || ""; | ||
| if (!args.length) { | ||
| args.push(cur); | ||
| } else { | ||
| args[args.length - 1] = `${current}${cur}`; | ||
| } | ||
| } | ||
| return cmd.toUpperCase() === "BLOCKMENTIONSDATED" | ||
| ? coalesceBlockMentionsDatedDates(args) | ||
| : args; | ||
| }; | ||
|
|
||
| export default splitSmartBlockArgs; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { test, expect } from "@playwright/test"; | ||
| import getNextAvailableHotKey from "../src/utils/getNextAvailableHotKey"; | ||
|
|
||
| test("picks next control hotkey when control+o is taken", () => { | ||
| expect(getNextAvailableHotKey({ "control+o": "uid-1" })).toBe("control+p"); | ||
| }); | ||
|
|
||
| test("falls back to alt hotkeys when control combos are exhausted", () => { | ||
| const taken = Object.fromEntries( | ||
| "opklijuyhgfdsawertqzxcvbnm1234567890" | ||
| .split("") | ||
| .map((k, i) => [`control+${k}`, `uid-${i}`]) | ||
| ); | ||
| expect(getNextAvailableHotKey(taken)).toBe("alt+o"); | ||
| }); | ||
|
|
||
| test("throws when all modifier+suffix combos are exhausted", () => { | ||
| const allTaken: Record<string, string> = {}; | ||
| for (const mod of ["control", "alt", "shift", "meta"]) { | ||
| for (const key of "opklijuyhgfdsawertqzxcvbnm1234567890".split("")) { | ||
| allTaken[`${mod}+${key}`] = `uid-${mod}-${key}`; | ||
| } | ||
| } | ||
| expect(() => getNextAvailableHotKey(allTaken)).toThrow(); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getNextAvailableHotKeycan throw inside a state updater, which may crash the component.If all 144 hotkey combinations are exhausted,
getNextAvailableHotKeythrows. Since this throw occurs inside thesetKeyscallback (a React state updater), React will propagate it as an unhandled error, potentially unmounting the component tree. Consider catching the error and showing a user-friendly notification instead.🛡️ Proposed fix
setKeys((currentKeys) => { + let nextHotkey: string; + try { + nextHotkey = getNextAvailableHotKey(currentKeys); + } catch { + // All combinations exhausted — return state unchanged + return currentKeys; + } - const nextHotkey = getNextAvailableHotKey(currentKeys); const newKeys = Object.fromEntries( Object.entries(currentKeys).concat([ [nextHotkey, randomWorkflow.uid], ]) ); extensionAPI.settings.set("hot-keys", newKeys); return newKeys; });📝 Committable suggestion
🤖 Prompt for AI Agents