Skip to content

Open path:line[:col] cmd-click links at the right position - #8152

Open
liflovs wants to merge 2 commits into
manaflow-ai:mainfrom
liflovs:fix-cmd-click-line-column-locator
Open

liflovs wants to merge 2 commits into
manaflow-ai:mainfrom
liflovs:fix-cmd-click-line-column-locator

Conversation

@liflovs

@liflovs liflovs commented Jul 15, 2026 •

Copy link
Copy Markdown

Summary

  • What changed? Cmd-click on a terminal file path with a trailing line locator — src/state.js:87 or src/state.js:87:12, the form printed by compilers, grep -n, and stack traces — now resolves to the file and opens the editor at that line/column. Previously such a token did not linkify at all (no pointing-hand cursor, dead click).
  • Why? The cmd-click path resolver locates the clicked token by probing it for existence. src/state.js:87 is never a real file, so resolution returned nil and the link silently no-op'd. The bare path src/state.js worked; the :87 broke detection.

The fix has two parts, both in the shared, well-tested path layer:

  1. TerminalPathResolver now probes the locator-stripped spelling as an additional candidate — appended after every literal candidate, so a real file whose name genuinely ends in :<number> still wins first. Locator segments must be positive integers and leave a non-empty token, and the existence probe is the real guard, so ordinary paths and path: (no number) are untouched.
  2. The trailing :line[:col] is parsed off the clicked token and forwarded to the preferred editor as the path:line[:col] argument that Zed, VS Code (code), and Sublime accept. The UI-test capture seam and the system-default fallback still receive the bare path (non-editor handlers can't take a position), and the in-cmux preview route already opens the stripped path.

No user-facing strings were added, so no localization changes.

Testing

Behavior lives entirely in the pure String/TerminalPathResolver/PreferredEditorService layer. Added a two-commit red/green regression (commit 1 = failing test, commit 2 = fix) plus unit coverage:

  • TerminalPathResolverTests — locator resolution (:line, :line:col), literal-file-with-colon precedence, splittingTrailingLineColumn parsing (rejects path:, path:noline, :0, empty token), and the TerminalFilePathLineLocator rawToken-vs-resolved-path guard.
  • PreferredEditorServiceTests — the configured editor command receives path:87:12 and path:87, while capture/fallback keep the bare path.

Verified by extracting the pure sources + their real tests into a standalone SwiftPM package and running swift test (the full app build needs the GhosttyKit xcframework + submodules):

  • Resolver suite: 33 tests, 6 suites — all pass. Confirmed the two locator-resolution tests go red when the candidate-stripping is removed (proves the regression test catches the bug).
  • Editor-service suite: 6 tests — all pass (including the 4 pre-existing, so bare-path behavior is unchanged).

Not verified locally: the full macOS app/Xcode build of GhosttyTerminalView.swift (requires the GhosttyKit binary + bonsplit submodule not present in a shallow fork clone). The app-target edit is two call sites routed through one new private helper using the public TerminalFilePathLineLocator API.

Demo Video

  • N/A (behavior verified via unit tests; the app-target build could not be produced in this environment).

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally (unit tests via standalone SwiftPM package; app-target build not possible in this environment)
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed (no docs/changelog impact)
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes cmd-click on terminal paths with trailing line/column (e.g., src/state.js:87:12) so the file opens at the correct position in the preferred editor. Previously these tokens didn’t linkify; now they resolve and pass path:line[:col] to editors like Zed, code, and Sublime.

  • Bug Fixes
    • TerminalPathResolver now also probes a locator-stripped candidate after literal candidates, preserving real filenames that end with :<number>.
    • Added parsing of trailing :line[:col] and a guard to avoid treating literal colon-number filenames as positions; position is forwarded only when appropriate.
    • PreferredEditorService.open accepts optional line/column and sends path:line[:col] to the editor command; capture and system-open still receive the bare path.
    • GhosttyTerminalView now forwards the parsed line/column when opening in the preferred editor.

Written for commit 431a2de. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Cmd-clicking file references with :line or :line:column locators now opens the file at the specified position in the preferred editor.
    • File resolution now supports paths with trailing line and column references while preserving valid filenames containing colon-number segments.
    • Preferred editor commands receive line and column information when available.
  • Bug Fixes

    • Improved handling of ambiguous file paths and locator suffixes.

liflovs added 2 commits July 15, 2026 10:38
Cmd-click resolves a clicked token to a file by probing it for existence.
Text like `src/state.js:87` (compiler output, grep -n, stack traces) never
matches a real file, so the token does not resolve and the link is dead.

Assert that `resolveQuicklookPath` treats a trailing `:line` / `:line:col`
locator as pointing at the file itself. Red until the resolver learns to
probe the locator-stripped spelling.
Cmd-click resolved a clicked token to a file by probing it for existence, so
`src/state.js:87` and `src/state.js:87:12` (compiler output, grep -n, stack
traces) never matched a file and the link was dead.

Teach the path resolver to also probe the locator-stripped spelling, after
every literal candidate so a real file whose name ends in a colon-number
still wins. Parse the trailing `:line` / `:line:col` off the clicked token
and forward it to the preferred editor as the `path:line[:col]` locator Zed,
VS Code, and Sublime accept; the capture seam and the system-default
fallback still receive the bare path, and the in-cmux preview route already
opens the stripped path.

No user-facing strings added, so no localization changes.
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

@liflovs is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Trailing :line and :line:column locators are parsed during terminal path resolution, preserved when opening files in a preferred editor, and forwarded from cmd-click fallback handling. Tests cover parsing, candidate precedence, and editor command arguments.

Changes

Terminal locator forwarding

Layer / File(s) Summary
Locator parsing and path resolution
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/PathResolution/..., Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPathResolverTests.swift
Trailing line and optional column suffixes are parsed into TerminalFileLinePosition; stripped candidates are added while literal colon-number filenames retain precedence.
Editor locator arguments
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/FileOpen/PreferredEditorService.swift, Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/FileOpen/PreferredEditorServiceTests.swift
PreferredEditorService accepts optional line and column values and formats editor arguments as path:line[:column], with tests for both forms.
Cmd-click locator forwarding
Sources/GhosttyTerminalView.swift
Cmd-click fallback derives locator metadata from the raw token and passes it to PreferredEditorService.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GhosttyNSView
  participant TerminalFilePathLineLocator
  participant PreferredEditorService
  participant EditorCommand
  GhosttyNSView->>TerminalFilePathLineLocator: derive line and column from clicked token
  TerminalFilePathLineLocator-->>GhosttyNSView: return optional position
  GhosttyNSView->>PreferredEditorService: open resolved path with position
  PreferredEditorService->>EditorCommand: pass path:line[:column]
Loading

Possibly related PRs

  • manaflow-ai/cmux#8101: Updates related terminal locator parsing, path resolution, and editor-opening behavior.

Suggested reviewers: azooz2003-bit


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux No Ambient Global State ❌ Error The PR adds public enum TerminalFilePathLineLocator as a caseless static-only namespace in TerminalFileLinePosition.swift. Move the locator logic onto an owning instance type (e.g. TerminalPathResolver) or a private file-scope helper, and expose it there instead of a public static namespace.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed The new types are pure value utilities, and the added editor-opening path stays on @MainActor UI code; no new mutable Sendable refs or background UI access.
Cmux Swift Blocking Runtime ✅ Passed Changed code adds pure parsing and Process.terminationHandler; no new waits, sleeps, semaphores, main.sync, or locks were introduced in the diff.
Cmux Browser Automation Off-Main ✅ Passed PR only changes terminal path parsing/editor opening files; it doesn't touch browser socket routing, socketWorkerMethods, or processV2Command.
Cmux Expensive Synchronous Load ✅ Passed Diff adds path/locator parsing and editor launching only; no agent-history, transcript, JSONL, or other heavy synchronous loads were added to main-actor or interactive paths.
Cmux Cache Substitution Correctness ✅ Passed No fresh read was replaced by a cache; the PR adds path/locator parsing and editor forwarding in transient cmd-click flows, not persistence/history/undo/snapshot state.
Cmux No Hacky Sleeps ✅ Passed Diff only changes Swift source/tests; no TS/JS/shell/build-runtime files were added, so the runtime-no-hacky-sleeps rule doesn’t apply.
Cmux Algorithmic Complexity ✅ Passed PASS: The changes only add constant-bounded token parsing/candidate probing; no scalable collection scans, batch rescans, or repeated hot-path sorts/filters were introduced.
Cmux Swift Concurrency ✅ Passed No disallowed legacy async pattern was introduced; the only new Task is a main-actor hop from Process.terminationHandler, an allowed OS callback boundary.
Cmux Swift @Concurrent ✅ Passed Changed Swift code is synchronous/UI-bound or actor-isolated; no new nonisolated async work or invalid @concurrent usage appears in the diff.
Cmux Swift Package Boundaries ✅ Passed Reusable path/locator logic lives in CmuxTerminalCore/CmuxWorkspaces packages; the app-target edit is private Ghostty glue calling those APIs.
Cmux Swiftpm Lockfiles ✅ Passed PR changes only Swift source/tests; no Package.swift, Package.resolved, .gitignore, or Xcode project lockfile diffs appear.
Cmux Swift Logging ✅ Passed Patch adds no print/debugPrint/dump/NSLog/Logger in runtime code; only a #if DEBUG cmuxDebugLog probe, which the rule allows.
Cmux User-Facing Error Privacy ✅ Passed The changes only parse/forward file locators and add tests; no user-facing errors, alerts, command output, or recovery copy were added.
Cmux Full Internationalization ✅ Passed Logic-only path/editor changes; no new user-facing text, localized keys, or catalog/plist/web locale files were added or changed.
Cmux Swiftui State Layout ✅ Passed No new ObservableObject/@Published/GeometryReader state was added; changes are pure path parsing/editor forwarding plus an AppKit NSViewRepresentable helper.
Cmux Architecture Rethink ✅ Passed Adds pure locator parsing and one shared editor-open helper; no new locks, polling, observers, or split ownership beyond a required main-actor bridge.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes path-resolution/editor helper code and a terminal view helper; no standalone NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test Swift files; no logs, caches, build output, or scratch artifacts appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Changed production files add real path/locator APIs only; no new DEBUG/test-only seam or test-hook member was introduced in Sources/.
Title check ✅ Passed The title clearly states the main change: cmd-click links now open path:line[:col] targets at the right position.
Description check ✅ Passed The description covers Summary, Testing, Demo Video, Review Trigger, and Checklist, with concrete implementation and verification details.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@liflovs

liflovs commented Jul 15, 2026

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@liflovs I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,631 of the 240,000 allowed lines of code this month. Reviews resume on 1 August 2026 (in 17 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

@liflovs Sure, I'll review the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes cmd-click on path:line[:col] tokens (compiler output, grep -n, stack traces) that previously failed to linkify because the file-existence probe saw src/state.js:87 rather than src/state.js. Resolution now appends locator-stripped spellings as candidates after all literal forms, and the clicked token's trailing position is forwarded to the configured editor as path:line[:col].

  • String.pathResolutionCandidates() appends stripped candidates after every literal form, preserving precedence for files whose names genuinely end in :<number>.
  • TerminalFileLinePosition and splittingTrailingLineColumn() parse the :line[:col] suffix; PreferredEditorService.open(_:line:column:) appends it to the shell argument before single-quoting; the UI-test capture seam and system-default fallback keep the bare path.
  • TerminalFilePathLineLocator is introduced as a caseless enum to hold one static disambiguation function — this pattern conflicts with the no-ambient-global-state rule; the logic also has an untested edge case for column locators on colon-named files.

Confidence Score: 4/5

The core path-resolution and editor-open changes are correct and well-tested; the two findings are confined to a new helper type's shape and an untested disambiguation edge case involving files literally named with colon-number suffixes.

The candidate-stripping logic in pathResolutionCandidates() and the shell-argument construction in PreferredEditorService are sound. The main concerns are that TerminalFilePathLineLocator uses a caseless-enum namespace (against codebase conventions) and that position disambiguation produces a wrong line/column when a colon-named file like weird:87 is clicked as weird:87:12 — an edge case not covered by the test suite. Neither finding breaks the primary happy path or any existing behavior.

TerminalFileLinePosition.swift — both the namespace shape of TerminalFilePathLineLocator and the position comparison logic that can misclassify column locators on colon-named files.

Important Files Changed

Filename Overview
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/PathResolution/TerminalFileLinePosition.swift New file introducing TerminalFileLinePosition struct, splittingTrailingLineColumn() String extension, and TerminalFilePathLineLocator caseless enum (namespace violation). Position disambiguation logic has an edge case where rawToken="weird:87:12" resolving to "weird:87" produces wrong position {87,12} instead of {12,nil}.
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/PathResolution/String+TerminalPathTokens.swift Adds locator-stripped candidates after all literal candidates in pathResolutionCandidates(). Ordering correctly preserves literal-file-with-colon-number precedence. Logic is clean and well-motivated.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/FileOpen/PreferredEditorService.swift Adds open(:line:column:) overload that appends path:line[:col] before shell single-quoting for editor commands; capture seam and system-default fallback correctly keep the bare path. Clean delegation from the existing open(:) overload.
Sources/GhosttyTerminalView.swift Extracts openInPreferredEditor helper that reads TerminalFilePathLineLocator.position and forwards line/column to PreferredEditorService; both cmd-click call sites routed through the helper cleanly.
Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPathResolverTests.swift Good unit coverage for splittingTrailingLineColumn (positive/negative cases, zero rejection, empty-token guard) and the resolver integration (locator stripped, literal-file-wins precedence). Missing test for rawToken="weird:87:12" resolving to "weird:87" edge case.
Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/FileOpen/PreferredEditorServiceTests.swift Two new tests exercise line+column and line-only locator forwarding via a real shell script; polling loop (Task.sleep in a for loop) is acceptable test scaffolding. Bare-path backward-compatibility is asserted by the pre-existing tests.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User as User (cmd-click)
    participant GTV as GhosttyNSView
    participant TPR as TerminalPathResolver
    participant TFLL as TerminalFilePathLineLocator
    participant PES as PreferredEditorService

    User->>GTV: cmd-click on "src/state.js:87:12"
    GTV->>TPR: resolveQuicklookPath("src/state.js:87:12", cwd)
    Note over TPR: pathResolutionCandidates() tries<br/>"src/state.js:87:12" (miss),<br/>then stripped "src/state.js" (hit)
    TPR-->>GTV: "resolvedPath = "/repo/src/state.js""
    GTV->>TFLL: position(rawToken:"src/state.js:87:12", resolvedPath:"/repo/src/state.js")
    Note over TFLL: splittingTrailingLineColumn() →<br/>{line:87, column:12}
    TFLL-->>GTV: TerminalFileLinePosition(line:87, column:12)
    GTV->>PES: open(URL("/repo/src/state.js"), line:87, column:12)
    Note over PES: argument = "/repo/src/state.js:87:12"<br/>shell: sh -c "editor '/repo/src/state.js:87:12'"
    PES-->>User: Editor opens at line 87, col 12
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User as User (cmd-click)
    participant GTV as GhosttyNSView
    participant TPR as TerminalPathResolver
    participant TFLL as TerminalFilePathLineLocator
    participant PES as PreferredEditorService

    User->>GTV: cmd-click on "src/state.js:87:12"
    GTV->>TPR: resolveQuicklookPath("src/state.js:87:12", cwd)
    Note over TPR: pathResolutionCandidates() tries<br/>"src/state.js:87:12" (miss),<br/>then stripped "src/state.js" (hit)
    TPR-->>GTV: "resolvedPath = "/repo/src/state.js""
    GTV->>TFLL: position(rawToken:"src/state.js:87:12", resolvedPath:"/repo/src/state.js")
    Note over TFLL: splittingTrailingLineColumn() →<br/>{line:87, column:12}
    TFLL-->>GTV: TerminalFileLinePosition(line:87, column:12)
    GTV->>PES: open(URL("/repo/src/state.js"), line:87, column:12)
    Note over PES: argument = "/repo/src/state.js:87:12"<br/>shell: sh -c "editor '/repo/src/state.js:87:12'"
    PES-->>User: Editor opens at line 87, col 12
Loading

Reviews (1): Last reviewed commit: "Open path:line[:col] cmd-click links at ..." | Re-trigger Greptile

Comment on lines +55 to +72
public enum TerminalFilePathLineLocator {
/// The position to open `resolvedPath` at, given the `rawToken` the user
/// clicked.
///
/// Returns the token's trailing locator, unless the resolved file itself
/// carries the same numeric suffix (a real filename ending in `:line`), in
/// which case the digits are part of the name and no position is forwarded.
public static func position(
rawToken: String,
resolvedPath: String
) -> TerminalFileLinePosition? {
guard let (_, position) = rawToken.splittingTrailingLineColumn() else { return nil }
if let (_, resolvedPosition) = resolvedPath.splittingTrailingLineColumn(),
resolvedPosition == position {
return nil
}
return position
}

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.

P2 Caseless enum used as static-function namespace

TerminalFilePathLineLocator has no cases and a single static method, making it a pure namespace type. This pattern is explicitly prohibited by the cmux no-ambient-global-state rule, which flags caseless enums and empty structs used purely as static func/static let namespaces. The preferred shape is a static method on the type that owns the data — here TerminalFileLinePosition already exists and is the natural home for this factory logic (e.g., TerminalFileLinePosition.extracting(rawToken:resolvedPath:)).

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +62 to +70
public static func position(
rawToken: String,
resolvedPath: String
) -> TerminalFileLinePosition? {
guard let (_, position) = rawToken.splittingTrailingLineColumn() else { return nil }
if let (_, resolvedPosition) = resolvedPath.splittingTrailingLineColumn(),
resolvedPosition == position {
return nil
}

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.

P2 Position comparison misclassifies column locators on colon-named files

When a file is literally named weird:87 and the user clicks weird:87:12, the resolver correctly returns weird:87 as resolvedPath. splittingTrailingLineColumn on the rawToken weird:87:12 yields position {line: 87, column: 12}, while the resolved path yields {line: 87, column: nil}. Because these two positions differ, the guard falls through and the editor receives {line: 87, column: 12} — but the correct locator for that resolved file is {line: 12, column: nil}. The current comparison strategy cannot distinguish "the column extends a locator that was already the filename" from "column is a new locator on top of the stripped path".

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes cmd-click on terminal path tokens that carry a trailing line/column locator (e.g. src/state.js:87 or src/state.js:87:12), which previously failed to linkify because the resolver probed the literal string and found no such file. The fix is confined to the pure CmuxTerminalCore and CmuxWorkspaces packages, with a two-line change to the app-target glue.

  • pathResolutionCandidates() now appends a locator-stripped candidate after every literal candidate, preserving the literal-wins-first invariant for real filenames that end in :<number>.
  • TerminalFileLinePosition and TerminalFilePathLineLocator are introduced to parse and disambiguate the trailing locator; PreferredEditorService.open gains an (url:line:column:) overload that passes path:line[:col] to the configured editor command while keeping the bare path for the capture seam and system-open fallback.

Confidence Score: 4/5

The change is well-scoped to the pure path-resolution and editor-service layers, with solid unit coverage for the new parsing and disambiguation logic. The single structural concern is a new caseless enum used as a static-function namespace in production source.

The disambiguation guard, candidate-stripping loop, and path:line[:col] argument construction are all correct. The only flag is TerminalFilePathLineLocator being a caseless enum whose sole member is a static function — the method's natural home is a static extension on TerminalFileLinePosition, and the caseless-enum shape is the style the repo explicitly discourages.

Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/PathResolution/TerminalFileLinePosition.swift — the caseless enum namespace.

Important Files Changed

Filename Overview
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/PathResolution/TerminalFileLinePosition.swift New file introducing TerminalFileLinePosition struct and splittingTrailingLineColumn() parser — logic is correct, but TerminalFilePathLineLocator is a caseless enum namespace anti-pattern; the static method belongs on TerminalFileLinePosition.
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/PathResolution/String+TerminalPathTokens.swift Appends locator-stripped candidates after all literal candidates, correctly preserving literal-wins-first precedence; iteration is safe (Swift array copy-on-write) and duplicates are guarded.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/FileOpen/PreferredEditorService.swift Adds open(_ url:line:column:) overload; path:line:col forwarded only to the editor command (Int-typed, safe from injection); capture seam and system-open fallback keep bare path. Backward-compatible via existing open(_ url:) delegation.
Sources/GhosttyTerminalView.swift Refactors two call sites into openInPreferredEditor(_:) helper; correctly reads rawToken from WordPathResolution (pre-existing field) and delegates disambiguation to TerminalFilePathLineLocator.
Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPathResolverTests.swift Good coverage of splittingTrailingLineColumn (including zero-rejection, empty-token, and literal-wins cases) and resolver integration tests for :line and :line:col tokens.
Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/FileOpen/PreferredEditorServiceTests.swift Tests confirm path:line:col and path:line are forwarded to the editor script; capture/system-open still receive bare path. Test uses a polling Task.sleep loop (test-only scaffolding, passes the rule).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User as User (cmd-click)
    participant GTV as GhosttyNSView
    participant Resolver as TerminalPathResolver
    participant Locator as TerminalFilePathLineLocator
    participant Editor as PreferredEditorService

    User->>GTV: cmd-click "src/state.js:87:12"
    GTV->>Resolver: resolveQuicklookPath("src/state.js:87:12", cwd:)
    Note over Resolver: candidates: ["src/state.js:87:12", "src/state.js"] (stripped)
    Resolver-->>GTV: "resolvedPath = "/repo/src/state.js""
    GTV->>Locator: position(rawToken: "src/state.js:87:12", resolvedPath: "/repo/src/state.js")
    Note over Locator: resolvedPath has no :N suffix → forward position
    Locator-->>GTV: TerminalFileLinePosition(line: 87, column: 12)
    GTV->>Editor: open(url, line: 87, column: 12)
    Note over Editor: capture seam receives bare path
    Editor->>Editor: "argument = "/repo/src/state.js:87:12""
    Editor->>Editor: /bin/sh -c "code '/repo/src/state.js:87:12'"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User as User (cmd-click)
    participant GTV as GhosttyNSView
    participant Resolver as TerminalPathResolver
    participant Locator as TerminalFilePathLineLocator
    participant Editor as PreferredEditorService

    User->>GTV: cmd-click "src/state.js:87:12"
    GTV->>Resolver: resolveQuicklookPath("src/state.js:87:12", cwd:)
    Note over Resolver: candidates: ["src/state.js:87:12", "src/state.js"] (stripped)
    Resolver-->>GTV: "resolvedPath = "/repo/src/state.js""
    GTV->>Locator: position(rawToken: "src/state.js:87:12", resolvedPath: "/repo/src/state.js")
    Note over Locator: resolvedPath has no :N suffix → forward position
    Locator-->>GTV: TerminalFileLinePosition(line: 87, column: 12)
    GTV->>Editor: open(url, line: 87, column: 12)
    Note over Editor: capture seam receives bare path
    Editor->>Editor: "argument = "/repo/src/state.js:87:12""
    Editor->>Editor: /bin/sh -c "code '/repo/src/state.js:87:12'"
Loading

Reviews (2): Last reviewed commit: "Open path:line[:col] cmd-click links at ..." | Re-trigger Greptile

Comment on lines +55 to +73
public enum TerminalFilePathLineLocator {
/// The position to open `resolvedPath` at, given the `rawToken` the user
/// clicked.
///
/// Returns the token's trailing locator, unless the resolved file itself
/// carries the same numeric suffix (a real filename ending in `:line`), in
/// which case the digits are part of the name and no position is forwarded.
public static func position(
rawToken: String,
resolvedPath: String
) -> TerminalFileLinePosition? {
guard let (_, position) = rawToken.splittingTrailingLineColumn() else { return nil }
if let (_, resolvedPosition) = resolvedPath.splittingTrailingLineColumn(),
resolvedPosition == position {
return nil
}
return position
}
}

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.

P2 Caseless enum used as a static-function namespace

TerminalFilePathLineLocator is a caseless public enum whose entire API is a single static func. This is exactly the ambient-global-state anti-pattern flagged in the repo's review rules: a caseless enum acting as a namespace for a static function. The method's natural home is a static func on TerminalFileLinePosition itself (the type it produces), or — because the function is only used in GhosttyTerminalView — a fileprivate free function or a private/internal static extension kept close to its call site. A caseless enum that is never constructed and holds no data carries no semantic value over a properly owned method.

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes cmd-click on compiler-style path:line and path:line:col tokens in the terminal so the editor opens at the correct position. Previously these tokens didn't linkify because the literal path:87 string is not a real file; now TerminalPathResolver probes a locator-stripped candidate after all literal candidates, and the parsed position is forwarded to the preferred editor as a path:line[:col] argument.

  • String+TerminalPathTokens.swift appends one stripped candidate per existing candidate, preserving real filenames that end in :<number> by trying literals first.
  • TerminalFileLinePosition.swift adds splittingTrailingLineColumn(), TerminalFileLinePosition, and TerminalFilePathLineLocator for position parsing and disambiguation.
  • PreferredEditorService gains an open(_:line:column:) overload that appends :line[:col] to the editor command argument while keeping the capture seam and system-open fallback on the bare path; GhosttyTerminalView is updated at both cmd-click call sites.

Confidence Score: 4/5

Safe to merge; the fix is well-contained in the pure path-resolution and editor-service layers, and all new logic is exercised by unit tests including a red/green regression pair.

The implementation is correct and the test coverage is thorough. The one flagged issue is TerminalFilePathLineLocator being shaped as a caseless enum namespace rather than the idiomatic free-function or String-extension form the codebase's style rules prescribe — it is a style concern, not a correctness defect.

TerminalFileLinePosition.swift — the TerminalFilePathLineLocator enum declaration should be reviewed against the no-ambient-global-state rule before merging.

Important Files Changed

Filename Overview
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/PathResolution/TerminalFileLinePosition.swift New file: defines TerminalFileLinePosition struct and splittingTrailingLineColumn() String extension, plus TerminalFilePathLineLocator caseless-enum namespace — the last pattern is flagged by the no-ambient-global-state rule
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/PathResolution/String+TerminalPathTokens.swift Appends locator-stripped candidates after all literal candidates so path:line tokens resolve; deduplication in append() is correct and loop snapshot semantics are safe
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/FileOpen/PreferredEditorService.swift Adds open(_:line:column:) overload that appends path:line[:col] to the editor command argument; capture seam and system-open fallback correctly keep the bare path
Sources/GhosttyTerminalView.swift Extracts openInPreferredEditor helper that calls TerminalFilePathLineLocator.position and forwards line/column to PreferredEditorService; two existing call sites updated consistently
Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPathResolverTests.swift New test suites cover locator splitting, literal-file-with-colon precedence, and resolver resolution for :line and :line:col tokens; good red/green regression coverage
Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/FileOpen/PreferredEditorServiceTests.swift Two new tests verify the editor command receives path:line:col and path:line; polling pattern with Task.sleep is consistent with pre-existing test scaffolding in the file

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User as User (cmd-click)
    participant GTV as GhosttyNSView
    participant Resolver as TerminalPathResolver
    participant Locator as TerminalFilePathLineLocator
    participant Editor as PreferredEditorService

    User->>GTV: cmd-click on "src/state.js:87:12"
    GTV->>Resolver: resolveQuicklookPath("src/state.js:87:12", cwd)
    Note over Resolver: candidates: ["src/state.js:87:12", "src/state.js"] (stripped added)
    Resolver-->>GTV: WordPathResolution(path: "/repo/src/state.js", rawToken: "src/state.js:87:12")
    GTV->>Locator: position(rawToken: "src/state.js:87:12", resolvedPath: "/repo/src/state.js")
    Note over Locator: splittingTrailingLineColumn → {line:87, col:12}
    Note over Locator: resolvedPath has no numeric suffix → forward position
    Locator-->>GTV: TerminalFileLinePosition(line: 87, column: 12)
    GTV->>Editor: open(URL("/repo/src/state.js"), line: 87, column: 12)
    Note over Editor: argument = "/repo/src/state.js:87:12"
    Editor->>Editor: /bin/sh -c "editor '/repo/src/state.js:87:12'"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User as User (cmd-click)
    participant GTV as GhosttyNSView
    participant Resolver as TerminalPathResolver
    participant Locator as TerminalFilePathLineLocator
    participant Editor as PreferredEditorService

    User->>GTV: cmd-click on "src/state.js:87:12"
    GTV->>Resolver: resolveQuicklookPath("src/state.js:87:12", cwd)
    Note over Resolver: candidates: ["src/state.js:87:12", "src/state.js"] (stripped added)
    Resolver-->>GTV: WordPathResolution(path: "/repo/src/state.js", rawToken: "src/state.js:87:12")
    GTV->>Locator: position(rawToken: "src/state.js:87:12", resolvedPath: "/repo/src/state.js")
    Note over Locator: splittingTrailingLineColumn → {line:87, col:12}
    Note over Locator: resolvedPath has no numeric suffix → forward position
    Locator-->>GTV: TerminalFileLinePosition(line: 87, column: 12)
    GTV->>Editor: open(URL("/repo/src/state.js"), line: 87, column: 12)
    Note over Editor: argument = "/repo/src/state.js:87:12"
    Editor->>Editor: /bin/sh -c "editor '/repo/src/state.js:87:12'"
Loading

Reviews (3): Last reviewed commit: "Open path:line[:col] cmd-click links at ..." | Re-trigger Greptile

Comment on lines +55 to +73
public enum TerminalFilePathLineLocator {
/// The position to open `resolvedPath` at, given the `rawToken` the user
/// clicked.
///
/// Returns the token's trailing locator, unless the resolved file itself
/// carries the same numeric suffix (a real filename ending in `:line`), in
/// which case the digits are part of the name and no position is forwarded.
public static func position(
rawToken: String,
resolvedPath: String
) -> TerminalFileLinePosition? {
guard let (_, position) = rawToken.splittingTrailingLineColumn() else { return nil }
if let (_, resolvedPosition) = resolvedPath.splittingTrailingLineColumn(),
resolvedPosition == position {
return nil
}
return position
}
}

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.

P2 Caseless enum used as static-func namespace

TerminalFilePathLineLocator has no cases and exposes a single public static func. The cmux-no-ambient-global-state rule flags this pattern — a caseless enum or empty struct used purely as a static func/static let namespace should instead be a method on the type that owns the data or a nonisolated free function. Here the computation is a pure function of its two String inputs, so a free function (or an extension String helper alongside splittingTrailingLineColumn) would be the idiomatic fit without introducing a namespace type at all.

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

This branch has not been deployed

No deployments
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.

2 participants