Skip to content

[issues/342] Forgiving filename navigation — resolve bare filenames via workspace search#497

Merged
couimet merged 12 commits into
mainfrom
issues/342
Mar 25, 2026
Merged

[issues/342] Forgiving filename navigation — resolve bare filenames via workspace search#497
couimet merged 12 commits into
mainfrom
issues/342

Conversation

@couimet
Copy link
Copy Markdown
Owner

@couimet couimet commented Mar 24, 2026

Summary

RangeLink now resolves bare filenames (without directory paths) when exactly one matching file exists in the workspace. This covers a common scenario where AI tools generate code references like RangeLinkNavigationHandler.ts#L10 without the directory prefix — previously these showed "Cannot find file", now they navigate directly.

Changes

  • Added filename-only fallback to resolveWorkspacePath: when standard resolution fails and the path has no / or \ separators, uses vscode.workspace.findFiles with a **/{filename} glob (capped at 2 results) to find a unique match
  • Introduced ResolvedPath interface with uri and resolvedVia strategy field ('absolute' | 'workspace-relative' | 'filename-fallback'), derived from the existing PathFormat enum to prevent string literal drift
  • Both RangeLinkNavigationHandler and FilePathNavigationHandler now log the resolution strategy at DEBUG level
  • Added 4 QA test cases under filename-fallback-navigation slug (2 automated, 2 manual)
  • Added integration test file filenameOnlyNavigation.test.ts covering unique-match navigation and standard-resolution control case

Related

Summary by CodeRabbit

  • New Features

    • Filename-only navigation fallback: clicking a bare filename opens the file when exactly one workspace match exists.
  • Bug Fixes / UX

    • Added a user-facing warning for ambiguous filename matches: "RangeLink: Multiple files match: {path}".
  • Tests

    • Added QA, integration, and unit tests covering filename-only navigation, ambiguity, nonexistence, and normal path resolution.
  • Documentation

    • Changelog updated to document the new filename navigation behavior.

Charles Ouimet added 5 commits March 23, 2026 23:48
Callers had no visibility into which resolution strategy succeeded — the function returned a bare URI. This made it impossible for handlers to log or adjust UI feedback for the filename-fallback (Issue #342) vs standard resolution.

The new ResolvedPath interface carries `uri` + `resolvedVia` ('absolute' | 'workspace-relative' | 'filename-fallback'). Both navigation handlers now log the strategy at DEBUG level, giving output channel visibility into how each link was resolved.

Benefits:
- Handlers can distinguish fallback navigation from standard resolution in logs
- Opens the door for strategy-aware toast messages without further refactoring
- No behavioral change — existing callers just destructure `.uri` from the result
…ent drift

The 'absolute' and 'workspace-relative' string literals were duplicated between PathFormat (input) and PathResolutionStrategy (output). Using a template literal type ties them together so changes to PathFormat automatically propagate.
…back

Ensures the fallback behavior from Issue #342 is covered by both the QA framework and automated integration tests. Four TCs added under slug `filename-fallback-navigation`: two automated (unique match navigates, relative path uses standard resolution) and two manual (ambiguous/missing filenames show toast — blocked by showWarningMessage in extension host).

The QA validator confirms all 57 `automated: true` entries match integration test IDs.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 24, 2026

Warning

Rate limit exceeded

@couimet has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 14 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ebdc6491-44d1-4a1e-865d-c4d8d8e997e6

📥 Commits

Reviewing files that changed from the base of the PR and between affec45 and 0ddc127.

📒 Files selected for processing (2)
  • packages/rangelink-vscode-extension/src/__tests__/helpers/createMockWorkspace.ts
  • packages/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts

Walkthrough

Adds a filename-only navigation fallback: when a RangeLink contains only a bare filename (no directory separators), the resolver globs the workspace; a single match returns a ResolvedPath and navigation proceeds, multiple matches return an ambiguity sentinel and trigger a warning, and zero matches remain a not-found result. New types and tests added.

Changes

Cohort / File(s) Summary
Type definitions
packages/rangelink-vscode-extension/src/types/ResolvedPath.ts, packages/rangelink-vscode-extension/src/types/index.ts
Added ResolvedPath, PathResolutionStrategy, ResolveWorkspacePathResult, and FILENAME_AMBIGUOUS sentinel; re-exported from types/index.ts.
Resolution util
packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts
Return shape changed to ResolveWorkspacePathResult; added filename-only fallback using workspace.findFiles('**/<escaped>'); returns ResolvedPath with resolvedVia: 'filename-fallback', ambiguity sentinel for multiple matches, and preserves absolute/workspace-relative outcomes.
IDE adapter
packages/rangelink-vscode-extension/src/ide/vscode/VscodeAdapter.ts
Public method resolveWorkspacePath updated to return ResolveWorkspacePathResult and document richer outcomes.
Navigation handlers
packages/rangelink-vscode-extension/src/navigation/FilePathNavigationHandler.ts, packages/rangelink-vscode-extension/src/navigation/RangeLinkNavigationHandler.ts
Consume ResolveWorkspacePathResult: handle FILENAME_AMBIGUOUS by warning and early return, extract uri from ResolvedPath, add debug log of resolvedVia, preserve existing fallbacks for not-found cases.
Messages / i18n
packages/rangelink-vscode-extension/src/i18n/messages.en.ts, packages/rangelink-vscode-extension/src/types/MessageCode.ts
Added WARN_NAVIGATION_FILENAME_AMBIGUOUS message code and English template RangeLink: Multiple files match: {path}.
Tests — unit
packages/rangelink-vscode-extension/src/__tests__/utils/resolveWorkspacePath.test.ts, packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts, packages/rangelink-vscode-extension/src/__tests__/navigation/FilePathNavigationHandler.test.ts, packages/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts
Updated mocks/expectations to the new ResolvedPath shape; added tests covering filename-only fallback success, ambiguity, no-match, and skipping fallback for paths with separators.
Tests — integration & QA
packages/rangelink-vscode-extension/src/__integration-tests__/suite/filenameOnlyNavigation.test.ts, packages/rangelink-vscode-extension/qa/qa-test-cases-v1.1.0-2026-03-18.yaml
New integration suite creating a temp file and asserting bare-filename navigation and directory-relative behavior; QA file documents four test cases for filename-only fallback.
Docs / changelog / config
packages/rangelink-vscode-extension/CHANGELOG.md, .prettierignore, eslint.config.mjs
Changelog entry for forgiving filename navigation; updated Prettier/ESLint ignore lists.
Tests — helpers
packages/rangelink-vscode-extension/src/__tests__/helpers/createMockWorkspace.ts
Mock workspace now includes findFiles for tests exercising filename fallback behaviors.

Sequence Diagram

sequenceDiagram
    participant User
    participant Handler as RangeLinkNavigationHandler
    participant Adapter as VscodeAdapter
    participant Utils as resolveWorkspacePath
    participant Workspace as vscode.workspace
    participant Editor as vscode.window

    User->>Handler: Click RangeLink (bare filename)
    Handler->>Adapter: resolveWorkspacePath(filename)
    Adapter->>Utils: delegate resolution

    Utils->>Workspace: try absolute fs.stat / workspace-relative stat
    Workspace-->>Utils: not found

    alt input has no separators
        Utils->>Workspace: findFiles("**/<escaped-filename>", maxResults=2)
        Workspace-->>Utils: [matches]

        alt exactly 1 match
            Utils-->>Adapter: ResolvedPath{ uri, resolvedVia: 'filename-fallback' }
            Adapter-->>Handler: ResolvedPath
            Handler->>Editor: showTextDocument(resolved.uri)
            Editor-->>User: navigated
        else multiple matches
            Utils-->>Adapter: FILENAME_AMBIGUOUS
            Adapter-->>Handler: FILENAME_AMBIGUOUS
            Handler-->>User: show warning (Multiple files match)
        else 0 matches
            Utils-->>Adapter: undefined
            Adapter-->>Handler: undefined
            Handler-->>User: show not-found warning
        end
    else contains separators
        Utils-->>Adapter: undefined
        Adapter-->>Handler: undefined
        Handler-->>User: show not-found warning
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I hopped through workspace groves at night,
Sniffed a lone filename by moonlight,
One match — I lead you straight and true,
Two — I stop, I warn, I twitch my shoe,
None — I whisper "not found" and skip the cue.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: implementing filename-only navigation via workspace search for bare filenames, which is the primary objective of the changeset.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #342: bare filename resolution when exactly one match exists [#342], ambiguity handling via FILENAME_AMBIGUOUS sentinel [#342], workspace findFiles integration [#342], and user feedback via new warning message [#342].
Out of Scope Changes check ✅ Passed All changes are directly related to implementing filename-only navigation. Minor ancillary updates to .prettierignore and eslint.config.mjs (directories for work artifacts) are reasonable supporting changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issues/342

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 24, 2026

⚠️ QA Coverage Gap Detected

The PR introduces a new feature for forgiving filename navigation, which allows links with bare filenames to navigate correctly when there is a unique match. This includes new scenarios for both successful navigation and handling of ambiguous or nonexistent filenames.

Suggested test cases:

  • Filename-Only Navigation Fallback: Bare filename with unique match navigates to correct file and line (automatable)
  • Filename-Only Navigation Fallback: Bare filename with multiple matches shows ambiguity warning (manual — Requires verification of a toast notification for multiple matches.)
  • Filename-Only Navigation Fallback: Bare filename with no matches shows file-not-found warning (manual — Requires verification of a toast notification for file not found.)
  • Filename-Only Navigation Fallback: Path with directory separators uses standard resolution, not fallback (automatable)

Generated by QA Gap Check (GPT-4o-mini via GitHub Models)

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: 2

🧹 Nitpick comments (3)
packages/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts (1)

573-575: Standardize resolvedVia mocks to enum values across updated tests.

These updated mock blocks should use the project enum member(s) instead of 'workspace-relative' string literals to keep setup type-safe and resilient to enum renames.

As per coding guidelines: "Use literal values in expect() assertions for project enums, user-facing text, and config keys - Use enum values in test setup/mocks for type safety".

Also applies to: 780-782, 828-830, 1099-1101, 1211-1213

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

In
`@packages/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts`
around lines 573 - 575, The mock setup uses the string 'workspace-relative' for
the resolvedVia field; replace these with the project's enum member (import the
project's ResolvedVia/ResolvedViaKind enum) and use the appropriate enum value
instead of the literal string in the mockResolvedValue for
mockAdapter.resolveWorkspacePath (and the other identical mocks at the indicated
locations). Update imports to include the enum, change resolvedVia:
'workspace-relative' to resolvedVia: ResolvedVia.<WORKSPACE_RELATIVE_MEMBER>,
and run tests to ensure type-safety and consistency.
packages/rangelink-vscode-extension/src/__tests__/navigation/FilePathNavigationHandler.test.ts (1)

130-130: Use enum members for resolvedVia in test mocks.

At Line 130, Line 171, and Line 183, prefer PathFormat enum values instead of raw strings so mocks stay type-safe if enum contracts evolve.

As per coding guidelines: "Use literal values in expect() assertions for project enums, user-facing text, and config keys - Use enum values in test setup/mocks for type safety".

Also applies to: 171-171, 181-183

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

In
`@packages/rangelink-vscode-extension/src/__tests__/navigation/FilePathNavigationHandler.test.ts`
at line 130, Replace the raw string literals used for resolvedVia in test mocks
with the corresponding PathFormat enum members (e.g., use PathFormat.<member>
that maps to 'workspace-relative') in the .mockResolvedValue({... resolvedVia:
...}) calls and any related expect() assertions in
FilePathNavigationHandler.test (locations around the shown .mockResolvedValue
and the other mentions at lines ~171 and ~183); import PathFormat if needed and
use the enum members instead of raw strings so the mocks remain type-safe if the
enum changes.
packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts (1)

412-416: Prefer enum-backed resolvedVia mock value.

At Line 414, switch from the raw string literal to the corresponding enum member for stronger type safety in test setup.

As per coding guidelines: "Use literal values in expect() assertions for project enums, user-facing text, and config keys - Use enum values in test setup/mocks for type safety".

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

In
`@packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts`
around lines 412 - 416, Replace the raw string literal used for
mockResolved.resolvedVia with the appropriate enum member from the codebase
(rather than 'workspace-relative'); import the enum and set resolvedVia to that
enum member (e.g., ResolvedVia.WorkspaceRelative) in the mockResolved object
used by spyOnResolveWorkspacePath(), so the test uses the enum for type-safe
mocking.
🤖 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/rangelink-vscode-extension/qa/qa-test-cases-v1.1.0-2026-03-18.yaml`:
- Around line 2377-2386: Test case filename-fallback-navigation-002 expects the
generic "Cannot find file" message for ambiguous filename matches; update both
the YAML expected_result and the code that shows the toast so ambiguity is
reported instead of "not found". Change the QA entry (id
filename-fallback-navigation-002, feature 'Filename-Only Navigation Fallback')
expected_result to assert a warning like "RangeLink: Multiple files found for
filename: index.ts" and modify the file-resolution path in the code (the
function that resolves RangeLink targets — e.g., resolveRangeLinkTarget /
findFilesByName / handleRangeLinkClick) so when more than one match is returned
it emits a distinct toast message indicating multiple matches and does not
navigate. Ensure the toast text exactly matches the updated expected_result.

In `@packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts`:
- Around line 64-69: The glob pattern built in resolveWorkspacePath (where
pattern = `**/${linkPath}`) directly interpolates linkPath and fails for
filenames containing glob metacharacters; update the pattern construction in
resolveWorkspacePath to escape glob metacharacters using VS Code bracket-range
escaping (e.g., `[` → `[[]`, `]` → `[]]`, `*` → `[*]`, `?` → `[?]`, `{`/`}`
similarly) before inserting into `**/${escaped}` so filenames like `[id].ts`,
`foo?.ts`, `file*.ts` are treated as literals; also add unit tests to the
filename-fallback test suite that cover bare filenames containing `*`, `?`, `[`
and `]` to prevent regressions.

---

Nitpick comments:
In
`@packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts`:
- Around line 412-416: Replace the raw string literal used for
mockResolved.resolvedVia with the appropriate enum member from the codebase
(rather than 'workspace-relative'); import the enum and set resolvedVia to that
enum member (e.g., ResolvedVia.WorkspaceRelative) in the mockResolved object
used by spyOnResolveWorkspacePath(), so the test uses the enum for type-safe
mocking.

In
`@packages/rangelink-vscode-extension/src/__tests__/navigation/FilePathNavigationHandler.test.ts`:
- Line 130: Replace the raw string literals used for resolvedVia in test mocks
with the corresponding PathFormat enum members (e.g., use PathFormat.<member>
that maps to 'workspace-relative') in the .mockResolvedValue({... resolvedVia:
...}) calls and any related expect() assertions in
FilePathNavigationHandler.test (locations around the shown .mockResolvedValue
and the other mentions at lines ~171 and ~183); import PathFormat if needed and
use the enum members instead of raw strings so the mocks remain type-safe if the
enum changes.

In
`@packages/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts`:
- Around line 573-575: The mock setup uses the string 'workspace-relative' for
the resolvedVia field; replace these with the project's enum member (import the
project's ResolvedVia/ResolvedViaKind enum) and use the appropriate enum value
instead of the literal string in the mockResolvedValue for
mockAdapter.resolveWorkspacePath (and the other identical mocks at the indicated
locations). Update imports to include the enum, change resolvedVia:
'workspace-relative' to resolvedVia: ResolvedVia.<WORKSPACE_RELATIVE_MEMBER>,
and run tests to ensure type-safety and consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 75ccd31d-79f8-45cb-8552-25f14acf161a

📥 Commits

Reviewing files that changed from the base of the PR and between d6fb2c5 and e6f7d88.

📒 Files selected for processing (13)
  • packages/rangelink-vscode-extension/CHANGELOG.md
  • packages/rangelink-vscode-extension/qa/qa-test-cases-v1.1.0-2026-03-18.yaml
  • packages/rangelink-vscode-extension/src/__integration-tests__/suite/filenameOnlyNavigation.test.ts
  • packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts
  • packages/rangelink-vscode-extension/src/__tests__/navigation/FilePathNavigationHandler.test.ts
  • packages/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts
  • packages/rangelink-vscode-extension/src/__tests__/utils/resolveWorkspacePath.test.ts
  • packages/rangelink-vscode-extension/src/ide/vscode/VscodeAdapter.ts
  • packages/rangelink-vscode-extension/src/navigation/FilePathNavigationHandler.ts
  • packages/rangelink-vscode-extension/src/navigation/RangeLinkNavigationHandler.ts
  • packages/rangelink-vscode-extension/src/types/ResolvedPath.ts
  • packages/rangelink-vscode-extension/src/types/index.ts
  • packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts

Comment on lines +2377 to +2386
- id: filename-fallback-navigation-002
feature: 'Filename-Only Navigation Fallback'
scenario: 'Bare filename with multiple matches shows file-not-found warning'
preconditions:
- 'Extension installed from .vsix build'
- 'Two or more files with the same name exist in different directories (e.g., index.ts in src/ and lib/)'
steps:
- 'Click a RangeLink using only the ambiguous filename (e.g., index.ts#L1)'
expected_result: 'A warning toast is shown: "RangeLink: Cannot find file: index.ts". No navigation occurs.'
automated: false
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

Distinguish ambiguous filenames from missing files.

This case currently expects the same Cannot find file warning as the no-match path. For the new fallback that is misleading—the actionable problem is ambiguity, not absence. Please make the warning, and this QA expectation, say that multiple matches were found.

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

In `@packages/rangelink-vscode-extension/qa/qa-test-cases-v1.1.0-2026-03-18.yaml`
around lines 2377 - 2386, Test case filename-fallback-navigation-002 expects the
generic "Cannot find file" message for ambiguous filename matches; update both
the YAML expected_result and the code that shows the toast so ambiguity is
reported instead of "not found". Change the QA entry (id
filename-fallback-navigation-002, feature 'Filename-Only Navigation Fallback')
expected_result to assert a warning like "RangeLink: Multiple files found for
filename: index.ts" and modify the file-resolution path in the code (the
function that resolves RangeLink targets — e.g., resolveRangeLinkTarget /
findFilesByName / handleRangeLinkClick) so when more than one match is returned
it emits a distinct toast message indicating multiple matches and does not
navigate. Ensure the toast text exactly matches the updated expected_result.

Comment thread packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts Outdated
Charles Ouimet added 4 commits March 24, 2026 09:24
…lenames

Bare filenames containing glob metacharacters ([id].ts, foo?.ts) were interpreted as patterns by findFiles, making real files unresolvable. Now escaped using VS Code's bracket-range convention before building the glob.

Ambiguous filenames (2+ matches) previously showed the generic "Cannot find file" warning — misleading when the file exists in multiple locations. Now returns a distinct FILENAME_AMBIGUOUS sentinel so handlers show "Multiple files match: {filename}" instead, giving users actionable feedback to add a directory prefix.

Benefits:
- Next.js/SvelteKit [slug].ts files are now resolvable via bare filename
- Users know when to disambiguate vs when the file is truly missing
- resolveWorkspacePath return type cleanly signals three outcomes: resolved, ambiguous, not found

Ref: #497 (review)
…vedVia

Test setup was using string literals ('workspace-relative', 'absolute') for the resolvedVia field in mock objects. Project rule T003 requires enum values in setup for type safety — literals are reserved for assertions where they freeze the contract.

Ref: #497 (review)
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts (1)

65-98: ⚠️ Potential issue | 🟠 Major

Bare filenames can still bypass the ambiguity check.

Because the workspace-relative loop runs first, index.ts#L1 will immediately resolve to <workspace>/index.ts even if src/index.ts also exists. That still guesses on ambiguous bare filenames, which is the case this feature is supposed to reject.

💡 One straightforward way to fix it
   const workspaceFolders = ideInstance.workspace.workspaceFolders;
   if (!workspaceFolders || workspaceFolders.length === 0) {
     return undefined;
   }

+  const isBareFilename = !linkPath.includes('/') && !linkPath.includes('\\');
+  if (isBareFilename) {
+    const pattern = `**/${escapeGlobPattern(linkPath)}`;
+    try {
+      const matches = await ideInstance.workspace.findFiles(
+        pattern,
+        undefined,
+        AMBIGUITY_THRESHOLD,
+      );
+      if (matches.length === 1) {
+        return { uri: matches[0], resolvedVia: 'filename-fallback' };
+      }
+      if (matches.length >= AMBIGUITY_THRESHOLD) {
+        return FILENAME_AMBIGUOUS;
+      }
+    } catch {
+      // findFiles failed (e.g., no workspace open) — fall through to undefined
+    }
+    return undefined;
+  }
+
   for (const folder of workspaceFolders) {
     const absolutePath = path.join(folder.uri.fsPath, linkPath);
     const uri = ideInstance.Uri.file(absolutePath);

     try {
       await ideInstance.workspace.fs.stat(uri);
       return { uri, resolvedVia: 'workspace-relative' };
     } catch {
       // File doesn't exist in this workspace folder, try next
       continue;
     }
   }
-
-  const isBareFilename = !linkPath.includes('/') && !linkPath.includes('\\');
-  if (isBareFilename) {
-    const pattern = `**/${escapeGlobPattern(linkPath)}`;
-    ...
-  }

Please also add a regression case for a workspace-root index.ts plus another src/index.ts.

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

In `@packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts` around
lines 65 - 98, The current logic lets a bare filename like "index.ts#L1" resolve
immediately via the workspace-relative loop, bypassing the ambiguity check;
change resolve behavior so bare filenames are disambiguated before accepting a
single workspace match: either (A) if isBareFilename run the
findFiles(...)/AMBIGUITY_THRESHOLD check first and return FILENAME_AMBIGUOUS if
ambiguous, or (B) when isBareFilename, collect matches across all
workspaceFolders instead of returning on the first stat success and only return
a single match when exactly one unique path is found (otherwise return
FILENAME_AMBIGUOUS); update the resolvedVia values accordingly and keep using
escapeGlobPattern, ideInstance.workspace.findFiles, AMBIGUITY_THRESHOLD and
FILENAME_AMBIGUOUS; add a regression test covering a workspace-root index.ts
plus src/index.ts to assert ambiguity is reported.
🧹 Nitpick comments (1)
packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts (1)

44-47: Keep the new workspace search behind the adapter boundary.

This PR adds another direct VS Code behavior call in src/utils. If resolveWorkspacePath keeps growing here, the ideAdapter abstraction stops buying much in testability or IDE portability. I’d move the findFiles branch into VscodeAdapter.resolveWorkspacePath() (or a helper under src/ide/vscode/) and keep this utility API-neutral.

As per coding guidelines, "No direct vscode.* behavior calls — use ideAdapter - Call behavior methods through ideAdapter (e.g., ideAdapter.showTextDocument()); import constants/enums directly from vscode".

Also applies to: 83-88

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

In `@packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts` around
lines 44 - 47, The resolveWorkspacePath util currently calls vscode.findFiles
directly; move the VS Code–specific workspace search logic into the IDE adapter
boundary (e.g., implement a VscodeAdapter.resolveWorkspacePath() or helper under
src/ide/vscode/) and have resolveWorkspacePath call
ideAdapter.resolveWorkspacePath() instead so the util remains IDE-agnostic;
update the code paths that perform the findFiles behavior (also referenced
around the other branch at lines 83–88) to use the adapter method and remove any
direct vscode.* calls from src/utils/resolveWorkspacePath.ts.
🤖 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/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts`:
- Around line 738-774: Add assertions to confirm the ambiguous case
short-circuits: after calling RangeLinkNavigationHandler.navigateToLink with
resolveWorkspacePath mocked to FILENAME_AMBIGUOUS, assert that the adapter's
navigation methods were not invoked (e.g.,
expect(mockAdapter.showTextDocument).not.toHaveBeenCalled() and
expect(mockAdapter.openTextDocument).not.toHaveBeenCalled() or whichever
navigation/open-file methods exist on mockAdapter). Ensure you create
spies/mocks for mockAdapter.showTextDocument and mockAdapter.openTextDocument
before invoking handler.navigateToLink so the test fails if those are called.

---

Outside diff comments:
In `@packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts`:
- Around line 65-98: The current logic lets a bare filename like "index.ts#L1"
resolve immediately via the workspace-relative loop, bypassing the ambiguity
check; change resolve behavior so bare filenames are disambiguated before
accepting a single workspace match: either (A) if isBareFilename run the
findFiles(...)/AMBIGUITY_THRESHOLD check first and return FILENAME_AMBIGUOUS if
ambiguous, or (B) when isBareFilename, collect matches across all
workspaceFolders instead of returning on the first stat success and only return
a single match when exactly one unique path is found (otherwise return
FILENAME_AMBIGUOUS); update the resolvedVia values accordingly and keep using
escapeGlobPattern, ideInstance.workspace.findFiles, AMBIGUITY_THRESHOLD and
FILENAME_AMBIGUOUS; add a regression test covering a workspace-root index.ts
plus src/index.ts to assert ambiguity is reported.

---

Nitpick comments:
In `@packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts`:
- Around line 44-47: The resolveWorkspacePath util currently calls
vscode.findFiles directly; move the VS Code–specific workspace search logic into
the IDE adapter boundary (e.g., implement a VscodeAdapter.resolveWorkspacePath()
or helper under src/ide/vscode/) and have resolveWorkspacePath call
ideAdapter.resolveWorkspacePath() instead so the util remains IDE-agnostic;
update the code paths that perform the findFiles behavior (also referenced
around the other branch at lines 83–88) to use the adapter method and remove any
direct vscode.* calls from src/utils/resolveWorkspacePath.ts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3fe1f21c-bacd-43a4-9ae4-87887ead2a9c

📥 Commits

Reviewing files that changed from the base of the PR and between e6f7d88 and 4b038d7.

📒 Files selected for processing (15)
  • .prettierignore
  • eslint.config.mjs
  • packages/rangelink-vscode-extension/qa/qa-test-cases-v1.1.0-2026-03-18.yaml
  • packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts
  • packages/rangelink-vscode-extension/src/__tests__/navigation/FilePathNavigationHandler.test.ts
  • packages/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts
  • packages/rangelink-vscode-extension/src/__tests__/utils/resolveWorkspacePath.test.ts
  • packages/rangelink-vscode-extension/src/i18n/messages.en.ts
  • packages/rangelink-vscode-extension/src/ide/vscode/VscodeAdapter.ts
  • packages/rangelink-vscode-extension/src/navigation/FilePathNavigationHandler.ts
  • packages/rangelink-vscode-extension/src/navigation/RangeLinkNavigationHandler.ts
  • packages/rangelink-vscode-extension/src/types/MessageCode.ts
  • packages/rangelink-vscode-extension/src/types/ResolvedPath.ts
  • packages/rangelink-vscode-extension/src/types/index.ts
  • packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts
✅ Files skipped from review due to trivial changes (4)
  • .prettierignore
  • eslint.config.mjs
  • packages/rangelink-vscode-extension/src/i18n/messages.en.ts
  • packages/rangelink-vscode-extension/src/tests/navigation/FilePathNavigationHandler.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/rangelink-vscode-extension/src/types/index.ts
  • packages/rangelink-vscode-extension/src/tests/ide/vscode/VscodeAdapter.test.ts
  • packages/rangelink-vscode-extension/src/navigation/FilePathNavigationHandler.ts
  • packages/rangelink-vscode-extension/src/tests/utils/resolveWorkspacePath.test.ts
  • packages/rangelink-vscode-extension/src/types/ResolvedPath.ts

Charles Ouimet added 2 commits March 24, 2026 23:47
Bare filenames like `index.ts` bypassed the ambiguity check because the workspace-relative loop ran first — resolving `<workspace>/index.ts` immediately even when `src/index.ts` also existed. Moving the `findFiles` check before the workspace-relative loop ensures bare filenames always go through ambiguity detection.

The bare-filename block now terminates with `return undefined` so bare filenames never fall through to the workspace-relative loop. Paths with directory separators are unaffected.

Benefits:
- `index.ts#L1` correctly reports "Multiple files match" when ambiguous
- Bare filenames that exist only at workspace root resolve via filename-fallback (not workspace-relative)
- 4 new regression tests verify the reorder's behavioral effects

Ignored Feedback:
- Move findFiles behind adapter boundary: `resolveWorkspacePath` is already called *by* `VscodeAdapter.resolveWorkspacePath()` — it's the adapter's implementation, not handler code. P004 targets direct vscode.* calls in business logic.

Ref: #497 (review)
The existing test verified the warning message but didn't prove that navigation was actually prevented. Added `not.toHaveBeenCalled()` assertions for `findOpenUntitledFile` and `showTextDocument` to lock down the early return.

Ref: #497 (review)
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/rangelink-vscode-extension/src/__tests__/utils/resolveWorkspacePath.test.ts (1)

184-184: Extract the fallback limit into a named constant.

These 2 literals are the ambiguity threshold, so the suite will drift if that cap changes in the resolver. A local AMBIGUITY_THRESHOLD keeps the intent clear and avoids repeating the same semantic value through the block.

Suggested cleanup
+const AMBIGUITY_THRESHOLD = 2;
+
 ...
-      expect(mockFindFiles).toHaveBeenCalledWith('**/auth.ts', undefined, 2);
+      expect(mockFindFiles).toHaveBeenCalledWith(
+        '**/auth.ts',
+        undefined,
+        AMBIGUITY_THRESHOLD,
+      );
As per coding guidelines "No magic numbers - Define named constants with SCREAMING_SNAKE_CASE for all numeric literals with semantic meaning."

Also applies to: 195-195, 204-204, 227-227, 239-239, 250-250, 261-261, 272-272

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

In
`@packages/rangelink-vscode-extension/src/__tests__/utils/resolveWorkspacePath.test.ts`
at line 184, Replace the repeated literal 2 used as the ambiguity/fallback limit
in resolveWorkspacePath tests with a named constant (e.g. AMBIGUITY_THRESHOLD)
declared at the top of the test file; update every
expect(mockFindFiles).toHaveBeenCalledWith(..., 2) occurrence (and any other
places using that semantic 2) to use AMBIGUITY_THRESHOLD so the intent is clear
and the value is centralized for future changes.
🤖 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/rangelink-vscode-extension/src/__tests__/helpers/createMockWorkspace.ts`:
- Line 49: The shared mock currently returns a non-empty default for findFiles
(jest.fn().mockResolvedValue([{ fsPath: '/workspace/mock-file', scheme: 'file'
}])) which causes unrelated tests to hit the filename-fallback path; change the
default in the createMockWorkspace mock so findFiles resolves to an empty array
([]) and make tests that need a positive match explicitly override findFiles to
return the desired result; update references to the mock's findFiles behavior
where necessary so fallback-specific tests set findFiles on the returned mock
object.

---

Nitpick comments:
In
`@packages/rangelink-vscode-extension/src/__tests__/utils/resolveWorkspacePath.test.ts`:
- Line 184: Replace the repeated literal 2 used as the ambiguity/fallback limit
in resolveWorkspacePath tests with a named constant (e.g. AMBIGUITY_THRESHOLD)
declared at the top of the test file; update every
expect(mockFindFiles).toHaveBeenCalledWith(..., 2) occurrence (and any other
places using that semantic 2) to use AMBIGUITY_THRESHOLD so the intent is clear
and the value is centralized for future changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d608e328-89b3-40c2-889d-df101a6a5a0a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b038d7 and affec45.

📒 Files selected for processing (4)
  • packages/rangelink-vscode-extension/src/__tests__/helpers/createMockWorkspace.ts
  • packages/rangelink-vscode-extension/src/__tests__/navigation/RangeLinkNavigationHandler.test.ts
  • packages/rangelink-vscode-extension/src/__tests__/utils/resolveWorkspacePath.test.ts
  • packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/rangelink-vscode-extension/src/utils/resolveWorkspacePath.ts
  • packages/rangelink-vscode-extension/src/tests/navigation/RangeLinkNavigationHandler.test.ts

Comment thread packages/rangelink-vscode-extension/src/__tests__/helpers/createMockWorkspace.ts Outdated
…ck resolveWorkspacePath

The shared createMockWorkspace returned a single-match findFiles by default, causing unrelated tests to silently succeed via the filename-fallback path. Defaulting to [] makes fallback behavior opt-in. Three handler test describe blocks that use bare filenames now explicitly spy on resolveWorkspacePath.

Ignored Feedback:
- Extract AMBIGUITY_THRESHOLD constant in tests: the literal `2` in assertions is acceptable as a contract-freezing value per T003 spirit.

Ref: #497 (review)
@couimet couimet merged commit a287be5 into main Mar 25, 2026
3 checks passed
@couimet couimet deleted the issues/342 branch March 25, 2026 04:23
couimet pushed a commit that referenced this pull request Mar 25, 2026
…lenameOnlyNavigation

Addressed CodeRabbit nitpicks and migrated the new PR #497 test file:
- `clipboardPreservation.test.ts`: replaced 5 inline clipboard read+assert blocks with `assertClipboardChanged`/`assertClipboardRestored` helpers (kept separate `#L` assertion where needed)
- `logBasedUiValidation.test.ts`: replaced `fs.unlinkSync` with `cleanupFiles`, removed `fs` import
- `navigationToastSettings.test.ts`: replaced manual file creation with `createWorkspaceFile`, derive `testFilename` via `path.basename(uri.fsPath)`
- `filePathNavigation.test.ts`: extracted `settle(1000)` into `NON_EXISTENT_PATH_SETTLE_MS` constant
- `filenameOnlyNavigation.test.ts` (from PR #497): replaced inline `getWorkspaceRoot`, `navigateViaHandleLinkClick`, activation boilerplate with shared helpers (kept `fs.mkdirSync`/`fs.writeFileSync` for subdirectory file creation — not supported by `createWorkspaceFile`)

Ignored Feedback:
- dirtyBufferWarning spy assertion: can't spy on extension internals from integration tests
- resetRangelinkSettings scope: behavioral change beyond refactor scope

Ref: #499 (review)
couimet added a commit that referenced this pull request Mar 25, 2026
* [refactor] Create testEnv.ts — shared workspace root, activation, and timing constants

First step in extracting shared utilities from the 14 integration test files. `testEnv.ts` provides `getWorkspaceRoot()` (duplicated in 11 files), `activateExtension()` (duplicated in 11 files), `settle()` (inline setTimeout pattern in 10+ files), and timing constants (`SETTLE_MS`, `TERMINAL_READY_MS`, `POLL_INTERVAL_MS`, `POLL_TIMEOUT_MS`) that were inconsistently named and valued across files.

* [refactor] Create shared helper modules for integration tests

Created 7 helper modules that extract duplicated patterns from the 14 integration test files:
- `fileHelpers.ts`: createWorkspaceFile, openEditor, cleanupFiles, closeAllEditors
- `clipboardHelpers.ts`: sentinel write/read/assert pattern (used in 3 files)
- `terminalHelpers.ts`: createAndBindTerminal with settle wait
- `editorHelpers.ts`: waitForActiveEditor (polling), selectAll
- `logHelpers.ts`: createLogger with suite-scoped prefix
- `settingsHelpers.ts`: loadSettingsProfile, resetRangelinkSettings (extracted from smartPadding)
- `navigationHelpers.ts`: navigateViaHandleLinkClick debounce pattern (duplicated in navigationPrecision + navigationClamping)

All exported from helpers/index.ts. No test files modified yet — migration happens in subsequent commits.

* [refactor] Migrate smartPadding.test.ts to shared integration test helpers

First test file migration — validates the helper API works in the VS Code host. Replaced 6 inline utility functions and 3 inline constants with imports from the shared helpers: `activateExtension()`, `getWorkspaceRoot()`, `settle()`, `createLogger()`, `loadSettingsProfile()`, `resetRangelinkSettings()`, `waitForActiveEditor()`, `cleanupFiles()`, `closeAllEditors()`.

The test-specific `setupEditorPair` and `setupUntitledEditorPair` functions remain inline — they're coupled to the editor-to-editor binding flow.

* [refactor] Migrate clipboardPreservation.test.ts and unbind.test.ts to shared helpers

Replaced inline `getWorkspaceRoot`, activation boilerplate, file creation/cleanup, and timing constants with imports from the shared helpers. `clipboardPreservation` now uses `createAndBindTerminal`, `createWorkspaceFile`, `openEditor`, `writeClipboardSentinel`, `CLIPBOARD_SENTINEL`, `cleanupFiles`, `closeAllEditors`. `unbind` now uses `createWorkspaceFile`, `settle(TERMINAL_READY_MS)`, `cleanupFiles`, `closeAllEditors`. Both files dropped `fs` and `path` imports entirely.

* [refactor] Migrate dirtyBufferWarning.test.ts and linkGeneration.test.ts to shared helpers

Both files replaced inline `getWorkspaceRoot`, activation boilerplate, and file creation/cleanup with shared helper imports. `linkGeneration` switched from `string[]` file tracking to `Uri[]` with `cleanupFiles()`. Both files dropped `fs` and `path` imports entirely.

* [refactor] Migrate 5 navigation test files to shared helpers

Replaced inline `getWorkspaceRoot`, activation boilerplate, file creation/cleanup, timing constants, and the duplicated `navigateViaHandleLinkClick` function across all 5 navigation test files:
- `navigationPrecision.test.ts`: replaced 78-line inline `navigateViaHandleLinkClick` + boilerplate with imports
- `navigationClamping.test.ts`: same — identical `navigateViaHandleLinkClick` eliminated
- `navigationToastSettings.test.ts`: replaced both `navigateViaHandleLinkClick` and inline `settle`/`SETTLE_MS`
- `filePathNavigation.test.ts`: replaced file creation with `createWorkspaceFile`, cleanup with `cleanupFiles`
- `untitledNavigation.test.ts`: replaced activation, `settle`, `closeAllEditors` (kept `navigateToUntitledLink` per-file — different pattern matching by URI)

* [refactor] Migrate logBasedUiValidation.test.ts to shared helpers

Replaced inline `getWorkspaceRoot`, activation boilerplate, `SETTLE_MS`/`settle`, and file creation with shared helper imports. Kept the test-specific `bindTerminal` and `openAndSelectLines` functions per-file — they have unique lifecycle management (terminal disposed in teardown).

* Ran `fix`

* Ran `fix`

* Ignore for coverage

* Handle `eslint-disable-line no-undef`

* [fix] Restore dropped tests, add QA entries, fix eslint no-undef

CI caught three issues from the navigation test migration:
- `navigationClamping.test.ts` was missing tests 002–004 (accidentally rewritten with different names/assertions instead of preserving originals)
- `navigationPrecision.test.ts` had two new `char-navigation` tests without matching QA YAML entries — added `char-navigation-001` and `char-navigation-002` entries with `automated: true`
- `logHelpers.ts` was missing `eslint-disable-line no-undef` on the `console.log` call (present in the original inline version but dropped during extraction)
- `jest.config.js` coverage exclusion widened from single `getLogCapture.ts` to all VS Code-dependent helpers except `logBasedUiAssertions.ts`

* [PR feedback] Harden integration test helpers — collision, console, terminal leak, command constant

Addressed 4 items from CodeRabbit review on PR #499:
- `fileHelpers.ts`: added incrementing counter to `createWorkspaceFile` filename to prevent collision on fast repeated calls
- `logHelpers.ts`: replaced `console.log` with `node:console` import — removes eslint-disable workaround
- `terminalHelpers.ts`: wrapped `bindToTerminalHere` in try/catch to dispose terminal on failure — prevents leaks in test failures
- `navigationHelpers.ts`: replaced string literal `'rangelink.handleDocumentLinkClick'` with `CMD_HANDLE_DOCUMENT_LINK_CLICK` constant

Ignored Feedback:
- resetRangelinkSettings scope change: the function was extracted exactly as it existed in smartPadding.test.ts — changing its semantics to enumerate all rangelink keys is a behavioral change beyond the refactor scope

Ref: #499 (review)

* [PR feedback] Use shared helpers in remaining test files + migrate filenameOnlyNavigation

Addressed CodeRabbit nitpicks and migrated the new PR #497 test file:
- `clipboardPreservation.test.ts`: replaced 5 inline clipboard read+assert blocks with `assertClipboardChanged`/`assertClipboardRestored` helpers (kept separate `#L` assertion where needed)
- `logBasedUiValidation.test.ts`: replaced `fs.unlinkSync` with `cleanupFiles`, removed `fs` import
- `navigationToastSettings.test.ts`: replaced manual file creation with `createWorkspaceFile`, derive `testFilename` via `path.basename(uri.fsPath)`
- `filePathNavigation.test.ts`: extracted `settle(1000)` into `NON_EXISTENT_PATH_SETTLE_MS` constant
- `filenameOnlyNavigation.test.ts` (from PR #497): replaced inline `getWorkspaceRoot`, `navigateViaHandleLinkClick`, activation boilerplate with shared helpers (kept `fs.mkdirSync`/`fs.writeFileSync` for subdirectory file creation — not supported by `createWorkspaceFile`)

Ignored Feedback:
- dirtyBufferWarning spy assertion: can't spy on extension internals from integration tests
- resetRangelinkSettings scope: behavioral change beyond refactor scope

Ref: #499 (review)

* Remove unused import
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Navigating to links could be more forgiving if there's a single filename in a workspace

1 participant