@@ -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
3039var 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
94108func (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+
102135func (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+
172317func (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-
233366func (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