Commit 44a55d41ad6b5c61c75456414e13aec94b367b02

Parents: c4e6de9d59cc534171fd0ef9fa51995e70a8b32e

From: Robin Jarry <robin@jarry.cc>
Date: Sat Aug 12 00:51:41 2023 +0700

complete: allow setting the completion key binding
Until now, if less than complete-min-chars were entered or if
completion-delay had not expired yet, the only way to force trigger
completion was to press <tab>.

In some cases, <tab> is already bound to another action (for example
:next-field in the compose::editor context). This makes forcing the
completion impossible.

Allow defining a key to trigger manual completion via the new $complete
special entry in binds.conf.

Leave the default binding to <tab>. Set it to <C-o> in the
[compose::editor] to avoid conflicting with the existing <tab> binding.

Changelog-added: Customize key to trigger completion with `$complete` in
 `binds.conf`.
Signed-off-by: Robin Jarry <robin@jarry.cc>
Reviewed-by: Tim Culverhouse <tim@timculverhouse.com>

Stats

app/compose.go +3/-0
app/exline.go +3/-0
config/binds.conf +1/-0
config/binds.go +22/-10
doc/aerc-binds.5.scd +6/-0
lib/ui/textinput.go +12/-18

Changeset

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
diff --git a/app/compose.go b/app/compose.go
index 9ab8206b9e40e6784bda767a45fc5464fb4c7ccb..bf1d32cb7cf57fd9b5782361be5244dfe243f5a4 100644
--- a/app/compose.go
+++ b/app/compose.go
@@ -245,6 +245,7 @@ 				e.input.TabComplete(
 					cmpl.ForHeader(h),
 					uiConfig.CompletionDelay,
 					uiConfig.CompletionMinChars,
+					&config.Binds.Compose.CompleteKey,
 				)
 			}
 			c.editors[h] = e
@@ -277,6 +278,7 @@ 					e.input.TabComplete(
 						cmpl.ForHeader(h),
 						uiConfig.CompletionDelay,
 						uiConfig.CompletionMinChars,
+						&config.Binds.Compose.CompleteKey,
 					)
 				}
 				c.editors[h] = e
@@ -1420,6 +1422,7 @@ 			e.input.TabComplete(
 				c.completer.ForHeader(header),
 				uiConfig.CompletionDelay,
 				uiConfig.CompletionMinChars,
+				&config.Binds.Compose.CompleteKey,
 			)
 		}
 		c.editors[header] = e
diff --git a/app/exline.go b/app/exline.go
index b941b10005d1f3f9fdebd66fd89a4b36e4e41bb4..61fc340a7a2f968d0c4a10404bdd772ed1e333fa 100644
--- a/app/exline.go
+++ b/app/exline.go
@@ -26,6 +26,7 @@ 		input.TabComplete(
 			tabcomplete,
 			config.Ui.CompletionDelay,
 			config.Ui.CompletionMinChars,
+			&config.Binds.Global.CompleteKey,
 		)
 	}
 	exline := &ExLine{
@@ -43,6 +44,7 @@ 	x.input.TabComplete(
 		tabComplete,
 		config.Ui.CompletionDelay,
 		config.Ui.CompletionMinChars,
+		&config.Binds.Global.CompleteKey,
 	)
 }
 
@@ -55,6 +57,7 @@ 		input.TabComplete(
 			tabcomplete,
 			config.Ui.CompletionDelay,
 			config.Ui.CompletionMinChars,
+			&config.Binds.Global.CompleteKey,
 		)
 	}
 	exline := &ExLine{
diff --git a/config/binds.conf b/config/binds.conf
index a38795d516c73ac003459cf402637b695c58339d..58d8150887aae7708667383edc4ce624a15f027d 100644
--- a/config/binds.conf
+++ b/config/binds.conf
@@ -115,6 +115,7 @@ # Keybindings used when the embedded terminal is not selected in the compose
 # view
 $noinherit = true
 $ex = <C-x>
+$complete = <C-o>
 <C-k> = :prev-field<Enter>
 <C-Up> = :prev-field<Enter>
 <C-j> = :next-field<Enter>
diff --git a/config/binds.go b/config/binds.go
index 4552030d626cb9a54a524d6d7963b2e52e67bc1d..1ea36a2d869541d7d6649619d473f83eb315e69f 100644
--- a/config/binds.go
+++ b/config/binds.go
@@ -57,6 +57,8 @@ 	// If false, disable global keybindings in this context
 	Globals bool
 	// Which key opens the ex line (default is :)
 	ExKey KeyStroke
+	// Which key triggers completion (default is <tab>)
+	CompleteKey KeyStroke
 
 	// private
 	contextualBinds  []*BindingConfigContext
@@ -154,7 +156,8 @@
 func LoadBindingSection(sec *ini.Section) (*KeyBindings, error) {
 	bindings := NewKeyBindings()
 	for key, value := range sec.KeysHash() {
-		if key == "$ex" {
+		switch key {
+		case "$ex":
 			strokes, err := ParseKeyStrokes(value)
 			if err != nil {
 				return nil, err
@@ -163,9 +166,7 @@ 			if len(strokes) != 1 {
 				return nil, errors.New("Invalid binding")
 			}
 			bindings.ExKey = strokes[0]
-			continue
-		}
-		if key == "$noinherit" {
+		case "$noinherit":
 			if value == "false" {
 				continue
 			}
@@ -173,13 +174,22 @@ 			if value != "true" {
 				return nil, errors.New("Invalid binding")
 			}
 			bindings.Globals = false
-			continue
-		}
-		binding, err := ParseBinding(key, value)
-		if err != nil {
-			return nil, err
+		case "$complete":
+			strokes, err := ParseKeyStrokes(value)
+			if err != nil {
+				return nil, err
+			}
+			if len(strokes) != 1 {
+				return nil, errors.New("Invalid binding")
+			}
+			bindings.CompleteKey = strokes[0]
+		default:
+			binding, err := ParseBinding(key, value)
+			if err != nil {
+				return nil, err
+			}
+			bindings.Add(binding)
 		}
-		bindings.Add(binding)
 	}
 	return bindings, nil
 }
@@ -266,6 +276,7 @@
 func NewKeyBindings() *KeyBindings {
 	return &KeyBindings{
 		ExKey:            KeyStroke{tcell.ModNone, tcell.KeyRune, ':'},
+		CompleteKey:      KeyStroke{tcell.ModNone, tcell.KeyTab, 0},
 		Globals:          true,
 		contextualCache:  make(map[bindsContextKey]*KeyBindings),
 		contextualCounts: make(map[bindsContextType]int),
@@ -329,6 +340,7 @@ 		}
 	}
 	merged.Bindings = filterAndCleanBindings(merged.Bindings)
 	merged.ExKey = bindings[0].ExKey
+	merged.CompleteKey = bindings[0].CompleteKey
 	merged.Globals = bindings[0].Globals
 	return merged
 }
diff --git a/doc/aerc-binds.5.scd b/doc/aerc-binds.5.scd
index 228c6cd1ca038e3aef2f3340f6413abbeaaec752..99ef11880978fe7cfe11e761a64b0e994edbe46f 100644
--- a/doc/aerc-binds.5.scd
+++ b/doc/aerc-binds.5.scd
@@ -122,6 +122,12 @@ 	context.
 
 	Default: _:_
 
+*$complete* = _<key-stroke>_
+	This can be set to a keystroke which will trigger command completion in
+	this context for text inputs that support it.
+
+	Default: _<tab>_
+
 # SUPPORTED KEYS
 
 In addition to letters and some characters (e.g. *a*, *RR*, *gu*, *?*, *!*,
diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go
index dd946aec80e3d6d94c8f7d2958d18d1e2a041e4e..4b051d884b322ca36855404bcd894cea91afa73c 100644
--- a/lib/ui/textinput.go
+++ b/lib/ui/textinput.go
@@ -35,6 +35,7 @@ 	completeIndex     int
 	completeDelay     time.Duration
 	completeDebouncer *time.Timer
 	completeMinChars  int
+	completeKey       *config.KeyStroke
 	uiConfig          *config.UIConfig
 }
 
@@ -62,12 +63,12 @@ }
 
 func (ti *TextInput) TabComplete(
 	tabcomplete func(s string) ([]string, string),
-	d time.Duration,
-	minChars int,
+	d time.Duration, minChars int, key *config.KeyStroke,
 ) *TextInput {
 	ti.tabcomplete = tabcomplete
 	ti.completeDelay = d
 	ti.completeMinChars = minChars
+	ti.completeKey = key
 	return ti
 }
 
@@ -344,55 +345,48 @@ func (ti *TextInput) Event(event tcell.Event) bool {
 	ti.Lock()
 	defer ti.Unlock()
 	if event, ok := event.(*tcell.EventKey); ok {
+		c := ti.completeKey
+		if c != nil && c.Key == event.Key() && c.Modifiers == event.Modifiers() {
+			ti.showCompletions()
+			return true
+		}
+
+		ti.invalidateCompletions()
+
 		switch event.Key() {
 		case tcell.KeyBackspace, tcell.KeyBackspace2:
-			ti.invalidateCompletions()
 			ti.backspace()
 		case tcell.KeyCtrlD, tcell.KeyDelete:
-			ti.invalidateCompletions()
 			ti.deleteChar()
 		case tcell.KeyCtrlB, tcell.KeyLeft:
-			ti.invalidateCompletions()
 			if ti.index > 0 {
 				ti.index--
 				ti.ensureScroll()
 				ti.Invalidate()
 			}
 		case tcell.KeyCtrlF, tcell.KeyRight:
-			ti.invalidateCompletions()
 			if ti.index < len(ti.text) {
 				ti.index++
 				ti.ensureScroll()
 				ti.Invalidate()
 			}
 		case tcell.KeyCtrlA, tcell.KeyHome:
-			ti.invalidateCompletions()
 			ti.index = 0
 			ti.ensureScroll()
 			ti.Invalidate()
 		case tcell.KeyCtrlE, tcell.KeyEnd:
-			ti.invalidateCompletions()
 			ti.index = len(ti.text)
 			ti.ensureScroll()
 			ti.Invalidate()
 		case tcell.KeyCtrlK:
-			ti.invalidateCompletions()
 			ti.deleteLineForward()
 		case tcell.KeyCtrlW:
-			ti.invalidateCompletions()
 			ti.deleteWord()
 		case tcell.KeyCtrlU:
-			ti.invalidateCompletions()
 			ti.deleteLineBackward()
 		case tcell.KeyESC:
-			if ti.completions != nil {
-				ti.invalidateCompletions()
-				ti.Invalidate()
-			}
-		case tcell.KeyTab:
-			ti.showCompletions()
+			ti.Invalidate()
 		case tcell.KeyRune:
-			ti.invalidateCompletions()
 			ti.insert(event.Rune())
 		}
 	}