peterh / liner

Pure Go line editor with history, inspired by linenoise
MIT License
1.05k stars 132 forks source link

Completion at cursor position #14

Closed gwenn closed 10 years ago

gwenn commented 10 years ago

Hello, Currently, the completion seems to work only at the end of the line. Would it be possible to make it work at current cursor position ? For an example: sql> create table test (id int, name text); sql> select t. from test t; When the cursor is after the dot, the table columns can be completed. Regards.

peterh commented 10 years ago

Does this do what you want?

diff --git a/line.go b/line.go
index 53f725f..a6a0eaf 100644
--- a/line.go
+++ b/line.go
@@ -122,22 +122,23 @@ func (s *State) refresh(prompt string, buf string, pos int) error {
    return err
 }

-func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error) {
+func (s *State) tabComplete(p string, line []rune, pos int) ([]rune, int, interface{}, error) {
    if s.completer == nil {
-       return line, rune(tab), nil
+       return line, pos, rune(tab), nil
    }
-   list := s.completer(string(line))
+   list := s.completer(string(line[:pos]))
    if len(list) <= 0 {
-       return line, rune(tab), nil
+       return line, pos, rune(tab), nil
    }
    listEntry := 0
+   tail := string(line[pos:])
    for {
        pick := list[listEntry]
-       s.refresh(p, pick, len(pick))
+       s.refresh(p, pick+tail, utf8.RuneCountInString(pick))

        next, err := s.readNext()
        if err != nil {
-           return line, rune(tab), err
+           return line, pos, rune(tab), err
        }
        if key, ok := next.(rune); ok {
            if key == tab {
@@ -149,7 +150,7 @@ func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error)
                continue
            }
            if key == esc {
-               return line, rune(esc), nil
+               return line, pos, rune(esc), nil
            }
        }
        if a, ok := next.(action); ok && a == shiftTab {
@@ -160,10 +161,10 @@ func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error)
            }
            continue
        }
-       return []rune(pick), next, nil
+       return []rune(pick + tail), utf8.RuneCountInString(pick), next, nil
    }
    // Not reached
-   return line, rune(tab), nil
+   return line, pos, rune(tab), nil
 }

 // Prompt displays p, and then waits for user input. Prompt allows line editing
@@ -194,15 +195,12 @@ mainLoop:
            return "", err
        }

-       if pos == len(line) {
-           if key, ok := next.(rune); ok && key == tab {
-               line, next, err = s.tabComplete(p, line)
-               if err != nil {
-                   return "", err
-               }
-               pos = len(line)
-               s.refresh(p, string(line), pos)
+       if key, ok := next.(rune); ok && key == tab {
+           line, pos, next, err = s.tabComplete(p, line, pos)
+           if err != nil {
+               return "", err
            }
+           s.refresh(p, string(line), pos)
        }

        switch v := next.(type) {
gwenn commented 10 years ago

Many thanks for your patch. But to make the completion work, I need the entire line (for example, to find that t is an alias to the table test) and ideally the cursor position (in the line). I know that editline/readline gives access to the entire line with the rl_line_buffer variable and to the cursor position with the rl_point variable. If the Completer function signature must be kept untouched, I've no idea how to make the (line, pos) variables, declared in the Prompt method, accessible to the Completer. Regards.

peterh commented 10 years ago

I was afraid of that.

I'm not willing to break the API. Therefore, the only way to do what you want is to add .SetCompleterWithPos(f CompleterWithPos) (with a better name, of course). Internally, we should only save the WithPos version, and SetCompleter would create a closure that saves the suffix from pos onwards, calls Completer with the prefix, and appends the suffix to all the replies.

I'd be willing to consider a patch that does the above, if you were to submit one.

gwenn commented 10 years ago

Your proposition seems good to me. I will give it a try and send you a patch. Regards.

gwenn commented 10 years ago

Ok, the first draft: No test has been performed to look for possible regression. I don't know if it is a good idea to let the user implements the logic needed to find the "word" to the left of the cursor... I will keep you posted if I find a better solution. Regards.

diff --git a/common.go b/common.go
index b424776..cc93188 100644
--- a/common.go
+++ b/common.go
@@ -19,7 +19,7 @@ type commonState struct {
    terminalSupported bool
    terminalOutput    bool
    history           []string
-   completer         Completer
+   completer         WordCompleter
    columns           int
 }

@@ -99,12 +99,28 @@ func (s *State) getHistoryByPrefix(prefix string) (ph []string) {
    return
 }

-// Completer takes the currently edited line and returns a list
-// of completion candidates.
+// Completer takes the currently edited line content at the left of the cursor
+// and returns a list of completion candidates.
+// If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed
+// to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!".
 type Completer func(line string) []string

+// WordCompleter takes the currently edited line with the cursor position and
+// returns the completion candidates for the partial word to be completed.
+// If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed
+// to the completer which may returns ("Hello, ", {"world", "Word"}) to have "Hello, world!!!".
+type WordCompleter func(line string, pos int) (head string, completions []string)
+
 // SetCompleter sets the completion function that Liner will call to
 // fetch completion candidates when the user presses tab.
 func (s *State) SetCompleter(f Completer) {
+   s.completer = func(line string, pos int) (string, []string) {
+       return "", f(line[:pos])
+   }
+}
+
+// SetWordCompleter sets the completion function that Liner will call to
+// fetch completion candidates when the user presses tab.
+func (s *State) SetWordCompleter(f WordCompleter) {
    s.completer = f
 }
diff --git a/line.go b/line.go
index 53f725f..b77ae77 100644
--- a/line.go
+++ b/line.go
@@ -122,22 +122,24 @@ func (s *State) refresh(prompt string, buf string, pos int) error {
    return err
 }

-func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error) {
+func (s *State) tabComplete(p string, line []rune, pos int) ([]rune, int, interface{}, error) {
    if s.completer == nil {
-       return line, rune(tab), nil
+       return line, pos, rune(tab), nil
    }
-   list := s.completer(string(line))
+   head, list := s.completer(string(line), pos)
    if len(list) <= 0 {
-       return line, rune(tab), nil
+       return line, pos, rune(tab), nil
    }
    listEntry := 0
+   hl := utf8.RuneCountInString(head)
+   tail := string(line[pos:])
    for {
        pick := list[listEntry]
-       s.refresh(p, pick, len(pick))
+       s.refresh(p, head+pick+tail, hl+utf8.RuneCountInString(pick))

        next, err := s.readNext()
        if err != nil {
-           return line, rune(tab), err
+           return line, pos, rune(tab), err
        }
        if key, ok := next.(rune); ok {
            if key == tab {
@@ -149,7 +151,7 @@ func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error)
                continue
            }
            if key == esc {
-               return line, rune(esc), nil
+               return line, pos, rune(esc), nil
            }
        }
        if a, ok := next.(action); ok && a == shiftTab {
@@ -160,10 +162,10 @@ func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error)
            }
            continue
        }
-       return []rune(pick), next, nil
+       return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil
    }
    // Not reached
-   return line, rune(tab), nil
+   return line, pos, rune(tab), nil
 }

 // Prompt displays p, and then waits for user input. Prompt allows line editing
@@ -194,15 +196,12 @@ mainLoop:
            return "", err
        }

-       if pos == len(line) {
-           if key, ok := next.(rune); ok && key == tab {
-               line, next, err = s.tabComplete(p, line)
-               if err != nil {
-                   return "", err
-               }
-               pos = len(line)
-               s.refresh(p, string(line), pos)
+       if key, ok := next.(rune); ok && key == tab {
+           line, pos, next, err = s.tabComplete(p, line, pos)
+           if err != nil {
+               return "", err
            }
+           s.refresh(p, string(line), pos)
        }

        switch v := next.(type) {
peterh commented 10 years ago

I don't know if it is a good idea to let the user implements the logic needed to find the "word" to the left of the cursor...

In general, liner can't tokenize the line, because liner doesn't know how the line should be tokenized. Different applications will have a different grammar.

My question is: Should we allow WordCompleter to erase or otherwise modify the tail?

gwenn commented 10 years ago

Yes, it may be useful. But, for me, only the user can choose between erase or append mode (for example, in Jetbrains IDEA, one uses 'enter' to append or 'tab' to replace). And to make the replace mode works, we need to know where to stop...

peterh commented 10 years ago

If you want to replace part of the tail, you need to replace the whole tail (same problem as in my previous comment. Tokenizing can't be done without the grammar, and I'm against adding a grammar engine to liner).

Given the 2nd sentence in the README, it may not surprise you to discover that I prefer the Linenoise version you've described. It's the only option if you want to replace any part of the tail. Unless I've misread, it sounds like you do want to be able to replace part of the tail.

But since you're the one who will be using it (I'll probably keep using the original Completer), I can be convinced either way.

gwenn commented 10 years ago

There are two similar issues for Linenoise:

And here is a patch where the WordCompleter returns the tail:

diff --git a/common.go b/common.go
index b424776..3ff110f 100644
--- a/common.go
+++ b/common.go
@@ -19,7 +19,7 @@ type commonState struct {
    terminalSupported bool
    terminalOutput    bool
    history           []string
-   completer         Completer
+   completer         WordCompleter
    columns           int
 }

@@ -99,12 +99,28 @@ func (s *State) getHistoryByPrefix(prefix string) (ph []string) {
    return
 }

-// Completer takes the currently edited line and returns a list
-// of completion candidates.
+// Completer takes the currently edited line content at the left of the cursor
+// and returns a list of completion candidates.
+// If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed
+// to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!".
 type Completer func(line string) []string

+// WordCompleter takes the currently edited line with the cursor position and
+// returns the completion candidates for the partial word to be completed.
+// If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed
+// to the completer which may returns ("Hello, ", {"world", "Word"}, "!!!") to have "Hello, world!!!".
+type WordCompleter func(line string, pos int) (head string, completions []string, tail string)
+
 // SetCompleter sets the completion function that Liner will call to
 // fetch completion candidates when the user presses tab.
 func (s *State) SetCompleter(f Completer) {
+   s.completer = func(line string, pos int) (string, []string, string) {
+       return "", f(line[:pos]), line[pos:]
+   }
+}
+
+// SetWordCompleter sets the completion function that Liner will call to
+// fetch completion candidates when the user presses tab.
+func (s *State) SetWordCompleter(f WordCompleter) {
    s.completer = f
 }
diff --git a/line.go b/line.go
index 53f725f..6411263 100644
--- a/line.go
+++ b/line.go
@@ -122,22 +122,23 @@ func (s *State) refresh(prompt string, buf string, pos int) error {
    return err
 }

-func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error) {
+func (s *State) tabComplete(p string, line []rune, pos int) ([]rune, int, interface{}, error) {
    if s.completer == nil {
-       return line, rune(tab), nil
+       return line, pos, rune(tab), nil
    }
-   list := s.completer(string(line))
+   head, list, tail := s.completer(string(line), pos)
    if len(list) <= 0 {
-       return line, rune(tab), nil
+       return line, pos, rune(tab), nil
    }
    listEntry := 0
+   hl := utf8.RuneCountInString(head)
    for {
        pick := list[listEntry]
-       s.refresh(p, pick, len(pick))
+       s.refresh(p, head+pick+tail, hl+utf8.RuneCountInString(pick))

        next, err := s.readNext()
        if err != nil {
-           return line, rune(tab), err
+           return line, pos, rune(tab), err
        }
        if key, ok := next.(rune); ok {
            if key == tab {
@@ -149,7 +150,7 @@ func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error)
                continue
            }
            if key == esc {
-               return line, rune(esc), nil
+               return line, pos, rune(esc), nil
            }
        }
        if a, ok := next.(action); ok && a == shiftTab {
@@ -160,10 +161,10 @@ func (s *State) tabComplete(p string, line []rune) ([]rune, interface{}, error)
            }
            continue
        }
-       return []rune(pick), next, nil
+       return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil
    }
    // Not reached
-   return line, rune(tab), nil
+   return line, pos, rune(tab), nil
 }

 // Prompt displays p, and then waits for user input. Prompt allows line editing
@@ -194,15 +195,12 @@ mainLoop:
            return "", err
        }

-       if pos == len(line) {
-           if key, ok := next.(rune); ok && key == tab {
-               line, next, err = s.tabComplete(p, line)
-               if err != nil {
-                   return "", err
-               }
-               pos = len(line)
-               s.refresh(p, string(line), pos)
+       if key, ok := next.(rune); ok && key == tab {
+           line, pos, next, err = s.tabComplete(p, line, pos)
+           if err != nil {
+               return "", err
            }
+           s.refresh(p, string(line), pos)
        }

        switch v := next.(type) {
peterh commented 10 years ago

Thank you for the patch!

I've rebased your patch on top of mine and pushed it out.