Skip to content

Commit 5effd38

Browse files
authored
Merge pull request #132 from FluxxField/dev
feat: patterns collector and filetype dispatch middleware
2 parents d93de77 + 1ccd9d0 commit 5effd38

13 files changed

Lines changed: 1054 additions & 0 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ Here's a taste of what you can change with a single override:
354354
| Delete without moving cursor | action | `action = "remote_delete"` |
355355
| Make any motion cross-window | metadata | `multi_window = true` |
356356
| Auto-jump to closest target | filter | `filter = "first_target"` |
357+
| Match by vim regex instead of TS | collector | `collector = "patterns"`, `patterns = { "\\v\\f+" }` |
358+
| Adapt per filetype | metadata | `filetype_overrides = { gitcommit = { ... } }` |
357359
| Customize labels for a motion | keys | `keys = "fdsarewq"` |
358360
| Exclude keys from labels | exclude_keys | `exclude_keys = "jk"` |
359361

docs/Advanced-Recipes.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,150 @@ action = merge({ "jump", "yank", "center" })
429429

430430
---
431431

432+
## Filetype-Aware Motions
433+
434+
The `filetype_overrides` system lets a single motion adapt its pipeline per filetype. Combined with the `patterns` collector, you can target structured text in any buffer — even without a treesitter parser.
435+
436+
### Remote Yank Across Filetypes
437+
438+
One keymap that yanks the "interesting thing" in any buffer:
439+
440+
```lua
441+
require("smart-motion").register_motion("ry", {
442+
collector = "treesitter",
443+
extractor = "pass_through",
444+
filter = "filter_visible",
445+
visualizer = "hint_before",
446+
modifier = "weight_distance",
447+
action = "remote_yank",
448+
map = true,
449+
trigger_key = "ry",
450+
modes = { "n" },
451+
metadata = {
452+
label = "Remote Yank",
453+
motion_state = {
454+
ts_node_types = { "identifier", "string_content", "constant" },
455+
multi_window = true,
456+
filetype_overrides = {
457+
gitcommit = {
458+
collector = "patterns",
459+
motion_state = {
460+
patterns = {
461+
"\\vmodified:\\s+\\zs\\S.*$",
462+
"\\vnew file:\\s+\\zs\\S.*$",
463+
"\\vdeleted:\\s+\\zs\\S.*$",
464+
},
465+
},
466+
},
467+
},
468+
},
469+
},
470+
})
471+
```
472+
473+
**How it works:** In code files, this yanks treesitter identifiers and strings remotely (without moving the cursor). In a fugitive `gitcommit` buffer, it switches to the `patterns` collector and targets modified file paths instead. One keymap, two completely different data sources, same action.
474+
475+
### Jump to Log File Entries
476+
477+
```lua
478+
require("smart-motion").register_motion("<leader>l", {
479+
collector = "treesitter",
480+
extractor = "pass_through",
481+
filter = "filter_visible",
482+
visualizer = "hint_start",
483+
action = "jump_centered",
484+
map = true,
485+
modes = { "n" },
486+
metadata = {
487+
motion_state = {
488+
ts_node_types = { "function_declaration", "function_definition" },
489+
filetype_overrides = {
490+
log = {
491+
collector = "patterns",
492+
motion_state = {
493+
patterns = {
494+
"\\v(ERROR|WARN|FATAL)",
495+
"\\v\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
496+
},
497+
},
498+
},
499+
},
500+
},
501+
},
502+
})
503+
```
504+
505+
**How it works:** In code, this jumps to functions. In `.log` files, it jumps to error keywords and ISO timestamps. The `patterns` collector handles the regex matching; the rest of the pipeline (filter, visualizer, action) stays the same.
506+
507+
### Per-Language Treesitter Customization
508+
509+
You don't need the `patterns` collector to use filetype overrides. You can swap treesitter config per language:
510+
511+
```lua
512+
require("smart-motion").register_motion("]]", {
513+
collector = "treesitter",
514+
extractor = "pass_through",
515+
filter = "filter_visible",
516+
visualizer = "hint_start",
517+
action = "jump_centered",
518+
map = true,
519+
modes = { "n", "o" },
520+
metadata = {
521+
motion_state = {
522+
ts_node_types = { "function_declaration", "function_definition" },
523+
filetype_overrides = {
524+
python = {
525+
motion_state = {
526+
ts_node_types = { "function_definition", "class_definition", "decorated_definition" },
527+
},
528+
},
529+
go = {
530+
motion_state = {
531+
ts_node_types = { "function_declaration", "method_declaration", "type_declaration" },
532+
},
533+
},
534+
sql = {
535+
motion_state = {
536+
ts_query = [[
537+
(function_definition fnc_name: (identifier) @fn)
538+
(select_list_element (identifier) @alias)
539+
]],
540+
},
541+
},
542+
},
543+
},
544+
},
545+
})
546+
```
547+
548+
**How it works:** Same treesitter collector everywhere, but each language gets the node types that make sense for it. Python includes `class_definition` and `decorated_definition`. Go includes `type_declaration`. SQL uses a raw query instead of node types. Languages without an override use the default `ts_node_types`.
549+
550+
### Standalone Patterns Motion
551+
552+
The `patterns` collector works independently — no filetype dispatch needed:
553+
554+
```lua
555+
-- Jump to URLs anywhere in the buffer
556+
require("smart-motion").register_motion("<leader>u", {
557+
collector = "patterns",
558+
extractor = "pass_through",
559+
filter = "filter_visible",
560+
visualizer = "hint_start",
561+
action = "jump",
562+
map = true,
563+
modes = { "n" },
564+
metadata = {
565+
motion_state = {
566+
patterns = { "\\vhttps?://\\S+" },
567+
},
568+
},
569+
})
570+
```
571+
572+
**How it works:** The `patterns` collector matches `https://...` URLs across visible buffer lines. No treesitter, no filetype dispatch — just a regex collector with standard pipeline stages.
573+
574+
---
575+
432576
## Next Steps
433577

434578
-> **[Recipes](Recipes.md)**: Pipeline basics, filter swaps, and preset overrides

docs/Building-Custom-Motions.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Each stage is optional (except collector, extractor, visualizer, action). Each s
5858
|-----------|------------------|
5959
| `lines` | All lines in the buffer |
6060
| `treesitter` | Treesitter syntax nodes |
61+
| `patterns` | Vim regex matches against buffer lines |
6162
| `diagnostics` | LSP diagnostics |
6263
| `git_hunks` | Git changed regions |
6364
| `quickfix` | Quickfix/location list entries |
@@ -637,6 +638,98 @@ Now `ghw`, `ghj`, `ghs` etc. all work automatically.
637638

638639
---
639640

641+
## Filetype-Aware Motions
642+
643+
A single motion can adapt its entire pipeline based on the current buffer's filetype. This is useful when:
644+
645+
- A filetype has no treesitter parser (e.g., fugitive's `gitcommit`)
646+
- A language needs a custom treesitter query (e.g., SQL with non-standard node types)
647+
- You want one keymap that behaves differently in different languages
648+
649+
### How It Works
650+
651+
Add `filetype_overrides` to your motion's `motion_state`. Each key is a filetype, and the value can override any pipeline module or motion_state field:
652+
653+
```lua
654+
require("smart-motion").register_motion("]]", {
655+
collector = "treesitter", -- default: use treesitter
656+
extractor = "pass_through",
657+
filter = "filter_visible",
658+
visualizer = "hint_start",
659+
action = "jump_centered",
660+
map = true,
661+
modes = { "n" },
662+
metadata = {
663+
motion_state = {
664+
ts_node_types = { "function_declaration", "function_definition" },
665+
filetype_overrides = {
666+
gitcommit = { -- no TS parser → use patterns
667+
collector = "patterns",
668+
motion_state = {
669+
patterns = {
670+
"\\vmodified:\\s+\\zs\\S.*$",
671+
"\\vnew file:\\s+\\zs\\S.*$",
672+
},
673+
},
674+
},
675+
sql = { -- has TS but needs custom query
676+
motion_state = {
677+
ts_query = [[(function_definition name: (identifier) @fn)]],
678+
},
679+
},
680+
},
681+
},
682+
},
683+
})
684+
```
685+
686+
**What happens:**
687+
- In a Lua/Python/JS file → treesitter collector, matches `function_declaration`/`function_definition`
688+
- In a `gitcommit` buffer → patterns collector, matches modified file paths
689+
- In a SQL file → treesitter collector, but with a custom query instead of node types
690+
691+
### What You Can Override
692+
693+
An override can swap **any** pipeline module:
694+
695+
| Field | What it swaps |
696+
|-------|---------------|
697+
| `collector` | Data source (e.g., `"treesitter"``"patterns"`) |
698+
| `extractor` | Target extraction (e.g., `"words"``"pass_through"`) |
699+
| `filter` | Target filtering |
700+
| `modifier` | Target enrichment |
701+
| `visualizer` | Hint display |
702+
| `action` | What happens on selection |
703+
| `motion_state` | Deep-merged into motion_state (patterns, ts_query, etc.) |
704+
705+
### Using the Patterns Collector
706+
707+
The `patterns` collector finds targets using vim regex. It reads two fields from `motion_state`:
708+
709+
- **`patterns`** — Array of vim regex strings (required)
710+
- **`patterns_whole_line`** — If `true`, the entire matching line is the target (default `false`)
711+
712+
```lua
713+
-- Jump to file paths
714+
motion_state = {
715+
patterns = { "\\v\\f+" },
716+
}
717+
718+
-- Jump to lines containing TODO (whole-line targets)
719+
motion_state = {
720+
patterns = { "TODO" },
721+
patterns_whole_line = true,
722+
}
723+
```
724+
725+
Use `pass_through` as the extractor since the patterns collector already produces complete targets. All standard filters, visualizers, and actions work with pattern targets.
726+
727+
### Operator Composition
728+
729+
Filetype dispatch runs before the infer system. If you have a composable operator like `d` and a filetype-overridden motion like `]]`, pressing `d]]` in a gitcommit buffer automatically uses the pattern-based pipeline. No extra configuration needed.
730+
731+
---
732+
640733
## Tips
641734

642735
1. **Start simple.** Get a basic motion working, then add complexity.

docs/Pipeline-Architecture.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,57 @@ The coroutine design enables:
5959
|-----------|----------------|
6060
| `lines` | Each line in the buffer with `text` and `line_number` |
6161
| `treesitter` | Syntax nodes matching configured types |
62+
| `patterns` | Targets matching vim regex patterns against buffer lines |
6263
| `diagnostics` | LSP diagnostic entries |
6364
| `git_hunks` | Git changed regions (via gitsigns or git diff) |
6465
| `quickfix` | Quickfix or location list entries |
6566
| `marks` | Vim marks (a-z local, A-Z global) |
6667
| `history` | Previous SmartMotion jump targets |
6768

69+
### Patterns Collector
70+
71+
The `patterns` collector finds targets using vim regex. It reads from `motion_state`:
72+
73+
- **`patterns`** (string[]) — Array of vim regex strings to match
74+
- **`patterns_whole_line`** (boolean) — If `true`, the entire matching line is the target instead of just the match
75+
76+
```lua
77+
motion_state = {
78+
patterns = { "\\v\\f+" }, -- match file paths
79+
}
80+
```
81+
82+
Multiple patterns are matched in order. Multiple matches per line are supported (the collector advances past each match). Invalid regex is safely caught via `pcall`.
83+
84+
The patterns collector yields standard targets, so it composes with any extractor, filter, visualizer, and action. Use `pass_through` as the extractor since the collector already produces complete targets.
85+
86+
### Filetype Dispatch
87+
88+
A pre-pipeline middleware that swaps any pipeline module based on the current buffer's filetype. This allows a single motion to adapt its behavior per filetype — e.g., using regex patterns for filetypes without treesitter parsers.
89+
90+
Configure it via `filetype_overrides` in a motion's `motion_state`:
91+
92+
```lua
93+
motion_state = {
94+
ts_node_types = { "function_declaration" },
95+
filetype_overrides = {
96+
gitcommit = {
97+
collector = "patterns",
98+
motion_state = {
99+
patterns = { "\\vmodified:\\s+\\zs\\S.*$" },
100+
},
101+
},
102+
sql = {
103+
motion_state = {
104+
ts_query = [[(function_definition name: (identifier) @fn)]],
105+
},
106+
},
107+
},
108+
}
109+
```
110+
111+
The override can swap any pipeline module (`collector`, `extractor`, `modifier`, `filter`, `visualizer`, `action`) and/or deep-merge `motion_state` fields. Overrides are per-motion, so different motions can have different filetype behavior. When no override matches the current filetype, the middleware is a no-op.
112+
68113
### Treesitter Collector Modes
69114

70115
The `treesitter` collector is powerful. It supports four modes:

docs/Recipes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ Collectors determine *where targets come from*.
267267
|------|--------|
268268
| `lines` | Visible buffer lines (default for most motions) |
269269
| `treesitter` | Treesitter AST nodes |
270+
| `patterns` | Vim regex matches against buffer lines |
270271
| `diagnostics` | LSP diagnostics |
271272
| `git_hunks` | Git changed regions |
272273
| `quickfix` | Quickfix or location list entries |

0 commit comments

Comments
 (0)