Skip to content

Commit 1d61a90

Browse files
committed
autocomplete / suggestions
1 parent d2a1110 commit 1d61a90

File tree

4 files changed

+194
-31
lines changed

4 files changed

+194
-31
lines changed

evaluator/evaluator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var (
3030
var lex *lexer.Lexer
3131

3232
func init() {
33-
Fns = getFns()
33+
Fns = GetFns()
3434
if os.Getenv("ABS_COMMAND_EXECUTOR") == "" {
3535
// Set the executor for system commands
3636
// thanks to @haifenghuang

evaluator/functions.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ func init() {
4242
/*
4343
Here be the hairy map to all the Builtin Functions ... ARRRGH, matey
4444
*/
45-
func getFns() map[string]*object.Builtin {
45+
// TODO these should just be module vars
46+
func GetFns() map[string]*object.Builtin {
4647
return map[string]*object.Builtin{
4748
// len(var:"hello")
4849
"len": &object.Builtin{
@@ -952,6 +953,7 @@ func argFn(tok token.Token, env *object.Environment, args ...object.Object) obje
952953
i := arg.Int()
953954

954955
if i > len(os.Args)-1 || i < 0 {
956+
// TODO this should maybe return null
955957
return &object.String{Token: tok, Value: ""}
956958
}
957959

terminal/styles.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ var styleCode = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
1313
var styleErr = lipgloss.NewStyle().Foreground(lipgloss.Color("#ed4747"))
1414

1515
var styleNestedContainer = lipgloss.NewStyle().PaddingTop(2).PaddingLeft(2)
16+
17+
var styleSuggestion = lipgloss.NewStyle().PaddingLeft(1).PaddingTop(1).Foreground(lipgloss.Color("2"))
18+
var styleSelectedSuggestion = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Underline(true)
19+
var styleSelectedPrefix = styleSelectedSuggestion.Underline(false)

terminal/terminal.go

Lines changed: 186 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,31 @@ import (
1010
"os"
1111
"os/user"
1212
"slices"
13+
"sort"
1314
"strings"
15+
"unicode"
1416

17+
"github.com/abs-lang/abs/evaluator"
18+
"github.com/abs-lang/abs/lexer"
1519
"github.com/abs-lang/abs/object"
1620
"github.com/abs-lang/abs/runner"
21+
"github.com/abs-lang/abs/token"
1722
"github.com/abs-lang/abs/util"
1823
"github.com/charmbracelet/bubbles/textarea"
1924
"github.com/charmbracelet/bubbles/textinput"
2025
tea "github.com/charmbracelet/bubbletea"
2126
)
2227

2328
// TODO
29+
// reverse search
30+
// unable to print literal tabs when using tab key?
2431
// autocompleter
32+
// up down change of direction messes history
33+
// more example statements
34+
// WONTFIXNOW
2535
// maybe only save incrementally in history https://stackoverflow.com/questions/7151261/append-to-a-file-in-go ?
2636
// worth renaming repl to runner? and maybe terminal back to repl
2737
// add prompt formatting tests
28-
// more example statements
2938

3039
var debug = os.Getenv("DEBUG") == "1"
3140

@@ -43,14 +52,15 @@ func NewTerminal(env *object.Environment, stdinRelay io.Writer) *tea.Program {
4352
in.Focus()
4453

4554
m := Model{
46-
in: in,
47-
env: env,
48-
stdinRelay: stdinRelay,
49-
prompt: prompt,
50-
history: history,
51-
historyIndex: len(history) - 1,
52-
historyFile: historyFile,
53-
historyMaxLInes: maxLines,
55+
in: in,
56+
env: env,
57+
stdinRelay: stdinRelay,
58+
prompt: prompt,
59+
history: history,
60+
historyIndex: len(history) - 1,
61+
historyFile: historyFile,
62+
historyMaxLInes: maxLines,
63+
suggestionsIndex: -1,
5464
}
5565

5666
p := tea.NewProgram(m)
@@ -89,6 +99,10 @@ type Model struct {
8999
historyIndex int
90100
historyFile string
91101
historyMaxLInes int
102+
// autocomplete
103+
suggestionsIndex int
104+
suggestions []string
105+
suggestToken string
92106
}
93107

94108
func (m Model) Init() tea.Cmd {
@@ -99,6 +113,25 @@ func (m Model) Init() tea.Cmd {
99113
)
100114
}
101115

116+
func (m Model) View() string {
117+
view := m.in.View()
118+
119+
if m.IsSuggesting() {
120+
view += "\n" + m.renderSuggestions()
121+
}
122+
123+
if debug {
124+
m := m.asMap()
125+
wrapper := ""
126+
for _, k := range slices.Sorted(maps.Keys(m)) {
127+
wrapper += fmt.Sprintf(("\n%s: %v"), k, m[k])
128+
}
129+
view += styleNestedContainer.Render(styleDebug.Render(wrapper))
130+
}
131+
132+
return view
133+
}
134+
102135
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
103136
var (
104137
tiCmd tea.Cmd
@@ -116,11 +149,25 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116149
if m.isEvaluating {
117150
return m.interceptStdin(msg)
118151
}
152+
153+
if m.IsSuggesting() {
154+
switch msg.Type {
155+
case tea.KeyEnter:
156+
return m.selectSuggestion(), nil
157+
case tea.KeyTab, tea.KeyDown:
158+
return m.suggest(+1), nil
159+
case tea.KeyUp:
160+
return m.suggest(-1), nil
161+
default:
162+
return m.exitSuggestions(), nil
163+
}
164+
}
165+
119166
switch msg.Type {
120167
case tea.KeyEsc, tea.KeyCtrlD:
121168
return m.quit()
122169
case tea.KeyCtrlC:
123-
m = m.resetHistory()
170+
m = m.resetInput()
124171
return m.interrupt()
125172
case tea.KeyEnter:
126173
// Let's get rid of the placeholder
@@ -140,7 +187,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
140187
m.history = append(m.history, m.in.Value())
141188
}
142189

143-
m = m.resetHistory()
190+
m = m.resetInput()
144191

145192
switch m.in.Value() {
146193
case "quit":
@@ -153,9 +200,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
153200
case tea.KeyTab:
154201
// If the placeholder code is shown,
155202
// allow the user to run it by tabbing
156-
if m.in.Placeholder != "" {
157-
return m.engagePlaceholder()
203+
if m.in.Value() == "" {
204+
if m.in.Placeholder != "" {
205+
return m.engagePlaceholder()
206+
}
207+
208+
return m, nil
158209
}
210+
211+
return m.suggest(0), nil
159212
case tea.KeyCtrlL:
160213
return m.clear()
161214
case tea.KeyUp:
@@ -169,6 +222,98 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
169222
return m, tiCmd
170223
}
171224

225+
func (m Model) exitSuggestions() Model {
226+
m.in.SetValue(m.dirtyInput)
227+
return m.resetInput()
228+
}
229+
230+
func (m Model) selectSuggestion() Model {
231+
return m.resetInput()
232+
}
233+
234+
func (m Model) IsSuggesting() bool {
235+
return len(m.suggestions) > 0
236+
}
237+
238+
func IsLetter(s string) bool {
239+
return !strings.ContainsFunc(s, func(r rune) bool {
240+
return !unicode.IsLetter(r)
241+
})
242+
}
243+
244+
func applySuggestion(s, suggestion string) string {
245+
ix := strings.LastIndex(s, s)
246+
247+
return s[:ix] + suggestion
248+
}
249+
250+
func (m Model) suggest(direction int) Model {
251+
if m.IsSuggesting() {
252+
m.suggestionsIndex += direction
253+
m.suggestionsIndex %= len(m.suggestions)
254+
255+
if m.suggestionsIndex < 0 {
256+
m.suggestionsIndex += len(m.suggestions)
257+
}
258+
259+
m.in.SetValue(applySuggestion(m.dirtyInput, m.suggestions[m.suggestionsIndex]))
260+
m.in.CursorEnd()
261+
}
262+
263+
if !m.IsSuggesting() {
264+
l := lexer.New(m.in.Value())
265+
tokens := []token.Token{}
266+
var done bool
267+
268+
for !done {
269+
t := l.NextToken()
270+
if t.Type == token.EOF {
271+
done = true
272+
break
273+
}
274+
275+
tokens = append(tokens, t)
276+
}
277+
278+
if len(tokens) == 0 {
279+
return m
280+
}
281+
282+
if tokens[len(tokens)-1].Type != token.IDENT {
283+
return m
284+
}
285+
286+
s := tokens[len(tokens)-1].Literal
287+
m.dirtyInput = m.in.Value()
288+
m.suggestToken = s
289+
m.suggestions = m.getSuggestions(s)
290+
291+
if len(m.suggestions) == 1 {
292+
m.in.SetValue(applySuggestion(m.dirtyInput, m.suggestions[0]))
293+
return m.resetInput()
294+
}
295+
}
296+
297+
return m
298+
}
299+
300+
func (m Model) renderSuggestions() string {
301+
lines := Lines{}
302+
303+
for i, s := range m.suggestions {
304+
prefix := " "
305+
306+
if m.suggestionsIndex == i {
307+
prefix = styleSelectedPrefix.Render(" → ")
308+
s = styleSelectedSuggestion.Render(s)
309+
}
310+
311+
lines.Add(prefix + s)
312+
}
313+
314+
return styleSuggestion.Render(lines.Join())
315+
}
316+
172317
func (m Model) maxHistoryIndex() int {
173318
return len(m.history) - 1
174319
}
@@ -208,34 +353,23 @@ func (m Model) nextHistory() Model {
208353
return m
209354
}
210355

211-
func (m Model) resetHistory() Model {
356+
func (m Model) resetInput() Model {
212357
m.dirtyInput = ""
213358
m.historyIndex = m.maxHistoryIndex()
359+
m.suggestionsIndex = -1
360+
m.suggestions = []string{}
361+
m.in.CursorEnd()
214362

215363
return m
216364
}
217365

218-
func (m Model) View() string {
219-
view := m.in.View()
220-
221-
if debug {
222-
m := m.asMap()
223-
wrapper := ""
224-
for _, k := range slices.Sorted(maps.Keys(m)) {
225-
wrapper += fmt.Sprintf(("\n%s: %v"), k, m[k])
226-
}
227-
view += styleNestedContainer.Render(styleDebug.Render(wrapper))
228-
}
229-
230-
return view
231-
}
232-
233366
func (m Model) asMap() map[string]any {
234367
return map[string]any{
235368
"history_index": m.historyIndex,
236369
"max_history_index": m.maxHistoryIndex(),
237370
"dirty_input": m.dirtyInput,
238371
"is_evaluating": m.isEvaluating,
372+
"suggestions_index": m.suggestionsIndex,
239373
}
240374
}
241375

@@ -397,3 +531,26 @@ func (m Model) interrupt() (Model, tea.Cmd) {
397531

398532
return m, tea.Println(l)
399533
}
534+
535+
func (m Model) getSuggestions(s string) []string {
536+
matches := []string{}
537+
538+
vars := m.env.GetKeys()
539+
sort.Strings(vars)
540+
541+
for _, v := range vars {
542+
if strings.HasPrefix(strings.ToLower(v), strings.ToLower(s)) {
543+
matches = append(matches, v)
544+
}
545+
}
546+
547+
for _, f := range slices.Sorted(maps.Keys(evaluator.GetFns())) {
548+
if strings.HasPrefix(strings.ToLower(f), strings.ToLower(s)) {
549+
matches = append(matches, f)
550+
}
551+
}
552+
553+
sort.Strings(matches)
554+
555+
return matches
556+
}

0 commit comments

Comments
 (0)