Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Continued pressing of the tab key inserts the next completion suggestion into a line. When all completion suggestions are exhausted, the original word is restored back.
- Loading branch information
1 parent
aa4606e
commit 10448d6
Showing
4 changed files
with
265 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "completions" | ||
|
||
module TTY | ||
class Reader | ||
# Responsible for word completion | ||
# | ||
# @api private | ||
class Completer | ||
# The completion suggestions | ||
attr_reader :completions | ||
|
||
# The handler for finding word completion suggestions | ||
attr_reader :handler | ||
|
||
# The word to complete | ||
attr_reader :word | ||
|
||
# Create a Completer instance | ||
# | ||
# @api private | ||
def initialize(handler: nil) | ||
@handler = handler | ||
@completions = Completions.new | ||
@show_initial = false | ||
@word = "" | ||
end | ||
|
||
# Find a suggestion to complete a word | ||
# | ||
# @param [Line] line | ||
# the line to complete a word in | ||
# | ||
# @return [Boolean, String] | ||
# the completed word or false when no suggestion is found | ||
# | ||
# @api public | ||
def complete(line, initial: false) | ||
initial ? complete_initial(line) : complete_next(line) | ||
end | ||
|
||
# Find suggestions and complete the initial word | ||
# | ||
# @param [Line] line | ||
# the line to complete a word in | ||
# | ||
# @return [Boolean, String] | ||
# the completed word or false when no suggestion is found | ||
# | ||
# @api public | ||
def complete_initial(line) | ||
@word = line.word_to_complete | ||
suggestions = handler.(word) | ||
completions.clear | ||
|
||
return false if suggestions.empty? | ||
|
||
completions.concat(suggestions) | ||
completed_word = completions.get | ||
|
||
line.remove(word.length) | ||
line.insert(completed_word) | ||
|
||
completed_word | ||
end | ||
|
||
# Complete a word with the next suggestion from completions | ||
# | ||
# @param [Line] line | ||
# the line to complete a word in | ||
# | ||
# @return [Boolean, String] | ||
# the completed word or false when no suggestion is found | ||
# | ||
# @api public | ||
def complete_next(line) | ||
return false if completions.empty? | ||
|
||
previous_suggestion = completions.get | ||
if completions.last? && !@show_initial | ||
@show_initial = true | ||
completed_word = word | ||
else | ||
if @show_initial | ||
@show_initial = false | ||
previous_suggestion = word | ||
end | ||
completions.next | ||
completed_word = completions.get | ||
end | ||
|
||
line.remove(previous_suggestion.length) | ||
line.insert(completed_word) | ||
|
||
completed_word | ||
end | ||
end # Completer | ||
end # Reader | ||
end # TTY |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe TTY::Reader, "complete word" do | ||
let(:input) { StringIO.new } | ||
let(:output) { StringIO.new } | ||
let(:env) { {"TTY_TEST" => true} } | ||
let(:left) { "\e[D" } | ||
let(:options) { | ||
{input: input, output: output, env: env, | ||
completion_handler: ->(word) do | ||
@completions.grep(/\A#{Regexp.escape(word)}/) | ||
end} | ||
} | ||
|
||
subject(:reader) { described_class.new(**options) } | ||
|
||
it "finds no completions for a word" do | ||
@completions = %w[aa ab ac] | ||
input << "x" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x\n") | ||
output.rewind | ||
expect(output.string).to eq("\e[2K\e[1Gx\e[2K\e[1Gx\e[2K\e[1Gx\n") | ||
end | ||
|
||
it "completes an empty line with the first suggestion" do | ||
@completions = %w[aa ab ac] | ||
input << "" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("aa\n") | ||
output.rewind | ||
expect(output.string).to eq("\e[2K\e[1Gaa\e[2K\e[1Gaa\n") | ||
end | ||
|
||
it "completes space inside line with the first suggestion" do | ||
@completions = %w[aa ab ac] | ||
input << "x" << " " << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x aa\n") | ||
end | ||
|
||
it "completes a word using the first suggestion" do | ||
@completions = %w[aa ab ac] | ||
input << "x" << " " << "a" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x aa\n") | ||
end | ||
|
||
it "completes a word using the next suggestion" do | ||
@completions = %w[aa ab ac] | ||
input << "x" << " " << "a" << "\t" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x ab\n") | ||
end | ||
|
||
it "completes a word using the last suggestion" do | ||
@completions = %w[aa ab ac] | ||
input << "x " << "a" << "\t" << "\t" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x ac\n") | ||
end | ||
|
||
it "cycles through completions back to the initial word" do | ||
@completions = %w[aa ab ac] | ||
input << "x " << "a" << "\t" << "\t" << "\t" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x a\n") | ||
end | ||
|
||
it "cycles through completions and completes using the first suggestion" do | ||
@completions = %w[aa ab ac] | ||
input << "x " << "a" << "\t" << "\t" << "\t" << "\t" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x aa\n") | ||
end | ||
|
||
it "resets suggestions when a new character is entered" do | ||
@completions = %w[aa ab ac] | ||
input << "x " << "a" << "\t" << "\b" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x aa\n") | ||
end | ||
|
||
it "completes edited text within a line" do | ||
@completions = %w[aa ab ac] | ||
input << "bb" << left << left << " " << left | ||
input << "a" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("aa bb\n") | ||
end | ||
|
||
it "completes within a word" do | ||
@completions = %w[aa ab ac] | ||
input << "x " << "aa" << left << "\t" << "\t" << "\n" | ||
input.rewind | ||
|
||
answer = reader.read_line | ||
|
||
expect(answer).to eq("x aba\n") | ||
end | ||
|
||
it "completes a multiline input " do | ||
@completions = %w[aa ab ac] | ||
input << "a" << "\t" << "\n" | ||
input << "a" << "\t" << "\t" << "\C-d" | ||
input.rewind | ||
|
||
answer = reader.read_multiline | ||
|
||
expect(answer).to eq(%W[aa\n ab]) | ||
end | ||
end |