Skip to content

v3.0.0 — major merge: deep links, sibling commands, autoactions, defaults overhaul#79

Open
noahcoad wants to merge 24 commits into
masterfrom
major-merge
Open

v3.0.0 — major merge: deep links, sibling commands, autoactions, defaults overhaul#79
noahcoad wants to merge 24 commits into
masterfrom
major-merge

Conversation

@noahcoad

@noahcoad noahcoad commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Major merge that unifies the long-divergent select-url-old feature set into master while preserving v1 architecture and config style. Replaces #78 (closed when the branch was renamed from merge-v2-into-v1 to major-merge).

What lands

  • Deep linkspath:LINE, path:START-END, path:"text", path:/regex/, plus combined path:LINE:/regex/ and path:LINE:"text" for unambiguous navigation.
  • New commands — Select URL, Copy Deep Link, Copy Transformed Path, Paste Relative Path, all gated by disable_default_key_bindings.
  • autoactions — per-extension auto-pick of *_custom_commands entries.
  • Sentinel commandsedit_in_sublime, open_in_new_window, system_open, add_to_project dispatched in-process; open_in_new_window uses the bundled macOS subl so project-load events fire reliably.
  • Per-opener fieldsterminal, pause, pre_command.
  • File-URI handlingfile:// URIs (incl. file:///, file://localhost/, percent-encoded) recognized as local paths.
  • Multi-line selection — single selection spanning multiple non-empty lines opens each as a separate URL.
  • Toolchain — Black removed (couldn't enforce tabs); flake8/isort/pyright + the test suite gate pre-push.
  • Tabs over spaces in all Python files (matches v2 style).
  • Docstrings on every def in production code; humanized docstrings on every test helper.
  • Defaults overhaul — fresh installs ship richer file/folder menus and sensible autoactions for .txt/.md/.exe/etc.

Compatibility

No setting key is removed. User-folder open_url.sublime-settings and project-level ["settings"]["open_url"] continue to win over defaults via merge_settings (regression covered by TestUserOverrideReplacesDefaults).

Tests

174/174 pass via python3 test_open_url.py.

🤖 Generated with Claude Code

noahcoad added 24 commits June 4, 2026 22:28
Adds test_open_url.py with mocked sublime/sublime_plugin so tests run in
plain Python. Imports open_url as a package to support its 'from .url'
relative import.

Phase 0 ships v1-surface coverage immediately:
- TestMergeSettings: User defaults + project override + missing data
- TestMatchOpeners: pattern AND os filter combinations
- TestGenerateUrls: search_paths × prefixes × suffixes combinatorics
- TestPrepareArgs: cwd substitution + commands string/array branches
- TestRemoveTrailingDelimiters, TestResolveAliases, TestPrependScheme

v2-symbol test classes (TestFindLocSep, TestParseFileLocation,
TestFindSelection, etc.) are skip-decorated until Phase 1+ lands the
helpers they reference.

test_cases.yaml ported verbatim from v2; consumed by table-driven tests
once Phase 1 un-skips them.

pre-push hook now runs the test suite. .gitignore picks up
__pycache__/ and .pytest_cache/.
…start scan

Adds module-level helpers strip_file_scheme, find_loc_sep, parse_file_location.
Threads deep-link locations (file:42, file:"text", file:/regex/) through
file_action -> file_done -> open_file_at_location, which uses
sublime.ENCODED_POSITION for line numbers and registers a load callback for
search/regex navigation.

OpenUrlCommand.run gains:
- multi-line region handling (single non-empty selection across multiple
  non-empty lines opens each line as a separate URL)
- line-start scan heuristic (cursor at column 0 with non-resolvable text
  scans rightward via _scan_line_for_url for the first resolvable token)
- per-url strip_file_scheme + os.path.expandvars before handle()

OpenUrlCommand.handle now also retries each candidate url as path:location
when the bare resolution misses, so 'foo.py:42' resolves to foo.py and
opens at line 42.

Behavior unification: show_menu=False on a path:location now navigates to
the location instead of just opening the file at the top.

New setting: deep_link_line_number_only (default false) restricts deep
links to numeric line markers, dropping :"text" and :/regex/ forms.

Tests: 87/87 pass. New classes TestFindLocSep (17), TestParseFileLocation
(13), TestStripFileScheme (9), TestIsResolvable (13), TestScanLineForUrl (8)
plus YAML-driven cases. TestFindSelection / TestSelectionMethod stay
skipped until Phase 2.
Adds four new TextCommand classes plus shared helpers:
- find_selection() / selection() on OpenUrlCommand: smarter expansion that
  handles enclosing quotes/backticks and deep-link tokens
  (path:42, path:"text", path:/regex/), distinct from get_selection's
  simpler delimiter-based expansion which still feeds the resolution path.
- apply_path_transform() module helper: shells out a {path} template,
  returning (new_path, error) — used by both Copy commands.

SelectUrlCommand expands the cursor to a URL/path region and copies it.
CopyDeepLinkCommand emits file:line / file:"text" / file:/regex/ depending
on selection state and current line content; honors copy_path_transform
and deep_link_line_number_only.
CopyTransformedPathCommand pipes the file path through the same transform
and only is_visible() in the palette when configured.
PasteRelativePathCommand picks the shortest of relative / tilde / absolute
forms, resolves symlinks on both sides, preserves any deep-link suffix on
clipboard text, and auto-wraps in backticks for markdown views.

Palette gains four "Open URL: ..." entries. Default keymaps gain four
new bindings on each platform, all gated by the existing
disable_default_key_bindings setting:
  macOS:  ctrl+shift+u, ctrl+alt+shift+u, ctrl+alt+v, ctrl+alt+shift+c
  Lin/Win: ctrl+alt+shift+u, ctrl+alt+shift+d, ctrl+alt+v, ctrl+alt+shift+c
The ctrl+alt+shift+c choice resolves the v1 ctrl+alt+u collision by giving
copy_transformed_path its own home rather than overloading the open_url key.

Settings additions: copy_path_transform (string, default ""),
paste_relative_path_markdown_backticks (bool, default true).

Tests: 121/121 pass. New classes TestFindSelection (22), TestSelectionMethod (2),
TestApplyPathTransform (3), TestCopyTransformedPathVisibility (2),
TestCopyDeepLinkBuildsLink (4).
Adds three additive opener-schema extensions in v1's *_custom_commands lists:
  terminal: bool      — wrap the command in xterm/cmd.exe
  pause: bool         — read -p prompt after exit (paired with terminal)
  pre_command: str    — prepended to commands (replaces v2's openwith/app)

Adds new top-level autoactions setting: list of regex/extension matchers
that pre-select an opener label and either auto-invoke it or pre-highlight
it in the menu. Schema:
  { os?, pattern? | endswith?, label, action: 'auto'|'menu' }

Reserves four sentinel command names dispatched in-process instead of
spawning a subprocess (BUILTIN_COMMANDS frozenset):
  edit_in_sublime    — open via Sublime (with deep-link if present)
  open_in_new_window — sublime.executable_path -n <path>
  system_open        — open / xdg-open / cmd start
  add_to_project     — append folder to window.project_data()
A user opener of { 'commands': 'add_to_project' } now works directly.

Wires select_default_opener() into file_action and folder_action: when
autoactions match, the opener is invoked immediately (action=auto) or the
quick panel opens with that opener pre-highlighted (action=menu). The
file menu's pseudo 'edit' entry is index 0; user openers shift to 1..n.

Existing user configs with no new fields and empty autoactions retain
identical behavior (regression covered by
test_existing_opener_without_new_fields_unchanged).

Tests: 138/138 pass. New classes TestSelectDefaultOpener (8) and
TestPrepareArgsBuiltinAndExtras (9).
Adds browser_search setting (string with {query} placeholder). Used by
modify_or_search_action with this precedence:

  - web_searchers empty AND browser_search set
      -> open browser_search directly, no panel
  - web_searchers non-empty
      -> show modify-or-search panel; if browser_search is also set, it
         appears as the last entry labeled "search ({term})"

Existing v1 default has web_searchers populated and browser_search empty,
so existing users see no behavior change.

New helper _resolve_browser_search(term, template, encoding) handles
{query} substitution with URL-encoded text.

Tests: 144/144 pass. New classes TestResolveBrowserSearch (2),
TestModifyOrSearchAction (4).
Rewrites the shipped open_url.sublime-settings so a fresh install behaves
like the v2 fork: rich file/folder menus driven by sentinel commands and
sensible autoactions for common file extensions.

Default file_custom_commands now: edit, run, reveal, open in new window,
system open. Default folder_custom_commands: open in new window, reveal,
add to project. Default autoactions auto-edit .txt/.md/.log/.config/
.sublime-settings/.sublime-project, auto-run Windows .exe/.com, and open
the menu pre-highlighting 'run' for Windows .bat/.cmd. Default
browser_search points at Google.

Every existing setting key is preserved with a value (no removals).
User-folder and project-level settings continue to win over these
defaults via merge_settings, so existing customizations are unaffected
(regression covered by TestUserOverrideReplacesDefaults).

Adds messages/3.0.0.md describing the migration and how to revert
defaults; registered in messages.json. New version: 3.0.0.

Tests: 156/156 pass. New classes TestDefaultSettings (8),
TestDefaultAutoactionsBehavior (3), TestUserOverrideReplacesDefaults (1).
Test harness now imports 're' at module scope to support the JSONC
parser used to validate the shipped settings file.
…lette entry

Settings (and key bindings) are now reachable via the standard Sublime
Preferences menu instead of via the command palette.
Open URL (Skip Menu) -> Open URL: Skip Menu
Open URL (Use Input) -> Open URL: Use Input
The previous implementation called the GUI binary at executable_path()
with -n. On macOS this works visually but doesn't fully load the folder
through Sublime's project machinery, so on_load_project_async listeners
(e.g. AutoOpenNotes) fire inconsistently or not at all.

Ports v2's mechanism: rewrite executable_path() to the bundled
.app/Contents/SharedSupport/bin/subl. This dispatches through the
running ST instance with full project event ordering, so listeners see
folders set before they inspect the window. Avoids both:
  - GUI-binary inconsistency
  - shelling out to PATH 'subl' (could be a different ST build)

For files, also passes the parent dir as a project root so the file
opens as a tab in a window with that folder loaded.

Drops -n; relies on subl's default (new window when not focused on
that folder) which matches v2's user-tested behavior.
file_action() prepends a synthetic 'edit' entry to whatever
file_custom_commands defines, so including an explicit 'edit' in defaults
made it appear twice in the quick panel.

The synthetic entry calls open_file_at_location which honors deep-link
suffixes, matching the behavior the explicit entry's edit_in_sublime
sentinel provided.
Previously the cursor-expansion path called get_selection, which uses
the user-configurable 'delimiters' setting to find token boundaries.
Default delimiters include '*' '<' '>' ',' which are valid characters
inside deep-link regex/search suffixes like /file.txt:/^\\s*http/.
get_selection terminated at the first '*' so the deep-link tokens were
truncated and never resolved.

find_selection (introduced in Phase 2 for SelectUrlCommand) is the
deep-link-aware expansion: hardcoded terminator set, recognizes
:42 / :\"text\" / :/regex/ tokens, handles enclosing quotes. v2 used
this expansion universally; we now do the same.

Also strip enclosing quotes/backticks and unescape '\\ ' before handing
off to handle(), matching v2 (so 'hello\\ world.txt' resolves correctly).

Adds regression to TestFindSelection plus 5 YAML cases covering regex
suffixes containing *, <, >, comma.
Copy Deep Link now emits 'path:LINE:/regex/' or 'path:LINE:"text"' so the
line number anchors the location even when the regex/search pattern is
ambiguous. Navigation behavior:

- legacy ':42'        -> jump to line 42 via ENCODED_POSITION (unchanged)
- legacy ':/regex/'   -> first regex match in the file (unchanged)
- legacy ':"text"'    -> first case-insensitive match (unchanged)
- new    ':N:/regex/' -> regex match closest to line N; if no match,
                          fall back to line N
- new    ':N:"text"'  -> search match closest to line N; same fallback

Implementation:
- New module helper split_path_and_loc_suffix peels the combined form for
  paste-relative-path's opaque-suffix use case.
- parse_file_location returns 'line: int|None' on regex/search dicts.
- _navigate_in_view collects all matches, picks nearest-by-row when a
  hint is present, falls back to the line itself when nothing matches.
- find_loc_sep / split_path_and_loc_suffix detect the combined form by
  recognizing path[...:N] before the outer suffix.

Tests: 170/170 pass.  Added TestNavigateInView (3 cases), 5 new
TestParseFileLocation cases covering each form, plus updated
TestCopyDeepLinkBuildsLink for the new emit format and a new test
asserting the legacy line-number-only emit path is unchanged.
…forms

Adds a new location type:
  path:123-320  -> select all text from line 123 through line 320 inclusive

parse_file_location returns {type: 'range', start, end} for this form.
open_file_at_location opens the file via ENCODED_POSITION (landing at the
first line) then expands the selection to span the full range once the
view finishes loading.

README gets a new 'Deep Links' section laying out all six forms with a
table of examples, plus what Copy Deep Link emits and how Paste Relative
Path preserves suffixes.

Tests: 175/175 pass. Added 4 TestParseFileLocation cases covering simple
ranges, degenerate single-line ranges, absolute paths, and the
non-matching 'foo:8080-bar' case. New TestNavigateInView case verifies
the actual selected region spans the requested rows.
…ns, transforms

Adds first-class coverage for everything that landed in the v2->v1 merge:
- Commands table with platform-specific keybindings for all five commands
- Per-command sections (Skip Menu, Use Input, Copy Deep Link, Copy
  Transformed Path, Paste Relative Path)
- Custom command schema with the new opener fields (terminal, pause,
  pre_command) and the four built-in sentinels
- autoactions section with shipped defaults
- browser_search alongside web_searchers
- Settings reference table covering every key in the shipped settings
- Project-specific settings still documented; project replaces (no merge)
- Disable default key bindings note covers all five bindings now

The original Deep Links section is kept; it already matched v3 behavior.
Black hardcodes 4-space indentation with no escape hatch, so we drop it.
The remaining toolchain (flake8 + isort + pyright + the test suite) is
enough to keep the codebase consistent without bikeshedding indentation.

Configuration changes:
- pre-push: remove the 'black --check .' line
- .flake8: ignore W191 (tab indentation), E101 (mixed leading whitespace),
  and E128 (continuation under-indent — meaningless with tabs)
- pyproject.toml: drop [tool.black]; add [tool.isort] indent = '\t' so
  isort rewrites import blocks with tabs when needed
- pyrightconfig.json: untouched (pyright is whitespace-agnostic)

Code changes:
- open_url.py, url.py, test_open_url.py: leading groups of 4 spaces are
  now tabs.  No content changes; the diff is whitespace-only.
- 175/175 tests still pass.
open_url.py and url.py: 31 hand-written docstrings covering helpers,
TextCommand entry points, and internal methods. Each focuses on intent
or non-obvious behavior; nothing redundant with the function name alone.

test_open_url.py: 224 single-line docstrings.  test_* methods get a
humanized version of the function name.  Helpers (setUp, _make_cmd,
fake_run, MockView methods, etc.) get a small lookup-table description.

Tests: 175/175 still pass.
…v1 behavior)

Phase 4 introduced browser_search as a parallel search mechanism alongside
v1's web_searchers. With both shipped non-empty by default, the panel
showed three entries — modify path, google search, search — the latter
two both pointing at Google.

Reverts to v1's panel-only behavior:

- Drop the browser_search setting from the schema and the shipped
  defaults.
- Restore modify_or_search_action / modify_or_search_done to v1's
  simpler signatures: panel always shows, populated from web_searchers.
- Default web_searchers continues to ship one Google entry. Users can
  empty the list (no engines, only modify-path), add more entries, or
  remove the default — config remains the single lever.

Tests: 173/173 pass (was 175; removed 2 browser_search-specific tests,
added regression test asserting browser_search is gone from defaults
and the v1 panel shape is preserved).

README and messages/3.0.0.md updated to drop browser_search references.
The modify-path entry now matches the visual style of web_searchers
('label (term)') and appears at the bottom of the panel where
'last-resort' actions belong, instead of stealing the default
quick-panel selection.
Shorter label fits better in the action menu and matches the visual
weight of neighbors like 'reveal' and 'edit'. Also updated docs and
the matching test assertions.
'run' and 'system open' were both wired to the same sentinel
('system_open') so they did literally identical things on every
platform. Removed 'run'. The shipped autoactions for .exe/.com/.bat/
.cmd on Windows now reference 'system open' instead.
These markers were useful during the merge but became noise once it
landed.  Replaces them with descriptive section headers and renames
test_*_match_v2_feel methods to plain test_default_*_menu_labels.
The Open URL command also responds to alt+double-click via the bundled
Default.sublime-mousemap; surfacing it in the keybindings table.
Quick orientation header listing the registered Sublime commands, key
module entry points, the settings file location, and the reserved
sentinel command names.
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.

1 participant