diff --git a/.lantern/lang/en/src/renderer/lib/lantern-pixi.lantern.json b/.lantern/lang/en/src/renderer/lib/lantern-pixi.lantern.json index ae1a55e..c5badfe 100644 --- a/.lantern/lang/en/src/renderer/lib/lantern-pixi.lantern.json +++ b/.lantern/lang/en/src/renderer/lib/lantern-pixi.lantern.json @@ -4,8 +4,9 @@ "index.ts": "Renders the interactive WebGL graph, file or macro, with viewport culling, accessible controls and the pull request marks and the open-file marker on macro blocks, links and outside systems, with the marks calmed and other blocks greyed while a file or a picked flow step's files are open (the marker takes a set of blocks), draws a block a selected platform only uses fainter than a lit one and stronger than a dimmed one, and opens a macro area card or stub on a double tap or Enter without letting a removed focused card re-enter the render. It gives the canvas a tooltip of the block or link under the pointer that follows it, and clears the selection only on a primary-button tap of the background", "macro-graphics.ts": "Draws the macro view's arrows and count labels with strong ones emphasised, only the main links at rest and every link of the selected block, lights the links a pull request touches and quiets the rest, keeps only the links of the blocks being read prominent while a file or a flow step's files are open, and mirrors each arrow as a hidden note for screen readers.", "node-graphics.ts": "Draws file cards, directory tiles and macro blocks with their purpose lines, a quieter look for shared foundation and a glow with changed-of-all files for blocks a pull request touches (a thin outline and the caption only while a file is open), a ringed READING badge on the block of the open file (frame widths from the shared legend list), a dashed edge for a card of features not yet in an area and an open chevron on area cards and stubs, with icons, status marks, PR badges, a quiet related mark, a dashed new look for files a pull request or a plan adds, and open-file buttons, all of which answer a primary-button tap only", - "open-actions.ts": "Draws the code and diff buttons on relation-view file tiles and their keyboard-reachable twins, and tells a primary-button tap from a right or middle one", + "open-actions.ts": "Draws the code and diff buttons on relation-view file tiles and their keyboard-reachable twins, which answer a primary-button tap only", "palette.ts": "Reads graph CSS colors and fonts for Pixi drawing", + "pointer.ts": "Tells a primary-button tap on a graph object from a right or middle one", "relation-graphics.ts": "Draws relation-view zones, directory areas that a primary-button tap pins, and focused-file edges", "text-quality.ts": "Defers text-resolution upgrades until graph zoom settles", "viewport-size.ts": "Measures the graph pane by its border box, to notice real resizes, and without rounding a fractional size up, so the canvas never overflows its own scroll area" diff --git a/.lantern/lang/en/test.lantern.json b/.lantern/lang/en/test.lantern.json index ce92c5c..73fac55 100644 --- a/.lantern/lang/en/test.lantern.json +++ b/.lantern/lang/en/test.lantern.json @@ -66,7 +66,8 @@ "lantern-delegate-required-skills.test.ts": "Checks each role's required skills are its fixed ones then its slot names, in order and without repeats, that the command prints them, including extras chosen with no skill linker from the project and the home folder, and warns about an extra it skips, that a project-level extra is printed only inside that project (also from a worktree of it, or through --project) after the global ones and warned about only there, a damaged project file is named while the global extras still print, and that no agent definition, protocol or skill line names the switchable skills", "lantern-delegate-skill-reads.test.ts": "Checks the skill-reads linter fails a linked skill with no later SKILL.md read, however the read is ordered, targeted or buried in a pipe, a chain or a for-loop over a literal list, and that only the configured skill linker counts as a link, whatever its name", "deploy-docs-workflow.test.ts": "Checks the docs workflow deploys from main only, builds on pull requests and by hand, grants Pages rights to the deploy job alone, reads the site settings from variables rather than secrets, and that the site pins the same pnpm as the root manifest", - "docs-website.test.ts": "Checks the docs site pages and their reading order, that the home comes from docs/index.md, the docs identity (CNAME, title) with no hero or showcase left, that every relative link in the Markdown resolves, links become routes, the base and site URL settings are normalised, the site stays out of the root workspace, and the README, docs home and site footer carry the same not-affiliated sentence and the footer the licence line", + "docs-cname.test.ts": "Checks website/public/CNAME exists and holds exactly docs.thelantern.dev and a newline, because that file decides the docs domain", + "docs-website.test.ts":"Checks the docs site pages and their reading order, that the home comes from docs/index.md, the docs identity (CNAME, title) with no hero or showcase left, that every relative link in the Markdown resolves, links become routes, the base and site URL settings are normalised, the site stays out of the root workspace, and the README, docs home and site footer carry the same not-affiliated sentence and the footer the licence line", "dev-instance.test.ts": "Checks that worktrees get their own key, name and stable colour, that the shared dev settings are copied once, that the instance record drops a previous run's ports, and the short name and tint of the menu bar mark", "dev-profile.test.ts": "Checks the dev profile moves the port, silences notifications unless overridden, and skips the skill bundle install", "devserver.test.ts": "Checks that every debug harness route refuses a missing or wrong token, a foreign Host, any Origin and a page-style Sec-Fetch-Site, answers with the token, sends no CORS headers, keeps the token to an owner-only record, and survives a second listener on its port", @@ -342,7 +343,8 @@ "extensions-panel.test.ts": "Checks the extensions panel load and save with the refusal messages kept as the file stays, the installed skill names merged from project and user folders and the picker source built from them without a skill linker, the skill linker path save and clear and its initialized, not set or invalid state, a list file imported without duplicates or locked skills, and a skill created for a slot: SKILL.md written into a new project folder, an existing folder refused untouched, empty content only adding an installed name, and bad input writing nothing.", "skills-remote.test.ts": "Checks that the skills repo remote is read only from the configured checkout's git config (empty for a plain folder or a checkout with none), follows an adopted checkout, that cloning without any remote asks for one, refuses an option-like URL and reuses the checkout's remote for a new folder, and that no source of the Skills window names a company.", "skills.test.ts": "Checks skills-repository adoption by its .git and a skill folder, whatever its linker is called, role discovery, repositories with no groups or other group names, recommendations by dependency name, that no group name of one skills repo is written in the main skills code or the renderer, linking results for bundled, local and hostile names, unlinking, per-name local Git exclusion that leaves user-added skills visible, and safe Codex skill roots.", - "state.test.ts": "Checks Claude snapshot reconciliation with silent baselines and waiting, permission, finished and failed notices, a turn that finished between two scans told by a newer change time, Codex hook turn tracking and deduplication, ordering, pruning, discovered and runner sessions and which sessions Clear finished may remove.", + "source-blocks.test.ts": "Checks the source-block helper finds a function body, call arguments or an IPC handler by matching brackets while ignoring brackets inside strings, template literals and comments, and still reports an unbalanced block", + "state.test.ts":"Checks Claude snapshot reconciliation with silent baselines and waiting, permission, finished and failed notices, a turn that finished between two scans told by a newer change time, Codex hook turn tracking and deduplication, ordering, pruning, discovered and runner sessions and which sessions Clear finished may remove.", "subagent.test.ts": "Checks subagent event and worktree recognition without rejecting ordinary user sessions.", "symbolfind.test.ts": "Checks declaration-first symbol lookup across supported language syntaxes, qualified methods and whole-word fallbacks.", "tray-action.test.ts": "Checks that the popover's actions are read back from their ids, that a test card is accepted only for a kind the menu offered and that anything else is refused.", diff --git a/.lantern/lang/en/test/e2e.lantern.json b/.lantern/lang/en/test/e2e.lantern.json index 94cee9e..300f020 100644 --- a/.lantern/lang/en/test/e2e.lantern.json +++ b/.lantern/lang/en/test/e2e.lantern.json @@ -8,7 +8,7 @@ "graph-macro-legend.spec.ts": "Checks in the real app that the Macro legend in the Overview drawer is short, fits it, and that its swatches (selected, uses, To assign) are painted exactly like the shapes on the map, and screenshots the areas map and flat keys.", "graph-macro-platform-uses.spec.ts": "Checks in the real app that of two bots of one kind the accounts area is lit strongly for the one with files there and as uses for the one that only uses it, with the legend drawing it the same solid way, lists both in the Overview drawer, lights nothing as uses at rest and screenshots each.", "graph-macro.spec.ts": "Checks the macro view in the real canvas: the counts drawn at rest, readable names, selection, Escape and the way back to the file graph.", - "graph-navigation.spec.ts": "Checks graph zoom, panning, right-drag, overscroll, focus and input behaviour in Electron", + "graph-navigation.spec.ts": "Checks graph zoom, panning, right-drag, a right-click of a node that leaves selection and layout alone, overscroll, focus and input behaviour in Electron", "graph-pixi.spec.ts": "Checks WebGL rendering, retained tile text across zoom, interaction and failure recovery", "graph-regression.spec.ts": "Guards graph appearance, selection, search and empty states against regressions", "tray-popover.spec.ts": "Drives the menu bar popover in its harness with synthetic projects in every state: urgency order, a long name and path, nothing needing you, the empty state, the More panel with and without notifications, arrow keys, Enter and Escape, pointer selection, a live update, a scrolling list, and the stopped spinner when motion is reduced.", diff --git a/.lantern/lang/en/test/helpers.lantern.json b/.lantern/lang/en/test/helpers.lantern.json index 16bdc69..8f37f4f 100644 --- a/.lantern/lang/en/test/helpers.lantern.json +++ b/.lantern/lang/en/test/helpers.lantern.json @@ -36,7 +36,7 @@ "macro-lab-world.ts": "Generates the invented Orchard Market project (six areas, eighteen features, five platforms with an infrastructure one and two chat bots, a foundation band, a feature left to assign, a pull request across three areas with its Flow, outside systems with different counts and some talked to by the foundation) and derives its Macro payload with the real derivation.", "lantern-plan-world.ts": "builds a throwaway project from the shared plan fixtures, with or without a remote, and runs the plan and sync scripts against it for the plan tests", "lantern-vocab-project.ts": "writes a synthetic project with its map, features, platforms, systems list and areas to a temp folder, runs the lantern-setup scripts on it and snapshots its .lantern files", - "source-blocks.ts": "finds a function body, an IPC handler's arguments or every occurrence of a call inside a source file by matching brackets, so wiring tests check where a call sits rather than counting matches", + "source-blocks.ts": "finds a function body, an IPC handler's arguments or every occurrence of a call inside a source file by matching brackets while skipping strings, template literals and comments, so wiring tests check where a call sits rather than counting matches", "lantern-map.ts":"builders for map nodes, whole maps (with their plans and unreadable plan files) and PR records", "lantern-sections.ts": "section fixtures (RBAC, states) and a mount helper for the map view", "macro-sizes-world.ts": "Generates a small (4 areas, 8 features) and a large (7 areas, 24 features) invented project and derives their Macro payloads with the real derivation.", diff --git a/.lantern/map/src/renderer/lib/lantern-pixi.lantern b/.lantern/map/src/renderer/lib/lantern-pixi.lantern index 0c5aa27..35f8098 100644 --- a/.lantern/map/src/renderer/lib/lantern-pixi.lantern +++ b/.lantern/map/src/renderer/lib/lantern-pixi.lantern @@ -6,13 +6,14 @@ src/renderer/lib/lantern-pixi/edge-graphics.ts . component 42b2871e19fb < src/renderer/lib/lanternctx.ts < src/renderer/lib/lanternlayout.ts#EdgeCurve -src/renderer/lib/lantern-pixi/index.ts . component 77ff3400d9ac +src/renderer/lib/lantern-pixi/index.ts . component fabb2f822ffa #feature file-graph = src/renderer/lib/lantern-pixi/edge-graphics.ts = src/renderer/lib/lantern-pixi/macro-graphics.ts#macroGraphics = src/renderer/lib/lantern-pixi/node-graphics.ts = src/renderer/lib/lantern-pixi/open-actions.ts = src/renderer/lib/lantern-pixi/palette.ts + = src/renderer/lib/lantern-pixi/pointer.ts#isPrimaryTap = src/renderer/lib/lantern-pixi/relation-graphics.ts#relationGraphics = src/renderer/lib/lantern-pixi/text-quality.ts < src/renderer/lib/lantern-pixi/viewport-size.ts#innerSize @@ -42,11 +43,12 @@ src/renderer/lib/lantern-pixi/macro-graphics.ts . component f7cbb30b31b9 < src/renderer/lib/lanternmacromarks.ts#MacroMarks < src/renderer/lib/lanternmacrostyles.ts#FLAT_LINE -src/renderer/lib/lantern-pixi/node-graphics.ts . component c33a2f532103 +src/renderer/lib/lantern-pixi/node-graphics.ts . component 3d82d474c23b #feature file-graph = src/renderer/lib/file-icons.ts#fileIconUrl = src/renderer/lib/lantern-pixi/open-actions.ts < src/renderer/lib/lantern-pixi/palette.ts#PixiPalette + = src/renderer/lib/lantern-pixi/pointer.ts#isPrimaryTap = src/renderer/lib/lanterncollapse.ts#tileFontSize = src/renderer/lib/lanternlayout.ts = src/renderer/lib/lanternmacro.ts#captionOf @@ -58,18 +60,22 @@ src/renderer/lib/lantern-pixi/node-graphics.ts . component c33a2f532103 = src/renderer/lib/lanterntile.ts#tileSlots < src/shared/lantern-plan-messages.ts#say -src/renderer/lib/lantern-pixi/open-actions.ts . component e712cc868338 +src/renderer/lib/lantern-pixi/open-actions.ts . component a3fa6f5ba722 #feature file-graph < src/renderer/lib/lantern-pixi/palette.ts#PixiPalette + = src/renderer/lib/lantern-pixi/pointer.ts#isPrimaryTap src/renderer/lib/lantern-pixi/palette.ts . util efd6ac83e6b9 #feature file-graph -src/renderer/lib/lantern-pixi/relation-graphics.ts . component 8ea124725b7a +src/renderer/lib/lantern-pixi/pointer.ts . util 2f6985fab160 +#feature file-graph + +src/renderer/lib/lantern-pixi/relation-graphics.ts . component ed6c5cf2fdd1 #feature file-graph = src/renderer/lib/lantern-pixi/edge-graphics.ts#drawEdge - = src/renderer/lib/lantern-pixi/open-actions.ts#isPrimaryTap < src/renderer/lib/lantern-pixi/palette.ts#PixiPalette + = src/renderer/lib/lantern-pixi/pointer.ts#isPrimaryTap < src/renderer/lib/lanternlayout.ts = src/renderer/lib/lanternprofile.ts#Profile < src/renderer/lib/lanternrelview.ts diff --git a/.lantern/map/test.lantern b/.lantern/map/test.lantern index 50b1dd3..707939d 100644 --- a/.lantern/map/test.lantern +++ b/.lantern/map/test.lantern @@ -336,6 +336,8 @@ test/diff-collapse.test.ts . test 8170f196b244 = src/renderer/lib/diffcollapse.ts = test/helpers/diff-text.ts +test/docs-cname.test.ts . test 8148e72c3e6f + test/docs-website.test.ts . test 4b8487436f7d < website/plugins/site.ts#normalizeBase < website/src/lib/links.ts#resolveLink @@ -442,7 +444,7 @@ test/idle-tabs.test.ts . test 7dece3b876fc #feature serve-web = src/renderer/lib/idle-tabs.ts -test/install-skills.test.ts . test e0e74002199c +test/install-skills.test.ts . test fc6c53216370 #feature skill-installer = scripts/install-skills.mjs = scripts/lantern-bundle.mjs @@ -2322,6 +2324,9 @@ test/skills.test.ts . test 60d4f06a1fff #feature skill-installer = src/main/skills.ts +test/source-blocks.test.ts . test 4a34775ee293 + = test/helpers/source-blocks.ts + test/state.test.ts . test eb2c2d09754c #feature session-monitoring = src/main/state.ts diff --git a/.lantern/map/test/e2e.lantern b/.lantern/map/test/e2e.lantern index 6c1814b..295afee 100644 --- a/.lantern/map/test/e2e.lantern +++ b/.lantern/map/test/e2e.lantern @@ -43,7 +43,7 @@ test/e2e/graph-macro.spec.ts . test 0f4c5055ed0f = test/e2e/support/graph-harness.ts = test/fixtures/lantern-macro-own-view.json -test/e2e/graph-navigation.spec.ts . test 0910cf6edfbd +test/e2e/graph-navigation.spec.ts . test 722e99b31b37 #feature file-graph = test/e2e/support/graph-harness.ts diff --git a/.lantern/map/test/helpers.lantern b/.lantern/map/test/helpers.lantern index 266bd89..2d6f708 100644 --- a/.lantern/map/test/helpers.lantern +++ b/.lantern/map/test/helpers.lantern @@ -217,7 +217,7 @@ test/helpers/updater-world.ts . test 577549414f03 #feature app-updates = src/main/updater/service.ts#createUpdater -test/helpers/source-blocks.ts . test 45cdb0e2dee8 +test/helpers/source-blocks.ts . test 73d367b95974 #feature serve-web test/helpers/pr-rows.ts . test ef812afdf693 diff --git a/.lantern/prs/36.pr b/.lantern/prs/36.pr new file mode 100644 index 0000000..cdd6fba --- /dev/null +++ b/.lantern/prs/36.pr @@ -0,0 +1,65 @@ +lantern-pr 1 +# 36 +#title Apply the review follow-ups of PRs 33 and 34 +#url https://github.com/MobileReality/lantern/pull/36 +#branch chore/review-followups-33-34 +#base main +#opened 2026-11-12 +#head 14c624e7e17091a5643ae29568df4756f71675b4 +#describe what Applies the review follow-ups of PRs 33 and 34: a right-click test, a pointer module, literal-aware source blocks, a cleanup and two docs entries. +#describe what Adds a light test that the docs CNAME holds exactly docs.thelantern.dev. +#describe before isPrimaryTap lived in open-actions.ts, and a brace in a string or comment unbalanced the source-block helper. +#describe after isPrimaryTap is in pointer.ts, and the helper skips strings, templates and comments; a right-click on a node is tested to change nothing. +#describe verified Typecheck, lint, unit tests and the graph e2e pass, apart from two visual snapshot specs that also fail on main on this machine. ++ src/renderer/lib/lantern-pixi/pointer.ts ++ test/docs-cname.test.ts ++ test/source-blocks.test.ts +~ docs/primitives.md +~ src/renderer/lib/lantern-pixi/index.ts +~ src/renderer/lib/lantern-pixi/node-graphics.ts +~ src/renderer/lib/lantern-pixi/open-actions.ts +~ src/renderer/lib/lantern-pixi/relation-graphics.ts +~ test/e2e/graph-navigation.spec.ts +~ test/helpers/source-blocks.ts +~ test/install-skills.test.ts +#order src/renderer/lib/lantern-pixi/pointer.ts +#order src/renderer/lib/lantern-pixi/open-actions.ts +#order src/renderer/lib/lantern-pixi/node-graphics.ts +#order src/renderer/lib/lantern-pixi/relation-graphics.ts +#order src/renderer/lib/lantern-pixi/index.ts +#order test/e2e/graph-navigation.spec.ts +#order test/helpers/source-blocks.ts +#order test/source-blocks.test.ts +#order test/install-skills.test.ts +#order test/docs-cname.test.ts +#order docs/primitives.md +#reason src/renderer/lib/lantern-pixi/pointer.ts Gives the primary-button check its own small module so graphics files stop importing it from the open-button code. +#reason test/docs-cname.test.ts Keeps a light guard on the file that decides the docs domain after the copy-text tests were removed. +#reason test/source-blocks.test.ts Proves the block finder ignores brackets in literals and comments and still reports an unbalanced block. +#reason docs/primitives.md Names the source-block and Overview-drawer helpers so the next change reuses them instead of writing its own. +#reason src/renderer/lib/lantern-pixi/index.ts Imports the primary-tap check from the pointer module now that it lives there. +#reason src/renderer/lib/lantern-pixi/node-graphics.ts Imports the primary-tap check from the pointer module now that it lives there. +#reason src/renderer/lib/lantern-pixi/open-actions.ts Drops the helper it only borrowed to others and imports it from the pointer module like everyone else. +#reason src/renderer/lib/lantern-pixi/relation-graphics.ts Imports the primary-tap check from the pointer module now that it lives there. +#reason test/e2e/graph-navigation.spec.ts Pins that a right-click on a node changes neither selection nor layout, so removing the primary-tap guard fails a test. +#reason test/helpers/source-blocks.ts Stops a brace inside a string, template or comment from unbalancing the block that wiring tests slice out. +#reason test/install-skills.test.ts Removes a redundant alias of the skill name list that the reviewer flagged. +#story A reviewer right-clicks a graph node +#step actor Person using the graph +#step action right-clicks +#step subject a file card in the graph +#file test/e2e/graph-navigation.spec.ts +#file src/renderer/lib/lantern-pixi/pointer.ts +#below interaction can open the context menu without selecting the card +#file src/renderer/lib/lantern-pixi/node-graphics.ts +#below consequence nothing checked this => a test fails if it selects +#story A maintainer writes wiring tests +#step actor Maintainer +#step action slices a function body from +#step subject source with braces in strings +#file test/helpers/source-blocks.ts +#file test/source-blocks.test.ts +#below interaction can trust the slice and guard the docs domain +#file test/docs-cname.test.ts +#file docs/primitives.md +#below consequence a stray brace broke the slice => strings and comments are skipped diff --git a/docs/primitives.md b/docs/primitives.md index 866ac3c..c6a218e 100644 --- a/docs/primitives.md +++ b/docs/primitives.md @@ -37,6 +37,14 @@ Build state with these instead of hand-writing it: - `test/helpers/lantern-plan-world.ts`: a project with the plan fixtures and a small map (`makeProject`), or one with an origin remote (`makeRemoteProject`) for item status from open and merged PR records. Any test of plans, in the skill scripts or in the app. +- `test/helpers/source-blocks.ts` (`functionBody`, `callArguments`, `ipcHandler`, `occurrences`, `within`): + locate a function body, a call's arguments or an `ipcMain.handle` handler in a source file by balanced + brackets, skipping string and template literals and `//` and `/* */` comments. Any test that asserts what + a piece of source does or does not contain inside one block. Never slice by line count or a regex over the + whole file; `test/source-blocks.test.ts` pins the skipping. Regex literals are not skipped. +- `test/e2e/support/overview.ts` (`openOverview`, `closeOverview`, `overview`): the Overview drawer. Macro + specs open it through this, not with their own selector. `.lt-side` also matches the hidden drawer, so a + spec that means the visible side pane uses `.lt-side:not(.lt-overview)`. - `test/fixtures/`: static maps and PR records (`lantern-map`, `lantern-prs`, `lantern-macro`, …) and `lantern-plans/` (`plan-on-the-map`, a worked plan; `draft-reader`, a draft; a small map beside them). diff --git a/src/renderer/lib/lantern-pixi/index.ts b/src/renderer/lib/lantern-pixi/index.ts index 19f5380..3725917 100644 --- a/src/renderer/lib/lantern-pixi/index.ts +++ b/src/renderer/lib/lantern-pixi/index.ts @@ -23,7 +23,8 @@ import { blockKindText, captionOf } from '../lanternmacro'; import { OPEN_FILE_LABEL } from '../lanternmacrocurrent'; import { calmedAlpha, isMarked, markNote, markedAlpha, systemsOfBlocks, weakNearAlpha } from '../lanternmacromarks'; import type { MacroMarks } from '../lanternmacromarks'; -import { isPrimaryTap, syncOpenButtons } from './open-actions'; +import { syncOpenButtons } from './open-actions'; +import { isPrimaryTap } from './pointer'; import type { FileActions } from './open-actions'; const CULL_MARGIN = 0; diff --git a/src/renderer/lib/lantern-pixi/node-graphics.ts b/src/renderer/lib/lantern-pixi/node-graphics.ts index ec22c88..19627dc 100644 --- a/src/renderer/lib/lantern-pixi/node-graphics.ts +++ b/src/renderer/lib/lantern-pixi/node-graphics.ts @@ -14,7 +14,8 @@ import { markCaption } from '../lanternmacromarks'; import type { BlockMark } from '../lanternmacromarks'; import { PURPOSE_LINE_H } from '../lanternmacrocards'; import { FLAT_LINE } from '../lanternmacrostyles'; -import { actionsWidth, drawFileActions, isPrimaryTap } from './open-actions'; +import { actionsWidth, drawFileActions } from './open-actions'; +import { isPrimaryTap } from './pointer'; import type { FileActions } from './open-actions'; const CARD_TEXT_Y = 4; diff --git a/src/renderer/lib/lantern-pixi/open-actions.ts b/src/renderer/lib/lantern-pixi/open-actions.ts index 09f3d3c..4d8ada6 100644 --- a/src/renderer/lib/lantern-pixi/open-actions.ts +++ b/src/renderer/lib/lantern-pixi/open-actions.ts @@ -1,6 +1,7 @@ import { Container, Graphics, Rectangle } from 'pixi.js'; import type { FederatedPointerEvent } from 'pixi.js'; import type { PixiPalette } from './palette'; +import { isPrimaryTap } from './pointer'; export interface FileActions { path: string; @@ -13,11 +14,6 @@ type OpenKind = 'code' | 'diff'; const BUTTON = 16; const BUTTON_GAP = 2; const EDGE_GAP = 4; -const PRIMARY_BUTTON = 0; - -export function isPrimaryTap(event: FederatedPointerEvent) { - return event.button === PRIMARY_BUTTON; -} export function actionsWidth(actions: FileActions | null) { if (!actions) return 0; diff --git a/src/renderer/lib/lantern-pixi/pointer.ts b/src/renderer/lib/lantern-pixi/pointer.ts new file mode 100644 index 0000000..59bb91f --- /dev/null +++ b/src/renderer/lib/lantern-pixi/pointer.ts @@ -0,0 +1,7 @@ +import type { FederatedPointerEvent } from 'pixi.js'; + +const PRIMARY_BUTTON = 0; + +export function isPrimaryTap(event: FederatedPointerEvent) { + return event.button === PRIMARY_BUTTON; +} diff --git a/src/renderer/lib/lantern-pixi/relation-graphics.ts b/src/renderer/lib/lantern-pixi/relation-graphics.ts index cdab7d7..e991026 100644 --- a/src/renderer/lib/lantern-pixi/relation-graphics.ts +++ b/src/renderer/lib/lantern-pixi/relation-graphics.ts @@ -5,7 +5,7 @@ import type { RelationLayout, Refs, Side, Zone } from '../lanternrelview'; import type { PixiPalette } from './palette'; import { drawEdge } from './edge-graphics'; import type { Profile } from '../lanternprofile'; -import { isPrimaryTap } from './open-actions'; +import { isPrimaryTap } from './pointer'; const ZONE_NAMES: Record> = { in: { none: 'Takes data from', mixed: 'Takes data from, referred to by', all: 'Referred to by' }, diff --git a/test/docs-cname.test.ts b/test/docs-cname.test.ts new file mode 100644 index 0000000..aacd8be --- /dev/null +++ b/test/docs-cname.test.ts @@ -0,0 +1,17 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const ROOT = path.resolve(__dirname, '..'); +const DOCS_DOMAIN = 'docs.thelantern.dev\n'; + +function read(file: string): string { + return fs.readFileSync(path.join(ROOT, file), 'utf8'); +} + +describe('docs domain', () => { + it('publishes the docs site on docs.thelantern.dev through website/public/CNAME', () => { + expect(fs.existsSync(path.join(ROOT, 'website/public/CNAME'))).toBe(true); + expect(read('website/public/CNAME')).toBe(DOCS_DOMAIN); + }); +}); diff --git a/test/e2e/graph-navigation.spec.ts b/test/e2e/graph-navigation.spec.ts index f841c65..67191ac 100644 --- a/test/e2e/graph-navigation.spec.ts +++ b/test/e2e/graph-navigation.spec.ts @@ -291,6 +291,46 @@ test.describe('pixi', () => { await expect(page.locator('html')).toHaveAttribute('data-context-menus', '1'); await expect(main).not.toHaveClass(/lt-panning/); }); + + test('leaves the selection and the layout alone on a right-click of a node', async () => { + const page = harness.page; + await loadGraphFixture(harness, 'overview'); + const main = page.locator('.lt-main'); + const selected = () => page.locator('.lt-pixi-node-access[aria-pressed="true"]').evaluateAll((buttons) => buttons.map((button) => button.getAttribute('aria-label'))); + const selectedBefore = await selected(); + const point = await page.locator('.lt-pixi-a11y').evaluate((accessibility) => { + const view = accessibility.closest('.lt-main') as HTMLElement; + const rect = view.getBoundingClientRect(); + const button = [...accessibility.querySelectorAll('.lt-pixi-node-access')].find((candidate) => { + const x = Number(candidate.dataset.graphX) - view.scrollLeft; + const y = Number(candidate.dataset.graphY) - view.scrollTop; + return candidate.getAttribute('aria-pressed') !== 'true' && x > 0 && y > 0 && x < view.clientWidth - Number(candidate.dataset.graphWidth) && y < view.clientHeight - Number(candidate.dataset.graphHeight); + }); + if (!button) throw new Error('No visible unselected Pixi node'); + return { + x: rect.left + Number(button.dataset.graphX) - view.scrollLeft + Number(button.dataset.graphWidth) / 2, + y: rect.top + Number(button.dataset.graphY) - view.scrollTop + Number(button.dataset.graphHeight) / 2, + key: button.getAttribute('aria-label') ?? '', + }; + }); + const scrollBefore = await main.evaluate((element) => ({ left: element.scrollLeft, top: element.scrollTop })); + await page.evaluate(() => { + const root = document.documentElement; + root.dataset.transitionsStarted = '0'; + new MutationObserver(() => { + if (root.dataset.graphTransition === 'running') root.dataset.transitionsStarted = String(Number(root.dataset.transitionsStarted) + 1); + }).observe(root, { attributes: true, attributeFilter: ['data-graph-transition'] }); + }); + await page.mouse.move(point.x, point.y); + await expect.poll(() => page.locator('html').getAttribute('data-graph-hovered')).toBe(point.key); + await page.mouse.click(point.x, point.y, { button: 'right' }); + await page.waitForTimeout(500); + expect(await selected()).toEqual(selectedBefore); + await expect(page.locator('html')).toHaveAttribute('data-transitions-started', '0'); + expect(await main.evaluate((element) => ({ left: element.scrollLeft, top: element.scrollTop }))).toEqual(scrollBefore); + await page.mouse.click(point.x, point.y); + await expect(page.getByRole('button', { name: point.key, exact: true })).toHaveAttribute('aria-pressed', 'true'); + }); }); test('does not swallow the next click after Space is released before the pointer in Pixi', async () => { diff --git a/test/helpers/source-blocks.ts b/test/helpers/source-blocks.ts index 674326e..8f0114f 100644 --- a/test/helpers/source-blocks.ts +++ b/test/helpers/source-blocks.ts @@ -6,13 +6,66 @@ export interface SourceRange { const CLOSERS: Record = { '(': ')', '{': '}', '[': ']' }; +function skipQuoted(source: string, start: number, quote: string): number { + let i = start + 1; + while (i < source.length && source[i] !== quote) i += source[i] === '\\' ? 2 : 1; + return i + 1; +} + +function skipTemplate(source: string, start: number): number { + let i = start + 1; + while (i < source.length && source[i] !== '`') { + if (source[i] === '\\') i += 2; + else if (source[i] === '$' && source[i + 1] === '{') i = skipTemplateExpression(source, i + 2); + else i++; + } + return i + 1; +} + +function skipTemplateExpression(source: string, start: number): number { + let depth = 1; + let i = start; + while (i < source.length) { + const next = skipLiteral(source, i); + if (next !== i) i = next; + else { + if (source[i] === '{') depth++; + else if (source[i] === '}' && --depth === 0) return i + 1; + i++; + } + } + return i; +} + +function skipLiteral(source: string, i: number): number { + const char = source[i]; + if (char === "'" || char === '"') return skipQuoted(source, i, char); + if (char === '`') return skipTemplate(source, i); + if (char === '/' && source[i + 1] === '/') { + const end = source.indexOf('\n', i); + return end < 0 ? source.length : end + 1; + } + if (char === '/' && source[i + 1] === '*') { + const end = source.indexOf('*/', i + 2); + return end < 0 ? source.length : end + 2; + } + return i; +} + function balancedFrom(source: string, open: number): SourceRange { const opener = source[open]; const closer = CLOSERS[opener]; let depth = 0; - for (let i = open; i < source.length; i++) { + let i = open; + while (i < source.length) { + const next = skipLiteral(source, i); + if (next !== i) { + i = next; + continue; + } if (source[i] === opener) depth++; else if (source[i] === closer && --depth === 0) return { start: open, end: i + 1, text: source.slice(open, i + 1) }; + i++; } throw new Error(`unbalanced ${opener} at ${open}`); } diff --git a/test/install-skills.test.ts b/test/install-skills.test.ts index 977330e..2bb2ad6 100644 --- a/test/install-skills.test.ts +++ b/test/install-skills.test.ts @@ -64,11 +64,10 @@ describe('install-skills', () => { it('installs every bundled skill into a fresh project, lantern-local-loop with its nested files and executable loop.sh', async () => { const { stdout } = await install(project); const shipped = fs.readdirSync(SKILLS_SOURCE).filter((entry) => fs.existsSync(path.join(SKILLS_SOURCE, entry, 'SKILL.md'))).sort(); - const all = SKILL_NAMES; - expect(all.length).toBeGreaterThan(0); - expect(new Set(all).size).toBe(all.length); - expect([...all].sort()).toEqual(shipped); - for (const name of all) { + expect(SKILL_NAMES.length).toBeGreaterThan(0); + expect(new Set(SKILL_NAMES).size).toBe(SKILL_NAMES.length); + expect([...SKILL_NAMES].sort()).toEqual(shipped); + for (const name of SKILL_NAMES) { expect(stdout, name).toMatch(new RegExp(`installed +${name}\\b`)); expect(fs.existsSync(path.join(skills, name, 'SKILL.md')), name).toBe(true); expect(fs.existsSync(path.join(skills, name, SKILL_MARKER)), name).toBe(true); diff --git a/test/source-blocks.test.ts b/test/source-blocks.test.ts new file mode 100644 index 0000000..7d5a444 --- /dev/null +++ b/test/source-blocks.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; +import { callArguments, functionBody, ipcHandler } from './helpers/source-blocks'; + +describe('source-blocks bracket matching', () => { + it('finds a plain function body', () => { + const source = 'function run(a) { if (a) { return 1; } return 2; }\nconst after = 3;'; + expect(functionBody(source, 'run').text).toBe('{ if (a) { return 1; } return 2; }'); + }); + + it('ignores braces inside single, double and template strings', () => { + const source = [ + 'function run() {', + " const open = '{';", + ' const close = "}";', + ' const both = `}{`;', + ' return open + close + both;', + '}', + 'const after = 1;', + ].join('\n'); + expect(functionBody(source, 'run').text.endsWith('return open + close + both;\n}')).toBe(true); + }); + + it('ignores escaped quotes inside strings', () => { + const source = "function run() { const s = 'it\\'s }'; return s; }\nconst after = 1;"; + expect(functionBody(source, 'run').text).toBe("{ const s = 'it\\'s }'; return s; }"); + }); + + it('balances braces inside a template expression and ignores those in its text', () => { + const source = 'function run(x) { return `}${x.map((i) => { return `{${i}`; })}{`; }\nconst after = 1;'; + expect(functionBody(source, 'run').text).toBe('{ return `}${x.map((i) => { return `{${i}`; })}{`; }'); + }); + + it('ignores braces inside line and block comments', () => { + const source = ['function run() {', ' // }', ' /* } { */', ' return 1;', '}', 'const after = 1;'].join('\n'); + expect(functionBody(source, 'run').text).toBe('{\n // }\n /* } { */\n return 1;\n}'); + }); + + it('keeps a quote inside a comment from opening a string', () => { + const source = ["function run() {", " // don't", ' return 1;', '}', 'const after = 1;'].join('\n'); + expect(functionBody(source, 'run').text.endsWith('return 1;\n}')).toBe(true); + }); + + it('applies to call arguments and ipc handlers too', () => { + expect(callArguments("register(')', () => { /* ) */ });", 'register').text).toBe("(')', () => { /* ) */ })"); + const source = "ipcMain.handle('x:y', () => { return '})'; });\nconst after = 1;"; + expect(ipcHandler(source, 'x:y').text).toBe("('x:y', () => { return '})'; })"); + }); + + it('still reports an unbalanced block', () => { + expect(() => functionBody('function run() { return 1;', 'run')).toThrow(/unbalanced/); + }); +});