Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support tab completion for variables. #304

Merged
merged 1 commit into from
Jun 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/gitsh/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ def fetch(key, force_default_git_command = false, &block)
raise Gitsh::UnsetVariableError, "Variable '#{key}' is not set"
end

def available_variables
(
magic_variables.available_variables +
variables.keys +
repo.available_config_variables
).uniq.sort
end

def config_variables
Hash[variables.select { |key, value| key.to_s.include?('.') }]
end
Expand Down
20 changes: 20 additions & 0 deletions lib/gitsh/git_repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ def config(name, force_default_git_command = false)
end
end

def available_config_variables
modern_git_available_config_variables ||
old_git_available_config_variables
end

def config_color(name, default)
git_output(
"config --get-color #{Shellwords.escape(name)} #{Shellwords.escape(default)}"
Expand Down Expand Up @@ -118,6 +123,21 @@ def abbreviated_sha
end
end

def modern_git_available_config_variables
command = git_command("config --list --name-only")
out, _, status = Open3.capture3(command)

if status.success?
out.lines.map { |line| line.chomp.to_sym }
end
end

def old_git_available_config_variables
git_output('config --list').lines.map do |line|
line.split('=').first.to_sym
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use line.upto('=').to_sym to avoid scanning the entire line.

Copy link
Collaborator Author

@georgebrock georgebrock Jun 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly, that's not what String#upto does (e.g. 'a'.upto('d') produces an enumerator over ['a', 'b', 'c', 'd']). Your version seems more useful though.

I could use split('=', 2).first, that should avoid scanning the whole thing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm...given that #split still returns an array in that case, it's unclear whether that's improving things much. The best alternative I could come up with was

line.each_char.take_while { |c| c != "-" }.join

But given that config lines aren't usually very long (I assume), it's probably not a big deal either way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll stick with split.first.

end
end

def git_output(command)
Open3.capture3(git_command(command)).first.chomp
end
Expand Down
8 changes: 4 additions & 4 deletions lib/gitsh/magic_variables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ def fetch(key)
end
end

private

attr_reader :repo

def available_variables
private_methods(false).grep(/^_/)
end

private

attr_reader :repo

def _prior
repo.revision_name('@{-1}') ||
raise(UnsetVariableError, 'No prior branch')
Expand Down
13 changes: 11 additions & 2 deletions lib/gitsh/tab_completion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ has typed so far.
The basis of gitsh's tab completion system is a slightly extended
Non-deterministic Finite Automaton (NFA).

Completing variable names doesn't use the NFA, but all other types of completion
do.

### Finite Automata

In a typical NFA, the automaton consists of a set of states joined by
Expand Down Expand Up @@ -104,10 +107,12 @@ so we'd offer file paths from the current directory as completions.
- `Gitsh::TabCompletion::Facade` provides a single interface to the rest of
gitsh. It's the only class inside the `TabCompletion` module that should be
referenced elsewhere. Its `#call` method is the entry point for generating
completions.
completions. It also decides if the `CommandCompleter` or `VariableCompleter`
should be invoked.

- `Gitsh::TabCompletion::Context` uses the `Gitsh::Lexer` to break up the user's
input into a series of words, so we know what to pass to the automaton.
input into a series of words, so we know what to pass to the automaton, or if
the input ends with a variable name.

- `Gitsh::TabCompletion::Automaton` implements the Non-deterministic Finite
Automaton (NFA).
Expand All @@ -123,3 +128,7 @@ so we'd offer file paths from the current directory as completions.

- `Gitsh::TabCompletion::CommandCompleter` orchestrates the interaction of the
various other parts.

- `Gitsh::TabCompletion::VariableCompleter` provides an alternative to
`CommandCompleter`. This doesn't use the automaton, and only completes
variable names.
16 changes: 16 additions & 0 deletions lib/gitsh/tab_completion/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Context
COMMAND_SEPARATORS = [
:AND, :OR, :SEMICOLON, :LEFT_PAREN, :SUBSHELL_START, :EOL,
].freeze
NOT_MEANINGFUL = [:EOS, :INCOMPLETE].freeze

def initialize(input)
@input = input
Expand All @@ -15,6 +16,10 @@ def prior_words
words[0...-1]
end

def completing_variable?
[:VAR, :MISSING].include?(last_meaningful_token.type)
end

private

attr_reader :input
Expand All @@ -28,6 +33,8 @@ def combine_words(token_groups)
tokens.inject("") do |result, token|
if token.type == :WORD
result + token.value
elsif token.type == :VAR
result + "${#{token.value}}"
else
result
end
Expand All @@ -53,7 +60,16 @@ def last_command(tokens)
reverse
end

def last_meaningful_token
tokens.reverse_each.
detect { |token| !NOT_MEANINGFUL.include?(token.type) }
end

def tokens
@_tokens ||= lex
end

def lex
Lexer.lex(input)
rescue RLTK::LexingError
[]
Expand Down
11 changes: 10 additions & 1 deletion lib/gitsh/tab_completion/facade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'gitsh/tab_completion/command_completer'
require 'gitsh/tab_completion/context'
require 'gitsh/tab_completion/escaper'
require 'gitsh/tab_completion/variable_completer'

module Gitsh
module TabCompletion
Expand All @@ -13,7 +14,11 @@ def initialize(line_editor, env)

def call(input)
context = Context.new(line_editor.line_buffer)
command_completions(context, input)
if context.completing_variable?
variable_completions(input)
else
command_completions(context, input)
end
end

private
Expand All @@ -30,6 +35,10 @@ def command_completions(context, input)
).call
end

def variable_completions(input)
VariableCompleter.new(line_editor, input, env).call
end

def automaton
@automaton ||= AutomatonFactory.build(env)
end
Expand Down
51 changes: 51 additions & 0 deletions lib/gitsh/tab_completion/variable_completer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module Gitsh
module TabCompletion
class VariableCompleter
def initialize(line_editor, input, env)
@line_editor = line_editor
@input = input
@env = env
end

def call
line_editor.completion_append_character = completion_append_character
line_editor.completion_suppress_quote = true

matches
end

private

attr_reader :line_editor, :input, :env

def completion_append_character
if prefix.end_with?('{')
'}'
else
nil
end
end

def matches
env.available_variables.
select { |name| name.to_s.start_with?(partial_name) }.
map { |name| "#{prefix}#{name}" }
end

def prefix
parse_input.first
end

def partial_name
parse_input.last
end

def parse_input
@parse_input ||= (
parts = input.rpartition(/\$\{?/)
[parts[0...-1].join, parts.last]
)
end
end
end
end
10 changes: 10 additions & 0 deletions spec/integration/tab_completion_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@
end
end

it 'completes variables' do
GitshRunner.interactive do |gitsh|
gitsh.type(':set greeting "Hello, world"')
gitsh.type(":echo $gre\t")

expect(gitsh).to output_no_errors
expect(gitsh).to output(/Hello, world/)
end
end

it 'completes after punctuation' do
GitshRunner.interactive do |gitsh|
gitsh.type('init')
Expand Down
24 changes: 24 additions & 0 deletions spec/units/environment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@
end
end

describe '#available_variables' do
it 'returns the names of all available variables' do
repository = double('GitRepository')
allow(repository).to receive(:available_config_variables).
and_return([:'user.name'])
factory = double('RepositoryFactory', new: repository)
magic_variables = double('MagicVariables')
allow(magic_variables).to receive(:available_variables).
and_return([:_prior])
env = described_class.new(
magic_variables: magic_variables,
repository_factory: factory,
)
env[:foo] = 'bar'
env['user.name'] = 'Config Override'

expect(env.available_variables).to eq [
:_prior,
:foo,
:'user.name',
]
end
end

describe '#clone' do
it 'creates a copy with an isolated set of variables' do
original = described_class.new
Expand Down
14 changes: 14 additions & 0 deletions spec/units/git_repository_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,20 @@
end
end

describe '#available_config_variables' do
it 'returns a list of all Git configuration variables' do
with_a_temporary_home_directory do
in_a_temporary_directory do
repo = described_class.new(env)
run 'git init'
run 'git config --local user.name "Grace Hopper"'

expect(repo.available_config_variables).to include(:'user.name')
end
end
end
end

context '#config_color' do
context 'when the config variable is set' do
it 'returns a color code for the color described by the setting' do
Expand Down
14 changes: 14 additions & 0 deletions spec/units/magic_variables_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,18 @@
end
end
end

describe '#available_variables' do
it 'returns an array of variable names' do
repo = double('GitRepository')
magic_variables = described_class.new(repo)

expect(magic_variables.available_variables).to match_array [
:_prior,
:_merge_base,
:_rebase_base,
:_root,
]
end
end
end
22 changes: 22 additions & 0 deletions spec/units/tab_completion/context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
expect(context.prior_words).to eq %w(stash drop)
end

it 'includes variables' do
context = described_class.new(':echo "name=$user.name" "email=')
expect(context.prior_words).to eq [':echo', 'name=${user.name}']
end

it 'only considers the current command' do
context = described_class.new('stash apply my-stash && stash drop my-')
expect(context.prior_words).to eq %w(stash drop)
Expand Down Expand Up @@ -42,4 +47,21 @@
end
end
end

describe '#completing_variable?' do
it 'returns true when the command ends with a variable' do
expect(described_class.new(':echo $my_va')).to be_completing_variable
expect(described_class.new(':echo "$my_va')).to be_completing_variable
expect(described_class.new(':echo ${my_va')).to be_completing_variable
expect(described_class.new(':echo $')).to be_completing_variable
expect(described_class.new(':echo ${')).to be_completing_variable
end

it 'returns false when the command does not end with a variable' do
expect(described_class.new(':echo hello')).not_to be_completing_variable
expect(described_class.new(':echo $my_var ')).not_to be_completing_variable
expect(described_class.new(':echo \'$varish')).not_to be_completing_variable
expect(described_class.new(':echo \'$')).not_to be_completing_variable
end
end
end
Loading