Repository: rsteube/carapace
Pull request #857 (2023-07-29 15:59:40)

added Action.Segments


Patch:
diff --git a/action.go b/action.go
index 957cb75..6ce6ad5 100644
--- a/action.go
+++ b/action.go
@@ -3,6 +3,7 @@ package carapace
 import (
 	"fmt"
 	"os"
+	"regexp"
 	"runtime"
 	"strings"
 	"time"
@@ -114,6 +115,76 @@ func (a Action) MultiParts(dividers ...string) Action {
 	})
 }
 
+// MultiPartsP is like MultiParts but with placeholder completion.
+func (a Action) MultiPartsP(delimiter string, placeholderPattern string, f func(segment string, matches map[string]string, c Context) Action) Action {
+	return ActionCallback(func(c Context) Action {
+		invoked := a.Invoke(c)
+
+		return ActionMultiParts(delimiter, func(c Context) Action {
+			placeholder := regexp.MustCompile(placeholderPattern)
+			matchedData := make(map[string]string)
+			matchedSegments := make(map[string]common.RawValue)
+			staticMatches := make(map[int]bool)
+
+		path:
+			for index, value := range invoked.rawValues {
+				segments := strings.Split(value.Value, delimiter)
+			segment:
+				for index, segment := range segments {
+					if index > len(c.Parts)-1 {
+						break segment
+					} else {
+						if segment != c.Parts[index] {
+							if !placeholder.MatchString(segment) {
+								continue path // skip this path as it doesn't match and is not a placeholder
+							} else {
+								matchedData[segment] = c.Parts[index] // store entered data for placeholder (overwrite if duplicate)
+							}
+						} else {
+							staticMatches[index] = true // static segment matches so placeholders should be ignored for this index
+						}
+					}
+				}
+
+				if len(segments) < len(c.Parts)+1 {
+					continue path // skip path as it is shorter than what was entered (must be after staticMatches being set)
+				}
+
+				for key := range staticMatches {
+					if segments[key] != c.Parts[key] {
+						continue path // skip this path as it has a placeholder where a static segment was matched
+					}
+				}
+
+				// store segment as path matched so far and this is currently being completed
+				if len(segments) == (len(c.Parts) + 1) {
+					matchedSegments[segments[len(c.Parts)]] = invoked.rawValues[index]
+				} else {
+					if segment := segments[len(c.Parts)]; placeholder.MatchString(segment) {
+						matchedSegments[segment] = common.RawValue{}
+					} else {
+						matchedSegments[segment+delimiter] = common.RawValue{}
+					}
+				}
+			}
+
+			actions := make([]Action, 0, len(matchedSegments))
+			for key, value := range matchedSegments {
+				if placeholder.MatchString(key) {
+					actions = append(actions, f(key, matchedData, c))
+				} else {
+					actions = append(actions, ActionStyledValuesDescribed(key, value.Description, value.Style)) // TODO tag,..
+				}
+			}
+
+			// TODO verify
+			a := Batch(actions...).ToA().NoSpace()
+			a.meta.Merge(invoked.meta)
+			return a
+		})
+	})
+}
+
 // NoSpace disables space suffix for given characters (or all if none are given).
 func (a Action) NoSpace(suffixes ...rune) Action {
 	return ActionCallback(func(c Context) Action {
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 02796da..f4783f8 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -23,6 +23,7 @@
     - [Invoke](./carapace/action/invoke.md)
     - [List](./carapace/action/list.md)
     - [MultiParts](./carapace/action/multiParts.md)
+    - [MultiPartsP](./carapace/action/multiPartsP.md)
     - [NoSpace](./carapace/action/noSpace.md)
     - [Prefix](./carapace/action/prefix.md)
     - [Retain](./carapace/action/retain.md)
diff --git a/docs/src/carapace/action/multiPartsP.cast b/docs/src/carapace/action/multiPartsP.cast
new file mode 100644
index 0000000..71cb172
--- /dev/null
+++ b/docs/src/carapace/action/multiPartsP.cast
@@ -0,0 +1,106 @@
+{"version": 2, "width": 108, "height": 24, "timestamp": 1690655294, "env": {"SHELL": "elvish", "TERM": "tmux-256color"}}
+[0.087131, "o", "\u001b[?7h\u001b[7m⏎\u001b[m                                                                                                           \r \r\u001b[?7l\u001b[?2004h"]
+[0.087578, "o", "\u001b[?25l\r???>                                                                                                   ???> \r\u001b[5C\u001b[?25h\u001b[?25l\r\u001b[5C\u001b[K\r\u001b[5C\u001b[?25h"]
+[0.100616, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"]
+[0.100714, "o", "\u001b[?25l\r\u001b[K\r\n\u001b[0;1;36mcarapace/example\u001b[0;m on \u001b[0;1;35m action-segments\u001b[0;m \u001b[0;1;31m[$!?⇡]\u001b[0;m via \u001b[0;1;36m🐹 v1.20.6 \r\n\u001b[0;1;37mesh\u001b[0;m \u001b[0;1;32m❯\u001b[0;m \r\u001b[6C\u001b[?25h"]
+[0.575153, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[0;31me\u001b[0;m\r\u001b[7C\u001b[?25h"]
+[0.575281, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"]
+[0.575773, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"]
+[0.591735, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"]
+[0.591849, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"]
+[0.591883, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"]
+[0.717249, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[7C\u001b[0;31mx\u001b[0;m\r\u001b[8C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[8C\u001b[?25h"]
+[0.833398, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;32mexa\u001b[0;m\r\u001b[9C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[9C\u001b[?25h"]
+[1.007529, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;31mexam\u001b[0;m\r\u001b[10C\u001b[?25h"]
+[1.007943, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[10C\u001b[?25h"]
+[1.025387, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[10C\u001b[0;31mp\u001b[0;m\r\u001b[11C\u001b[?25h"]
+[1.516198, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[11C\u001b[0;31ml\u001b[0;m\r\u001b[12C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[12C\u001b[?25h"]
+[1.610127, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;32mexample\u001b[0;m\r\u001b[13C\u001b[?25h"]
+[1.610225, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[13C\u001b[?25h"]
+[1.739784, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[13C \r\u001b[14C\u001b[?25h"]
+[1.90929, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[14Cm\r\u001b[15C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[15C\u001b[?25h"]
+[1.991874, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[15Co\r\u001b[16C\u001b[?25h"]
+[2.019321, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[16C\u001b[?25h"]
+[2.163819, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[16Cdifier \r\u001b[23C\u001b[?25h"]
+[2.532379, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[23C-\r\u001b[24C\u001b[?25h"]
+[2.532767, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[24C\u001b[?25h"]
+[2.685175, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[24C-\r\u001b[25C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[25C\u001b[?25h"]
+[2.894143, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[25Cm\r\u001b[26C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[26C\u001b[?25h"]
+[3.113029, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[26Cu\r\u001b[27C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[27C\u001b[?25h"]
+[3.234665, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[27Cltiparts\r\u001b[35C\u001b[?25h"]
+[3.658776, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[23C\u001b[K\u001b[0;4m--multiparts \r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7;34m--multiparts\u001b[0;2;7m (MultiParts())\u001b[0;m  \u001b[0;34m--multipartsp\u001b[0;2m (MultiPartsP())\u001b[0;m\u001b[1A\r\u001b[22C\u001b[?25h"]
+[4.011727, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[35C\u001b[K\u001b[0;4mp \r\n\r\n\u001b[0;m\u001b[K\u001b[0;34m--multiparts\u001b[0;2m (MultiParts())\u001b[0;m  \u001b[0;7;34m--multipartsp\u001b[0;2;7m (MultiPartsP())\u001b[0;m\u001b[1A\r\u001b[22C\u001b[?25h"]
+[4.374427, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[23C\u001b[K--multipartsp \r\n\u001b[J\u001b[A\r\u001b[37C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[37C\u001b[?25h"]
+[4.954615, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37C\u001b[0;4mkeys/\r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7mkeys/\u001b[0;m  \u001b[0;33mstyles\u001b[0;2m (list)\u001b[0;m  styles/\u001b[1A\r\u001b[22C\u001b[?25h"]
+[5.832453, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[37C\u001b[Kkeys/\r\n\u001b[J\u001b[A\r\u001b[42C\u001b[?25h"]
+[5.832549, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[42C\u001b[?25h"]
+[6.079439, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[42Ckey\r\u001b[45C\u001b[?25h"]
+[6.512266, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37C\u001b[K\u001b[0;4mkeys/key1\r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7mkey1\u001b[0;m  key2\u001b[1A\r\u001b[22C\u001b[?25h"]
+[6.512318, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\r\n\r\n\u001b[1A\r\u001b[22C\u001b[?25h"]
+[6.991968, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[37C\u001b[Kkeys/key1\r\n\u001b[J\u001b[A\r\u001b[46C\u001b[?25h"]
+[7.323388, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[46C/\r\u001b[47C\u001b[?25h"]
+[7.323789, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[47C\u001b[?25h"]
+[7.53157, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[47Cval\r\u001b[50C\u001b[?25h"]
+[7.984449, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37C\u001b[K\u001b[0;4mkeys/key1/val1\r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7mval1\u001b[0;m  val2\u001b[1A\r\u001b[22C\u001b[?25h"]
+[8.546894, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[50C\u001b[K\u001b[0;4m2\r\n\r\n\u001b[0;m\u001b[Kval1  \u001b[0;7mval2\u001b[0;m\u001b[1A\r\u001b[22C\u001b[?25h"]
+[8.98599, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[37C\u001b[Kkeys/key1/val2\r\n\u001b[J\u001b[A\r\u001b[51C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[51C\u001b[?25h"]
+[9.811243, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[50C\u001b[K\r\u001b[50C\u001b[?25h"]
+[9.972302, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[49C\u001b[K\r\u001b[49C\u001b[?25h"]
+[10.572819, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[48C\u001b[K\r\u001b[48C\u001b[?25h"]
+[10.61249, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[47C\u001b[K\r\u001b[47C\u001b[?25h"]
+[10.652673, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[46C\u001b[K\r\u001b[46C\u001b[?25h"]
+[10.692276, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[45C\u001b[K\r\u001b[45C\u001b[?25h"]
+[10.732127, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[44C\u001b[K\r\u001b[44C\u001b[?25h"]
+[10.772303, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[43C\u001b[K\r\u001b[43C\u001b[?25h"]
+[10.812368, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[42C\u001b[K\r\u001b[42C\u001b[?25h"]
+[11.200772, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[42Ckey\r\u001b[45C\u001b[?25h"]
+[11.617331, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37C\u001b[K\u001b[0;4mkeys/key1\r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7mkey1\u001b[0;m  key2\u001b[1A\r\u001b[22C\u001b[?25h"]
+[12.180675, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[45C\u001b[K\u001b[0;4m2\r\n\r\n\u001b[0;m\u001b[Kkey1  \u001b[0;7mkey2\u001b[0;m\u001b[1A\r\u001b[22C\u001b[?25h"]
+[12.480271, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[37C\u001b[Kkeys/key2\r\n\u001b[J\u001b[A\r\u001b[46C\u001b[?25h"]
+[12.480375, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[46C\u001b[?25h"]
+[12.874946, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[46C/\r\u001b[47C\u001b[?25h"]
+[12.875054, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[47C\u001b[?25h"]
+[13.03095, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[47Cval\r\u001b[50C\u001b[?25h"]
+[13.53782, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37C\u001b[K\u001b[0;4mkeys/key2/val3\r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7mval3\u001b[0;m  val4\u001b[1A\r\u001b[22C\u001b[?25h"]
+[13.53872, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\r\n\r\n\u001b[1A\r\u001b[22C\u001b[?25h"]
+[13.538974, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\r\n\r\n\u001b[1A\r\u001b[22C\u001b[?25h"]
+[14.606065, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[50C\u001b[K\u001b[0;4m4\r\n\r\n\u001b[0;m\u001b[Kval3  \u001b[0;7mval4\u001b[0;m\u001b[1A\r\u001b[22C\u001b[?25h"]
+[14.991996, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[37C\u001b[Kkeys/key2/val4\r\n\u001b[J\u001b[A\r\u001b[51C\u001b[?25h"]
+[14.992111, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[51C\u001b[?25h"]
+[15.239399, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[50C\u001b[K\r\u001b[50C\u001b[?25h"]
+[15.840098, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[49C\u001b[K\r\u001b[49C\u001b[?25h"]
+[15.879653, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[48C\u001b[K\r\u001b[48C\u001b[?25h"]
+[15.92016, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[47C\u001b[K\r\u001b[47C\u001b[?25h"]
+[15.959694, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[46C\u001b[K\r\u001b[46C\u001b[?25h"]
+[15.999772, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[45C\u001b[K\r\u001b[45C\u001b[?25h"]
+[16.039003, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[44C\u001b[K\r\u001b[44C\u001b[?25h"]
+[16.079143, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[43C\u001b[K\r\u001b[43C\u001b[?25h"]
+[16.118829, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[42C\u001b[K\r\u001b[42C\u001b[?25h"]
+[16.118908, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[42C\u001b[?25h"]
+[16.15946, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[41C\u001b[K\r\u001b[41C\u001b[?25h"]
+[16.199034, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[40C\u001b[K\r\u001b[40C\u001b[?25h"]
+[16.199111, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[40C\u001b[?25h"]
+[16.337077, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[39C\u001b[K\r\u001b[39C\u001b[?25h"]
+[16.655987, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[38C\u001b[K\r\u001b[38C\u001b[?25h"]
+[16.847731, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37C\u001b[K\r\u001b[37C\u001b[?25h"]
+[17.095744, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37Cs\r\u001b[38C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[38C\u001b[?25h"]
+[17.303075, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[38Ctyles\r\u001b[43C\u001b[?25h"]
+[17.790832, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37C\u001b[K\u001b[0;4mstyles\r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7;33mstyles\u001b[0;2;7m (list)\u001b[0;m  styles/\u001b[1A\r\u001b[22C\u001b[?25h"]
+[18.527858, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[43C\u001b[0;4m/\r\n\r\n\u001b[0;m\u001b[K\u001b[0;33mstyles\u001b[0;2m (list)\u001b[0;m  \u001b[0;7mstyles/\u001b[0;m\u001b[1A\r\u001b[22C\u001b[?25h"]
+[18.787785, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[37C\u001b[Kstyles/\r\n\u001b[J\u001b[A\r\u001b[44C\u001b[?25h"]
+[18.788227, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[44C\u001b[?25h"]
+[19.009425, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[37C\u001b[K\u001b[0;4mstyles/bg-black\r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7;40mbg-black         \u001b[0;m  \u001b[0;46mbg-cyan     \u001b[0;m  \u001b[0;94mbright-blue          \u001b[0;m  \u001b[0;32mgreen     \r\n\u001b[0;44mbg-blue          \u001b[0;m  \u001b[0;42mbg-green    \u001b[0;m  \u001b[0;96mbright-cyan          \u001b[0;m  \u001b[0;7minverse   \r\n\u001b[0;100mbg-bright-black  \u001b[0;m  \u001b[0;45mbg-magenta  \u001b[0;m  \u001b[0;92mbright-green         \u001b[0;m  \u001b[0;3mitalic    \r\n\u001b[0;104mbg-bright-blue   \u001b[0;m  \u001b[0;41mbg-red      \u001b[0;m  \u001b[0;95mbright-magenta       \u001b[0;m  \u001b[0;35mmagenta   \r\n\u001b[0;106mbg-bright-cyan   \u001b[0;m  \u001b[0;47mbg-white    \u001b[0;m  \u001b[0;91mbright-red           \u001b[0;m  \u001b[0;31mred       \r\n\u001b[0;102mbg-bright-green  \u001b[0;m  \u001b[0;43mbg-yellow   \u001b[0;m  \u001b[0;97mbright-white         \u001b[0;m  \u001b[0;4munderlined\r\n\u001b[0;105mbg-bright-magenta\u001b[0;m  \u001b[0;30mblack       \u001b[0;m  \u001b[0;93mbright-yellow        \u001b[0;m  \u001b[0;37mwhite     \r\n\u001b[0;101mbg-bright-red    \u001b[0;m  \u001b[0;5mblink       \u001b[0;m  color                  \u001b[0;33myellow    \r\n\u001b[0;107mbg-bright-white  \u001b[0;m  \u001b[0;34mblue        \u001b[0;m  \u001b[0;5;34mcustom\u001b[0;2m (custom style)\r\n\u001b[0;103mbg-bright-yellow \u001b[0;m  \u001b[0;1mbold        \u001b[0;m  \u001b[0;36mcyan                 \r\n\u001b[0;mbg-color           \u001b[0;90mbright-black\u001b[0;m  \u001b[0;2mdim                  \u001b[0;m\u001b[11A\r\u001b[22C\u001b[?25h"]
+[20.593663, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[47C\u001b[K\u001b[0;4mcyan\r\n\r\n\u001b[0;m\u001b[K\u001b[0;40mbg-black         \u001b[0;m  \u001b[0;7;46mbg-cyan     \u001b[0;m  \u001b[0;94mbright-blue          \u001b[0;m  \u001b[0;32mgreen     \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[0;m\u001b[11A\r\u001b[22C\u001b[?25h"]
+[20.778566, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[45C\u001b[K\u001b[0;4mright-blue\r\n\r\n\u001b[19C\u001b[0;m\u001b[K\u001b[0;46mbg-cyan     \u001b[0;m  \u001b[0;7;94mbright-blue          \u001b[0;m  \u001b[0;32mgreen     \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[0;m\u001b[11A\r\u001b[22C\u001b[?25h"]
+[23.121524, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[6C\u001b[K\r\n\u001b[J\u001b[A\r\u001b[6C\u001b[?25h"]
+[23.121636, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[6C\u001b[?25h"]
+[23.121979, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[6C\u001b[?25h"]
+[23.13986, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[6C\u001b[?25h"]
+[23.140193, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[6C\u001b[?25h"]
+[23.609036, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[0;31me\u001b[0;m\r\u001b[7C\u001b[?25h"]
+[23.609506, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"]
+[23.770074, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[7C\u001b[0;31mx\u001b[0;m\r\u001b[8C\u001b[?25h"]
+[23.770207, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[8C\u001b[?25h"]
+[23.896237, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[8C\u001b[0;31mi\u001b[0;m\r\u001b[9C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[9C\u001b[?25h"]
+[23.977997, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;32mexit\u001b[0;m\r\u001b[10C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[10C\u001b[?25h"]
+[24.069448, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"]
diff --git a/docs/src/carapace/action/multiPartsP.md b/docs/src/carapace/action/multiPartsP.md
new file mode 100644
index 0000000..bb1da16
--- /dev/null
+++ b/docs/src/carapace/action/multiPartsP.md
@@ -0,0 +1,35 @@
+# MultiPartsP
+
+[`MultiPartsP`] is like [MultiParts] but with placeholder completion.
+
+```go
+carapace.ActionStyledValuesDescribed(
+	"keys/<key>/<value>", "key/value example", style.Cyan,
+	"styles/<style>", "details", style.Magenta,
+	"styles/custom", "custom style", style.Of(style.Blue, style.Blink),
+	"styles", "list", style.Yellow,
+).MultiPartsP("/", "<.*>", func(segment string, matches map[string]string, c carapace.Context) carapace.Action {
+	switch segment {
+	case "<style>":
+		return carapace.ActionStyles()
+	case "<key>":
+		return carapace.ActionValues("key1", "key2")
+	case "<value>":
+		switch matches["<key>"] {
+		case "key1":
+			return carapace.ActionValues("val1", "val2")
+		case "key2":
+			return carapace.ActionValues("val3", "val4")
+		default:
+			return carapace.ActionValues()
+		}
+	default:
+		return carapace.ActionValues()
+	}
+})
+```
+
+![](./multiPartsP.cast)
+
+[MultiParts]:./multiParts.md
+[`MultiPartsP`]:https://pkg.go.dev/github.com/rsteube/carapace#Action.MultiPartsP
diff --git a/example/cmd/modifier.go b/example/cmd/modifier.go
index 2c2e855..d318c02 100644
--- a/example/cmd/modifier.go
+++ b/example/cmd/modifier.go
@@ -29,6 +29,7 @@ func init() {
 	modifierCmd.Flags().String("invoke", "", "Invoke()")
 	modifierCmd.Flags().String("list", "", "List()")
 	modifierCmd.Flags().String("multiparts", "", "MultiParts()")
+	modifierCmd.Flags().String("multipartsp", "", "MultiPartsP()")
 	modifierCmd.Flags().String("nospace", "", "NoSpace()")
 	modifierCmd.Flags().String("prefix", "", "Prefix()")
 	modifierCmd.Flags().String("retain", "", "Retain()")
@@ -115,6 +116,30 @@ func init() {
 			"dir/subdir1/fileB.txt",
 			"dir/subdir2/fileC.txt",
 		).MultiParts("/"),
+		"multipartsp": carapace.ActionStyledValuesDescribed(
+			"keys/<key>/<value>", "key/value example", style.Cyan,
+			"styles/<style>", "details", style.Magenta,
+			"styles/custom", "custom style", style.Of(style.Blue, style.Blink),
+			"styles", "list", style.Yellow,
+		).MultiPartsP("/", "<.*>", func(segment string, matches map[string]string, c carapace.Context) carapace.Action {
+			switch segment {
+			case "<style>":
+				return carapace.ActionStyles()
+			case "<key>":
+				return carapace.ActionValues("key1", "key2")
+			case "<value>":
+				switch matches["<key>"] {
+				case "key1":
+					return carapace.ActionValues("val1", "val2")
+				case "key2":
+					return carapace.ActionValues("val3", "val4")
+				default:
+					return carapace.ActionValues()
+				}
+			default:
+				return carapace.ActionValues()
+			}
+		}),
 		"prefix": carapace.ActionFiles().Prefix("file://"),
 		"retain": carapace.ActionValuesDescribed(
 			"1", "one",


Test patch:
diff --git a/example/cmd/modifier_test.go b/example/cmd/modifier_test.go
index f1b510a..7c0168a 100644
--- a/example/cmd/modifier_test.go
+++ b/example/cmd/modifier_test.go
@@ -159,6 +159,56 @@ func TestPrefix(t *testing.T) {
 	})
 }
 
+func TestMultiPartsP(t *testing.T) {
+	sandbox.Package(t, "github.com/rsteube/carapace/example")(func(s *sandbox.Sandbox) {
+		s.Run("modifier", "--multipartsp", "").
+			Expect(carapace.ActionStyledValuesDescribed(
+				"keys/", "", style.Default,
+				"styles", "list", style.Yellow,
+				"styles/", "", style.Default,
+			).NoSpace().
+				Usage("MultiPartsP()"))
+
+		s.Run("modifier", "--multipartsp", "keys/").
+			Expect(carapace.ActionValues(
+				"key1",
+				"key2",
+			).Prefix("keys/").
+				NoSpace().
+				Usage("MultiPartsP()"))
+
+		s.Run("modifier", "--multipartsp", "keys/key1/").
+			Expect(carapace.ActionValues(
+				"val1",
+				"val2",
+			).Prefix("keys/key1/").
+				NoSpace().
+				Usage("MultiPartsP()"))
+
+		s.Run("modifier", "--multipartsp", "keys/key2/").
+			Expect(carapace.ActionValues(
+				"val3",
+				"val4",
+			).Prefix("keys/key2/").
+				NoSpace().
+				Usage("MultiPartsP()"))
+
+		s.Run("modifier", "--multipartsp", "styles/c").
+			Expect(carapace.Batch(
+				carapace.ActionStyledValues(
+					"color", style.Default,
+					"cyan", style.Cyan,
+				).Tag("styles"),
+				carapace.ActionStyledValuesDescribed(
+					"custom", "custom style", style.Of(style.Blue, style.Blink),
+				),
+			).ToA().
+				Prefix("styles/").
+				NoSpace().
+				Usage("MultiPartsP()"))
+	})
+}
+
 func TestSplit(t *testing.T) {
 	os.Unsetenv("LS_COLORS")
 	sandbox.Package(t, "github.com/rsteube/carapace/example")(func(s *sandbox.Sandbox) {