|
1 | 1 | package shlex |
2 | 2 |
|
3 | | -import "encoding/json" |
4 | | - |
5 | | -// SubstitutionKind classifies the type of substitution scope. |
6 | | -type SubstitutionKind int |
7 | | - |
8 | | -const ( |
9 | | - // SUBSTITUTION_COMMAND is $(...), (...), or similar command/output capture. |
10 | | - SUBSTITUTION_COMMAND SubstitutionKind = iota |
11 | | - // SUBSTITUTION_ARITHMETIC is $((...)). |
12 | | - SUBSTITUTION_ARITHMETIC |
13 | | - // SUBSTITUTION_BACKTICK is `...` (POSIX backtick command substitution). |
14 | | - SUBSTITUTION_BACKTICK |
15 | | -) |
16 | | - |
17 | | -var substitutionKinds = map[SubstitutionKind]string{ |
18 | | - SUBSTITUTION_COMMAND: "SUBSTITUTION_COMMAND", |
19 | | - SUBSTITUTION_ARITHMETIC: "SUBSTITUTION_ARITHMETIC", |
20 | | - SUBSTITUTION_BACKTICK: "SUBSTITUTION_BACKTICK", |
21 | | -} |
22 | | - |
23 | | -func (k SubstitutionKind) MarshalJSON() ([]byte, error) { |
24 | | - return json.Marshal(substitutionKinds[k]) |
25 | | -} |
26 | | - |
27 | | -// SubstitutionScope describes a single substitution nesting level in the |
28 | | -// token stream. |
29 | | -type SubstitutionScope struct { |
30 | | - OpenIndex int // TokenSlice index of the opener token |
31 | | - CloseIndex int // TokenSlice index of the closer token, or -1 if unclosed |
32 | | - Kind SubstitutionKind |
33 | | - Depth int // nesting depth at this scope (1 = outermost) |
34 | | -} |
35 | | - |
36 | | -// SubstitutionScopes returns all substitution scopes in the token slice, |
37 | | -// ordered by open position. Unclosed scopes (cursor inside) have |
38 | | -// CloseIndex == -1. Backtick scopes are detected by scanning WORD_TOKEN |
39 | | -// RawValue for unescaped backticks and have OpenIndex == -1 (since there |
40 | | -// is no single opener token in the stream). |
41 | | -func (t TokenSlice) SubstitutionScopes() []SubstitutionScope { |
42 | | - var scopes []SubstitutionScope |
43 | | - var stack []SubstitutionScope |
44 | | - |
45 | | - for i, token := range t { |
46 | | - switch { |
47 | | - case token.WordbreakType == WORDBREAK_SUBSTITUTION_OPEN: |
48 | | - kind := SUBSTITUTION_COMMAND |
49 | | - // Detect arithmetic $(( : the opener RawValue contains two '(' |
50 | | - if len(token.RawValue) >= 2 && token.RawValue[len(token.RawValue)-2] == '(' { |
51 | | - kind = SUBSTITUTION_ARITHMETIC |
| 3 | +// innermostUnclosedCommandScope returns the TokenSlice index of the opener |
| 4 | +// of the innermost unclosed command substitution scope, or -1 if the cursor |
| 5 | +// is at top level. Arithmetic ($((...))) and backtick scopes are not |
| 6 | +// command scopes — they don't produce an inner completion context. |
| 7 | +func innermostUnclosedCommandScope(tokens TokenSlice) int { |
| 8 | + depth := 0 |
| 9 | + lastOpen := -1 |
| 10 | + |
| 11 | + for i, t := range tokens { |
| 12 | + switch t.WordbreakType { |
| 13 | + case WORDBREAK_SUBSTITUTION_OPEN: |
| 14 | + if isArithmeticOpener(t) { |
| 15 | + continue |
52 | 16 | } |
53 | | - scope := SubstitutionScope{ |
54 | | - OpenIndex: i, |
55 | | - CloseIndex: -1, |
56 | | - Kind: kind, |
57 | | - Depth: len(stack) + 1, |
| 17 | + depth++ |
| 18 | + lastOpen = i |
| 19 | + case WORDBREAK_SUBSTITUTION_CLOSE: |
| 20 | + if isArithmeticCloser(t) { |
| 21 | + continue |
58 | 22 | } |
59 | | - stack = append(stack, scope) |
60 | | - |
61 | | - case token.WordbreakType == WORDBREAK_SUBSTITUTION_CLOSE: |
62 | | - if len(stack) > 0 { |
63 | | - scope := &stack[len(stack)-1] |
64 | | - scope.CloseIndex = i |
65 | | - scopes = append(scopes, *scope) |
66 | | - stack = stack[:len(stack)-1] |
| 23 | + depth-- |
| 24 | + if depth == 0 { |
| 25 | + lastOpen = -1 |
67 | 26 | } |
68 | 27 | } |
69 | 28 | } |
70 | 29 |
|
71 | | - // Any unclosed scopes remain on the stack |
72 | | - for j := range stack { |
73 | | - s := stack[j] |
74 | | - scopes = append(scopes, s) |
75 | | - } |
76 | | - |
77 | | - // Scan for backtick substitution in WORD_TOKEN RawValues. |
78 | | - // This is detect-only: backtick content is already merged into |
79 | | - // word tokens by the tokenizer, so we can't extract inner tokens. |
80 | | - scopes = append(scopes, t.detectBacktickScopes()...) |
81 | | - |
82 | | - return scopes |
83 | | -} |
84 | | - |
85 | | -// detectBacktickScopes scans WORD_TOKENs for unescaped backticks. |
86 | | -// An odd count of backticks in a single word's RawValue indicates an |
87 | | -// unclosed backtick substitution starting at that word. The scope has |
88 | | -// OpenIndex == -1 (no single opener token) and Kind == SUBSTITUTION_BACKTICK. |
89 | | -func (t TokenSlice) detectBacktickScopes() []SubstitutionScope { |
90 | | - var scopes []SubstitutionScope |
91 | | - for i, token := range t { |
92 | | - if token.Type != WORD_TOKEN { |
93 | | - continue |
94 | | - } |
95 | | - count := countUnescapedBackticks(token.RawValue) |
96 | | - if count%2 == 1 { |
97 | | - scopes = append(scopes, SubstitutionScope{ |
98 | | - OpenIndex: -1, |
99 | | - CloseIndex: -1, |
100 | | - Kind: SUBSTITUTION_BACKTICK, |
101 | | - Depth: 1, |
102 | | - }) |
103 | | - _ = i // index kept for potential future use |
104 | | - } |
105 | | - } |
106 | | - return scopes |
107 | | -} |
108 | | - |
109 | | -// countUnescapedBackticks counts backtick characters in s that are not |
110 | | -// preceded by an escape character (backslash). |
111 | | -func countUnescapedBackticks(s string) int { |
112 | | - count := 0 |
113 | | - runes := []rune(s) |
114 | | - for i, r := range runes { |
115 | | - if r == '`' { |
116 | | - if i > 0 && runes[i-1] == '\\' { |
117 | | - continue // escaped backtick |
118 | | - } |
119 | | - count++ |
120 | | - } |
| 30 | + if depth > 0 { |
| 31 | + return lastOpen |
121 | 32 | } |
122 | | - return count |
| 33 | + return -1 |
123 | 34 | } |
124 | 35 |
|
125 | | -// innermostUnclosedScope returns the unclosed substitution scope with the |
126 | | -// greatest depth, or nil if all scopes are closed. When the cursor is |
127 | | -// inside a substitution, this is the scope whose content forms the inner |
128 | | -// completion context. |
129 | | -func innermostUnclosedScope(scopes []SubstitutionScope) *SubstitutionScope { |
130 | | - var result *SubstitutionScope |
131 | | - for i := range scopes { |
132 | | - s := &scopes[i] |
133 | | - if s.CloseIndex == -1 { |
134 | | - if result == nil || s.Depth > result.Depth { |
135 | | - result = s |
136 | | - } |
137 | | - } |
138 | | - } |
139 | | - return result |
| 36 | +func isArithmeticOpener(t Token) bool { |
| 37 | + return len(t.RawValue) >= 2 && t.RawValue[len(t.RawValue)-2] == '(' |
140 | 38 | } |
141 | 39 |
|
142 | | -// countUnclosedScopes returns the number of scopes with CloseIndex == -1. |
143 | | -func countUnclosedScopes(scopes []SubstitutionScope) int { |
144 | | - count := 0 |
145 | | - for _, s := range scopes { |
146 | | - if s.CloseIndex == -1 { |
147 | | - count++ |
148 | | - } |
149 | | - } |
150 | | - return count |
| 40 | +func isArithmeticCloser(t Token) bool { |
| 41 | + return len(t.RawValue) >= 2 && t.RawValue[0] == ')' && t.RawValue[1] == ')' |
151 | 42 | } |
0 commit comments