-
Notifications
You must be signed in to change notification settings - Fork 118
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
Rename current completor to RegexpCompletor and refactored for future extension #707
Conversation
5bcb9be
to
bb385c8
Compare
bb385c8
to
4e2232f
Compare
6a7abce
to
063aad2
Compare
06f71b7
to
107d4e2
Compare
lib/irb/completion.rb
Outdated
@@ -128,35 +140,42 @@ def self.retrieve_files_to_require_relative_from_current_dir | |||
if tok && tok.event == :on_ident && tok.state == Ripper::EXPR_CMDARG | |||
case tok.tok | |||
when 'require' | |||
result = retrieve_files_to_require_from_load_path.select { |path| | |||
result = BaseCompletor.retrieve_files_to_require_from_load_path.select { |path| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any benefit of having these retrieve_*
methods as class methods. Let's define these methods as instance methods too so we can simply call it here and memoize it on this instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since completor is created many times, memoizing in instance will make this cache disappear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the benefit of 1) initialising a new completer in every new invocation over 2) initialising it once and pass required arguments to it (like #711)?
IMO, using 2) would make memoizing easier to maintain and understand as we can cache the files in its ivars.
Also, using parent class' class methods for caching feels a bit anti-pattern to me 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example of not recreating a completor instance should be like this:
# Full spec. Reset when binding is modified.
class Completor
def reset_cache
# Invoked when binding.eval(something) is executed in IRB.
# All cache should be invalidated because lvars, ivars and other might be changed.
# Calculation result might change even if `binding` `preposing` `target` `postposing` is the same value.
# For example, `Readline=Reline` will change the completion canidates of `Readline.a`
end
def completion_candidates(preposing, target, postposing, binding)
cache.fetch([preposing, target, postposing]) { calculate_candidates }
end
def doc_namespace(preposing, matched_target, postposing, binding)
cache.fetch([preposing, target, postposing]) { calculate_doc }
end
end
# Simplified spec. Reset when one of preposing, target, postposing is changed or binding is modified.
class Completor
def reset_and_cache(preposing, target, postposing, binding)
# Always invalidate old cache and cache the parameters
end
def completion_candidates
# Use cached value or recalculate from cached parameters
end
def doc_namespace(matched_target)
# Use cached value or recalculate from cached parameters
end
end
class Completor
def completion_candidates(preposing, target, postposing, binding)
# Should always cache, always invalidate old cache
end
def doc_namespace(matched_target)
# Should use the cache
end
end
class Completor
def completion_candidates(preposing, target, postposing, binding)
# Don't cache
end
def doc_namespace(preposing, matched_target, postposing, binding)
# Always recalculate
end
end
For reset_cache()
and reset_and_cache(value_to_cache)
pattern, I think recreating an instance is better than implementing reset for an object.
For caching-in-completion_candidates pattern, there are implicit rules that completion_candidates
should always invalidate old cache, doc_namespace can always use the cache.
Recreating an instance instead of resetting an instance will simplify it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any "production" use case for resetting cache. If it's simply for testing, I think we can just memoise with ivar and recreate instances in test teardown. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reset is for production. not for test.
irb(main):001> a=apple=1
#=> 1
irb(main):002> a # complete('a','','') #=> ["a", "apple", "at_exit", ...]
irb(main):002> a. # complete('a.','','') #=> ["a.abs", "a.allbits?", ...]
irb(main):002> a.t # complete('a.t','','') #=> ["a.to_s", "a.to_int", ...]
irb(main):002> a.to_s # doc('a.to_s') #=> "Integer#to_s"
irb(main):002> a=:a;a.t # complete('a.t','a=:a;','') #=> ["a.to_s", "a.to_sym", "]
irb(main):002> a=:a;a.to_s # doc('a.to_s') #=> "Symbol#to_s"
irb(main):002> a=avocado='!'
#=> "!" # reset
irb(main):003> a # complete('a','','') #=> ["a", "apple", "avocado", ...]
irb(main):003> a. # complete('a.','','') #=> ["a.ascii_only?", "a.b", ...]
irb(main):003> a.t # complete('a.t','','') #=> ["a.to_s", "a.to_str", ...]
irb(main):003> a.to_s # doc('a.to_s') #=> "String#to_i"
Reset (or re-initialize) is needed because:
After calculating completion, doc('a.to_s')
will change from "Integer#to_s"
to "Symbol#to_s"
.
After evaluating input, complete('a','','')
will change from ["a", "apple", "at_exit", ...]
to ["a", "apple", "avocado", ...]
However, I think we can first implement with not recreating instance, and consider whether to change later. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I think I didn't specify what I meant by caching.
I think to match the current behaviour, we only need to cache the result of retrieve_files_to_require_from_load_path
in ivars?
So I prefer recreating instances with this spec:
class Completor
def completion_candidates(preposing, target, postposing, binding)
# Don't cache
end
def doc_namespace(preposing, matched_target, postposing, binding)
# Always recalculate
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
I'll update this pull request with that later
lib/irb/input-method.rb
Outdated
[226, 136, 130] # The "∂" that appears when Alt+d in pressed on iTerm2. | ||
] | ||
def show_doc_dialog_proc | ||
get_current_completor = -> { @completor } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't we simply do completor = @completor
and call it from the proc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is needed to get the latest completor value.
Completor is created every time completion_candidates is calculated and reused only for doc_namespace calculation.
This is to make cache lifetime of [preposing, target, postposing] to match completor instance lifetime and make completor's implementation simple.
} | ||
end | ||
|
||
def display_document(matched, driver: nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice extraction 👍
I'd like to store the driver in RelineInputMethod
because it doesn't need to be initialised every time we show the doc, and memoise it with lazy initialisation makes testing complicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For performance, I think storing driver is better but not important here.
RDoc::RI::Driver.new
takes 0.18sec for the first time and after that, takes 0.035sec.
In here, driver is only initialized when user press TAB key twice to explicitly show document in noautocomplete mode. I think 0.035sec is acceptable.
The problem is more with document dialog of autocomplete mode. It does not cache driver and it takes each 0.035sec while scrolling completion dialog by TAB key.
SHOW_DOC_DIALOG calling RDoc::RI::Driver.new (in branch master)
Lines 291 to 293 in 1d2d35d
options = {} | |
options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty? | |
driver = RDoc::RI::Driver.new(options) |
Creating RDoc::RI::Driver.new
at startup will slowdown IRB's startup time, so lazily initialize it and use it from both show_doc_dialog_proc
and display_document
might be better. (maybe after this pull request?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch on the performance penalty of initialising it repeatedly in dialog 👍
That said, I don't think 0.035
sec at the startup time would be too big of an issue. Storing it in the constructor makes the dependency easy to understand, which outweighs the minor delay IMO.
But I agree we should deal with this in another PR, and also let's do a proper profiling on IRB startup?
…ed to implement `completion_caididates` and `doc_namespace`
107d4e2
to
f618538
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there, just 1 question left 👍
lib/irb/input-method.rb
Outdated
Readline.completion_proc = IRB::InputCompletor::CompletionProc | ||
Readline.completion_proc = ->(target) { | ||
bind = IRB.conf[:MAIN_CONTEXT].workspace.binding | ||
RegexpCompletor.new.completion_candidates('', target, '', bind: bind) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't we store the completor in ivar for ReadlineInput
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch! done
} | ||
end | ||
|
||
def display_document(matched, driver: nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch on the performance penalty of initialising it repeatedly in dialog 👍
That said, I don't think 0.035
sec at the startup time would be too big of an issue. Storing it in the constructor makes the dependency easy to understand, which outweighs the minor delay IMO.
But I agree we should deal with this in another PR, and also let's do a proper profiling on IRB startup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the great work 👏
refactored for future extension (ruby/irb#707) * Move completion implementation to completion/regexp_completor for future extension * Remove constant CompletionProc and PerfectMatchedProc and add a class method * Move document display logic to InputCompletor. Each completor only need to implement `completion_caididates` and `doc_namespace` * Move display_document logic to RelineInputMethod * Use RegexpCompletor directly. Not through class method of InputCompletor. * RegexpCompletor extends BaseCompletor, move back definition to completion.rb * Move display_document test to input_method test * Stop re-initialize completor on each completion phase * Store completor to ReadlineInputMethod's iver ruby/irb@1e98521483
Current completion will be renamed to RegexpCompletor and another type of completor will be added in the future.
Completor needs to implement
IRB::InputCompletor::CompletionProc
IRB::InputCompletor::PerfectMatchedProc
IRB::RelineInputMethod::SHOW_DOC_DIALOG
is removed and refactored.## Requirements These utility methods is needed in each completor so leave it in- First, completion candidates is calculated from target, preposing, postposing and binding.
- After that, display_document or doc_namespace calculation(show document in dialog) is performed from matched(≒target). preposing and postposing is not available in this phase.
InputCompletorBaseCompletor. - BaseCompletor.retrieve_files_to_require_from_load_path - BaseCompletor.retrieve_files_to_require_relative_from_current_dirThe calculation order is:
For advanced completor, display_document and doc_namespace needs preposing, postposing and binding information. Each completor implementation can cache them in completion_candidates phase, but I want to encapsulate it in InputCompletor so that each completor implementation doesn't need to know about calculation order hack.
Displaying document logic is not completor specific. It should be implemented in InputCompletor by using the result of
completor.doc_namespace(matched)
.Conclusion
Each completor implements
completion_candidates
anddoc_namespace
method.Instance of completor is created just before completion_candidates calculation and reused in doc_namespace calculation.
Completor is initialized with target, preposing, postposing and bining.
In completion calculation phase, InputMethod calls
Completor.new(target, preposing, postposing, bind:)
andcompletor.completion_candidates()
. RelineInputMethod will cache the instance.In document namespace calculation phase, RelineInputMethod calls
completor.doc_namespace(target)
display_document logic (only for noautocomplete mode) is moved from
InputCompletor::PerfectMatchedProc
toRelineInputMethod#display_document
. It usescompletor.doc_namespace(matched)
Completor needs to implement
Example of parameters passed for code
if 1.ab
Parameters passed to constructor are
target='1.ab', preposing='if ', postposing=''
and binding.completor.completion_candidates
returns['1.abs', '1.abs2']
.matched
passed todoc_namespace
is'1.abs'
or'1.abs2'
. Not'1.ab'
.