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

Tab completion NFA DSL #310

Merged
merged 2 commits into from
Jul 3, 2017
Merged

Tab completion NFA DSL #310

merged 2 commits into from
Jul 3, 2017

Conversation

georgebrock
Copy link
Collaborator

#296 introduced a non-deterministic finite automaton as the basis of the tab completion system. This PR is the next step in my grand plan for tab completion: it introduces a DSL for loading the NFA's state graph from a configuration file.

The DSL format is described in the gitsh_completions.5 manual page, which is added in this PR.

The configuration file included in this PR (etc/completions) results in a graph very similar to the one that was hard coded prior to the introduction of the DSL. Subsequent PRs will add features to the DSL so it can be used for more complex graphs that produce better tab completion behaviour.

Note that I'm not using DSL in the sense it's usually used with Ruby: this language has a lexer and a parser, it's not just a pretty looking Ruby API.

rule(/\s+/) { }

rule(/#/) { push_state :comment }
rule(/[^\n]+/, :comment) { }

Choose a reason for hiding this comment

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

Space inside empty braces detected.

rule(/\(/) { :LEFT_PAREN }
rule(/\)/) { :RIGHT_PAREN }
rule(/\s*\n\s*\n/) { :BLANK }
rule(/\s+/) { }

Choose a reason for hiding this comment

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

Space inside empty braces detected.


def build(start_state, options = {})
with_optional_end_state(options) do
parts.inject(start_state) do |state, part|

Choose a reason for hiding this comment

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

Prefer reduce over inject.

Copy link
Contributor

Choose a reason for hiding this comment

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

But that's not what I'm here to tell you about.
I'm here to talk about the draft.
They got a buildin' down in New York City called Whitehall Street, where you
Walk in, you get injected, inspected, detected, infected, neglected and
Selected!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hound config updated.

it 'delegates to the various rule factories' do
start_state = double(:start_state)
rule_factory_1 = stub_rule_factory
rule_factory_2 = stub_rule_factory

Choose a reason for hiding this comment

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

Use normalcase for variable numbers.

describe '#build' do
it 'delegates to the various rule factories' do
start_state = double(:start_state)
rule_factory_1 = stub_rule_factory

Choose a reason for hiding this comment

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

Use normalcase for variable numbers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hound config updated.

result = described_class.parse(tokens(
[:WORD, 'push'], [:BLANK],
[:WORD, 'pull'], [:BLANK], [:BLANK],
[:WORD, 'fetch'], [:EOS],

Choose a reason for hiding this comment

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

Avoid comma after the last parameter of a method call, unless each item is on its own line.


it 'parses multiple rules in the same input' do
result = described_class.parse(tokens(
[:WORD, 'push'], [:BLANK],

Choose a reason for hiding this comment

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

Indent the first parameter one step more than tokens(.

it 'parses rules combining the pipe operator and a post-fix operator' do
result = parse_single_rule(tokens(
[:LEFT_PAREN], [:WORD, 'add'], [:OR], [:WORD, 'commit'], [:RIGHT_PAREN],
[:PLUS], [:EOS],

Choose a reason for hiding this comment

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

Avoid comma after the last parameter of a method call, unless each item is on its own line.


it 'recognises parentheses' do
expect('(add|commit)').to produce_tokens [
'LEFT_PAREN', 'WORD(add)', 'OR', 'WORD(commit)', 'RIGHT_PAREN', 'EOS',

Choose a reason for hiding this comment

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

Avoid comma after the last item of an array, unless each item is on its own line.


it 'recognises the pipe operator' do
expect('stash pop|drop').to produce_tokens [
'WORD(stash)', 'WORD(pop)', 'OR', 'WORD(drop)', 'EOS',

Choose a reason for hiding this comment

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

Avoid comma after the last item of an array, unless each item is on its own line.

end_state_1 = double(:end_state_1)
part_1 = double(:factory, build: end_state_1)
end_state_2 = double(:end_state_2, add_free_transition: nil)
part_2 = double(:factory, build: end_state_2)

Choose a reason for hiding this comment

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

Use normalcase for variable numbers.

end_state = double(:end_state)
end_state_1 = double(:end_state_1)
part_1 = double(:factory, build: end_state_1)
end_state_2 = double(:end_state_2, add_free_transition: nil)

Choose a reason for hiding this comment

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

Use normalcase for variable numbers.

start_state = double(:start_state)
end_state = double(:end_state)
end_state_1 = double(:end_state_1)
part_1 = double(:factory, build: end_state_1)

Choose a reason for hiding this comment

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

Use normalcase for variable numbers.

Copy link
Contributor

@mike-burns mike-burns left a comment

Choose a reason for hiding this comment

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

I don't love GITSH_CONFIG_DIRECTORY, but I also don't want to make you pass a string through a series of method calls.

Overall: LGTM!

factory = Parser.parse(tokens, gitsh_env: env)
factory.build(start_state)
rescue Errno::ENOENT
end
Copy link
Contributor

Choose a reason for hiding this comment

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

The silence here makes me suspect that we want a conditional elsewhere instead. I feel like DSL.load should complain about a missing file, but that DSL.load should only be called if we want such a complaint ... if that makes sense.

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 could move the rescue up a level to the Gitsh::TabCompletion::AutomatonFactory object?

I don't think I want a missing configuration file to cause an error that the user sees, but maybe it should—without the file there will be no tab completion.


def build(start_state, options = {})
with_optional_end_state(options) do
parts.inject(start_state) do |state, part|
Copy link
Contributor

Choose a reason for hiding this comment

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

But that's not what I'm here to tell you about.
I'm here to talk about the draft.
They got a buildin' down in New York City called Whitehall Street, where you
Walk in, you get injected, inspected, detected, infected, neglected and
Selected!

@@ -406,6 +406,13 @@ The
.Pa .gitshrc
file will not be loaded for non-interactive sessions, e.g. when gitsh is
invoked with the path to a script file.
.It Pa @prefix@/etc/gitsh/completions
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe @pkgsysconfdir@ instead of @prefix@ is more accurate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call.

file consists of rules defining the arguments that are expected
by various commands.
.Xr gitsh 1
uses the rules to match the input before the word being completed,
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably no comma.

.Pp
Each rule is separated from other rules by a blank line.
.Pp
The rule specifies the name of the command,
Copy link
Contributor

Choose a reason for hiding this comment

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

No comma.

def stub_text_matcher(text)
klass = Gitsh::TabCompletion::Matchers::TextMatcher
matcher = instance_double(klass)
allow(klass).to receive(:new).with(text).and_return(matcher)
Copy link
Contributor

Choose a reason for hiding this comment

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

#with makes this into a mock instead of a stub, right?

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 never get those distinctions right, but you're probably right.

end

def write_temp_completions_file(completions)
temp_file('gitsh_completions', completions).path
Copy link
Contributor

Choose a reason for hiding this comment

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

How's this work -- do these files get cleaned up?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. It uses the Ruby standard library's Tempfile class behind the scenes, so it should be cleaned up when the object is garbage collected.

@georgebrock
Copy link
Collaborator Author

I don't love GITSH_CONFIG_DIRECTORY, but I also don't want to make you pass a string through a series of method calls.

It might not be too bad: the file it's current defined in would pass it to Gitsh::CLI, to Gitsh::InputStrategies::Interactive, to Gitsh::TabCompletion::Facade, to Gitsh::TabCompletion::AutomatonFactory where it's used.

@georgebrock
Copy link
Collaborator Author

Thinking about this configuration thing some more: we already pass the Gitsh::Environment instance around all of these places, and it's already able to hold variables. If we put this path in a variable, that would allow people to override it in their .gitshrc file.

it 'builds a graph of automaton states by reading the given file' do
path = write_temp_completions_file([
'stash (apply|drop|pop|show)',
].join("\n"))

Choose a reason for hiding this comment

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

Put a comma after the last parameter of a multiline method call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Err … this isn't quite what I intended. Rubocop seems to struggle with nested multiline things.

[:WORD, 'push'], [:BLANK],
[:WORD, 'pull'], [:BLANK], [:BLANK],
[:WORD, 'fetch'], [:EOS],
), gitsh_env: double(:env))

Choose a reason for hiding this comment

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

Put a comma after the last parameter of a multiline method call.

result = parse_single_rule(tokens(
[:LEFT_PAREN], [:WORD, 'add'], [:OR], [:WORD, 'commit'], [:RIGHT_PAREN],
[:PLUS], [:EOS],
))

Choose a reason for hiding this comment

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

Put a comma after the last parameter of a multiline method call.

result = parse_single_rule(tokens(
[:LEFT_PAREN], [:WORD, 'stash'], [:WORD, 'pop'], [:OR],
[:WORD, 'add'], [:RIGHT_PAREN], [:EOS],
))

Choose a reason for hiding this comment

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

Put a comma after the last parameter of a multiline method call.

result = parse_single_rule(tokens(
[:LEFT_PAREN], [:WORD, 'commit'], [:OR],
[:WORD, 'add'], [:OR], [:VAR, 'path'], [:RIGHT_PAREN], [:EOS],
))

Choose a reason for hiding this comment

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

Put a comma after the last parameter of a multiline method call.

it 'parses rules with multiple words and variables' do
result = parse_single_rule(tokens(
[:WORD, 'stash'], [:WORD, 'pop'], [:VAR, 'revision'], [:EOS],
))

Choose a reason for hiding this comment

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

Put a comma after the last parameter of a multiline method call.

@@ -4,6 +4,9 @@ module Gitsh
module TabCompletion
module Matchers
class PathMatcher < BaseMatcher
def initialize(_env)

Choose a reason for hiding this comment

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

Put empty method definitions on a single line.

@@ -6,8 +6,9 @@
module Gitsh
class Environment
DEFAULT_GIT_COMMAND = '/usr/bin/env git'.freeze
DEFAULT_CONFIG_DIRECTORY = '/usr/local/etc/gitsh'

Choose a reason for hiding this comment

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

Freeze mutable objects assigned to constants.

@georgebrock
Copy link
Collaborator Author

@mike-burns I think 3a76b3f addresses your concerns about the global constant.

@mike-burns
Copy link
Contributor

Sure does!

LGTM.

This commit makes the Rubocop config used by Hound more consistent with the
existing project style.
This commit introduces a domain specific language (DSL) to define the state
graph for the tab completion automaton. It also replaces the hard-coded
state graph with an equivalent graph loaded from a configuration file.

The details of the DSL are given in the gitsh_completions(5) manual page
which is added in this commit.

The DSL will need to be expanded somewhat to do everything we want the tab
completion system to be able to do, but this is a good step in that
direction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants