-
-
Notifications
You must be signed in to change notification settings - Fork 32
fuzzy completions #308
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
fuzzy completions #308
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,23 @@ | ||
| ### baseline completions ### | ||
| import REPL.REPLCompletions, FuzzyCompletions | ||
|
|
||
| const CompletionSuggetion = Dict{Symbol, String} | ||
|
|
||
| # as an heuristic, suppress completions if there are over 500 completions, | ||
| # ref: currently `completions("", 0, Main)` returns 1098 completions as of v1.5 | ||
| const SUPPRESS_COMPLETION_THRESHOLD = 500 | ||
|
|
||
| # autocomplete-plus only shows at most 200 completions | ||
| # ref: https://github.com/atom/autocomplete-plus/blob/master/lib/suggestion-list-element.js#L49 | ||
| const MAX_COMPLETIONS = 200 | ||
|
|
||
| const DESCRIPTION_LIMIT = 200 | ||
|
|
||
| # threshold up to which METHOD_COMPLETION_CACHE can get fat, to make sure it won't eat up memory | ||
| # NOTE: with 2000 elements it used ~10MiB on my machine. | ||
| const MAX_METHOD_COMPLETION_CACHE = 3000 | ||
|
|
||
| # baseline completions | ||
| # -------------------- | ||
|
|
||
| handle("completions") do data | ||
| @destruct [ | ||
|
|
@@ -12,40 +31,27 @@ handle("completions") do data | |
| startRow || 0, | ||
| column || 1, | ||
| # configurations | ||
| is_fuzzy || true, | ||
aviatesk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| force || false | ||
| ] = data | ||
|
|
||
| withpath(path) do | ||
| basecompletionadapter( | ||
| # general | ||
| line, mod, | ||
| # local context | ||
| context, row - startRow, column, | ||
| # configurations | ||
| force | ||
| ) | ||
| end | ||
| adapter = is_fuzzy ? fuzzycompletionadapter : replcompletionadapter | ||
| return adapter( | ||
| # general | ||
| line, mod, | ||
| # local context | ||
| context, row - startRow, column, | ||
| # configurations | ||
| force | ||
| )::Vector{CompletionSuggetion} | ||
| end | ||
|
|
||
| using REPL.REPLCompletions | ||
| # NOTE: | ||
| # `replcompletionadapter` and `fuzzycompletionadapter` are really similar | ||
| # and maybe better to be refactored into a single function, | ||
| # but we define them as separate functions for type-stabilities for now | ||
|
|
||
| const CompletionSuggetion = Dict{Symbol, String} | ||
|
|
||
| # as an heuristic, suppress completions if there are over 500 completions, | ||
| # ref: currently `completions("", 0)` returns **1132** completions as of v1.3 | ||
| const SUPPRESS_COMPLETION_THRESHOLD = 500 | ||
|
|
||
| # autocomplete-plus only shows at most 200 completions | ||
| # ref: https://github.com/atom/autocomplete-plus/blob/master/lib/suggestion-list-element.js#L49 | ||
| const MAX_COMPLETIONS = 200 | ||
|
|
||
| const DESCRIPTION_LIMIT = 200 | ||
|
|
||
| # threshold up to which METHOD_COMPLETION_CACHE can get fat, to make sure it won't eat up memory | ||
| # NOTE: with 2000 elements it used ~10MiB on my machine. | ||
| const MAX_METHOD_COMPLETION_CACHE = 3000 | ||
|
|
||
| function basecompletionadapter( | ||
| function replcompletionadapter( | ||
| # general | ||
| line, m = "Main", | ||
| # local context | ||
|
|
@@ -56,22 +62,18 @@ function basecompletionadapter( | |
| mod = getmodule(m) | ||
|
|
||
| cs, replace, shouldcomplete = try | ||
| completions(line, lastindex(line), mod) | ||
| REPLCompletions.completions(line, lastindex(line), mod) | ||
| catch err | ||
| # might error when e.g. type inference fails | ||
| REPLCompletions.Completion[], 1:0, false | ||
| end | ||
| prefix = line[replace] | ||
|
|
||
| # suppress completions if there are too many of them unless activated manually | ||
| # e.g. when invoked with `$|`, `(|`, etc. | ||
| # TODO: check whether `line` is a valid text to complete in frontend | ||
| if !force && length(cs) > SUPPRESS_COMPLETION_THRESHOLD | ||
| cs = REPLCompletions.Completion[] | ||
| replace = 1:0 | ||
| end | ||
| # suppress completions if there are still too many of them i.e. when invoked with `$|`, `(|`, etc. | ||
| # XXX: the heuristics below mayn't be so robust to work for all the cases. | ||
| !force && (isempty(prefix) && length(cs) > SUPPRESS_COMPLETION_THRESHOLD) && (cs = REPLCompletions.Completion[]) | ||
|
|
||
| # initialize suggestions with local completions so that they show up first | ||
| prefix = line[replace] | ||
| comps = if force || !isempty(prefix) | ||
| filter!(let p = prefix | ||
| c -> startswith(c[:text], p) | ||
|
|
@@ -87,7 +89,7 @@ function basecompletionadapter( | |
| end | ||
| end | ||
|
|
||
| cs = cs[1:min(end, MAX_COMPLETIONS - length(comps))] | ||
| @inbounds cs = cs[1:min(end, MAX_COMPLETIONS - length(comps))] | ||
| afterusing = REPLCompletions.afterusing(line, Int(first(replace))) # need `Int` for correct dispatch on x86 | ||
| for c in cs | ||
| if afterusing | ||
|
|
@@ -99,6 +101,69 @@ function basecompletionadapter( | |
| return comps | ||
| end | ||
|
|
||
| function fuzzycompletionadapter( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems fairly similar to the plus have an
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hm, then the code after I haven't taken any actual benchmark, but I would like to endure the verbose code for performance here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm fair enough then. I don't think the dynamic dispatch would make much of a difference, but let's just keep this as-is then.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yeah maybe. I may be too nervous about dynamic dispatches.. |
||
| # general | ||
| line, m = "Main", | ||
| # local context | ||
| context = "", row = 1, column = 1, | ||
| # configurations | ||
| force = false | ||
| ) | ||
| mod = getmodule(m) | ||
|
|
||
| cs, replace, shouldcomplete = try | ||
| FuzzyCompletions.completions(line, lastindex(line), mod) | ||
| catch err | ||
| # might error when e.g. type inference fails | ||
| FuzzyCompletions.Completion[], 1:0, false | ||
| end | ||
| prefix = line[replace] | ||
|
|
||
| if !force | ||
| filter!(c -> FuzzyCompletions.score(c) ≥ 0, cs) # filter negative scores | ||
|
|
||
| # suppress completions if there are still too many of them i.e. when invoked with `$|`, `(|`, etc. | ||
| # XXX: the heuristics below mayn't be so robust to work for all the cases. | ||
| (isempty(prefix) && length(cs) > SUPPRESS_COMPLETION_THRESHOLD) && (cs = FuzzyCompletions.Completion[]) | ||
| end | ||
|
|
||
| # initialize suggestions with local completions so that they show up first | ||
| comps = localcompletions(context, row, column, prefix) | ||
| if !force | ||
| filter!(let p = prefix | ||
| # NOTE: let's be a bit strict on local completions so that we avoid verbose completions, e.g. troublemaker cases | ||
| comp -> FuzzyCompletions.fuzzyscore(p, comp[:text]) > 0 | ||
| end, comps) | ||
| end | ||
|
|
||
| # FIFO cache refreshing | ||
| if length(METHOD_COMPLETION_CACHE) ≥ MAX_METHOD_COMPLETION_CACHE | ||
| for k in collect(keys(METHOD_COMPLETION_CACHE))[1:MAX_METHOD_COMPLETION_CACHE÷2] | ||
| delete!(METHOD_COMPLETION_CACHE, k) | ||
| end | ||
| end | ||
|
|
||
| @inbounds cs = cs[1:min(end, MAX_COMPLETIONS - length(comps))] | ||
| afterusing = FuzzyCompletions.afterusing(line, Int(first(replace))) # need `Int` for correct dispatch on x86 | ||
| for c in cs | ||
| if afterusing | ||
| c isa FuzzyCompletions.PackageCompletion || continue | ||
| end | ||
| push!(comps, completion(mod, c, prefix)) | ||
| end | ||
|
|
||
| return comps | ||
| end | ||
|
|
||
| # for functions below works both for REPLCompletions and FuzzyCompletions modules | ||
| for c in [:KeywordCompletion, :PathCompletion, :ModuleCompletion, :PackageCompletion, | ||
| :PropertyCompletion, :FieldCompletion, :MethodCompletion, :BslashCompletion, | ||
| :ShellCompletion, :DictCompletion] | ||
| eval(:(const $c = Union{REPLCompletions.$c, FuzzyCompletions.$c})) | ||
| end | ||
| completion_text(c::REPLCompletions.Completion) = REPLCompletions.completion_text(c) | ||
| completion_text(c::FuzzyCompletions.Completion) = FuzzyCompletions.completion_text(c) | ||
|
|
||
| completion(mod, c, prefix) = CompletionSuggetion( | ||
| :replacementPrefix => prefix, | ||
| # suggestion body | ||
|
|
@@ -119,21 +184,21 @@ const MethodCompletionDetail = NamedTuple{(:rt,:desc),Tuple{String,String}} | |
| MethodCompletionInfo = NamedTuple{(:f,:m,:tt),Tuple{Any,Method,Type}} | ||
| MethodCompletionDetail = NamedTuple{(:rt,:desc),Tuple{String,String}} | ||
|
|
||
| [`OrderedCollections.OrderedDict`](@ref) that caches details of `REPLCompletions.MethodCompletion`s: | ||
| [`OrderedCollections.OrderedDict`](@ref) that caches details of [`MethodCompletion`](@ref)s: | ||
| - result of a return type inference | ||
| - documentation description | ||
|
|
||
| This cache object has the following structure: | ||
| Keys are hash `String` of a `REPLCompletions.MethodCompletion` object, | ||
| Keys are hash `String` of a `MethodCompletion` object, | ||
| which can be used for cache detection or refered lazily by | ||
| [autocompluete-plus's `getSuggestionDetailsOnSelect` API] | ||
| (https://github.com/atom/autocomplete-plus/wiki/Provider-API#defining-a-provider) | ||
| Values can be either of: | ||
| - `MethodCompletionInfo`: keeps (temporary) information of this `REPLCompletions.MethodCompletion` | ||
| - `MethodCompletionInfo`: keeps (temporary) information of this `MethodCompletion` | ||
| + `f`: a function object | ||
| + `m`: a `Method` object | ||
| + `tt`: an input `Tuple` type | ||
| - `MethodCompletionDetail`: stores the lazily computed detail of this `REPLCompletions.MethodCompletion` | ||
| - `MethodCompletionDetail`: stores the lazily computed detail of this `MethodCompletion` | ||
| + `rt`: `String` representing a return type of this method call | ||
| + `desc`: `String` representing documentation of this method | ||
|
|
||
|
|
@@ -142,15 +207,15 @@ If the second element is still a `MethodCompletionInfo` instance, that means its | |
| If the second element is already a `MethodCompletionDetail` then we can reuse this cache. | ||
|
|
||
| !!! note | ||
| If a method gets updated (i.e. redefined etc), key hash for the `REPLCompletions.MethodCompletion` | ||
| If a method gets updated (i.e. redefined etc), key hash for the `MethodCompletion` | ||
| will be different from the previous one, so it's okay if we just rely on the hash key. | ||
|
|
||
| !!! warning | ||
| Within the current implementation, we can't reflect changes that happens in backedges. | ||
| """ | ||
| const METHOD_COMPLETION_CACHE = OrderedDict{String,Union{MethodCompletionInfo,MethodCompletionDetail}}() | ||
|
|
||
| function completion(mod, c::REPLCompletions.MethodCompletion, prefix) | ||
| function completion(mod, c::MethodCompletion, prefix) | ||
| k = repr(hash(c)) | ||
|
|
||
| m = c.method | ||
|
|
@@ -194,27 +259,27 @@ function completion(mod, c::REPLCompletions.MethodCompletion, prefix) | |
| end | ||
|
|
||
| completiontext(c) = completion_text(c) | ||
| completiontext(c::REPLCompletions.MethodCompletion) = begin | ||
| completiontext(c::MethodCompletion) = begin | ||
| ct = completion_text(c) | ||
| m = match(r"^(.*) in .*$", ct) | ||
| m isa Nothing ? ct : m[1] | ||
| end | ||
| completiontext(c::REPLCompletions.DictCompletion) = rstrip(completion_text(c), [']', '"']) | ||
| completiontext(c::REPLCompletions.PathCompletion) = rstrip(completion_text(c), '"') | ||
| completiontext(c::DictCompletion) = rstrip(completion_text(c), [']', '"']) | ||
| completiontext(c::PathCompletion) = rstrip(completion_text(c), '"') | ||
|
|
||
| completionreturntype(c) = "" | ||
| completionreturntype(c::REPLCompletions.PropertyCompletion) = begin | ||
| completionreturntype(c::PropertyCompletion) = begin | ||
| isdefined(c.value, c.property) || return "" | ||
| shortstr(typeof(getproperty(c.value, c.property))) | ||
| end | ||
| completionreturntype(c::REPLCompletions.FieldCompletion) = | ||
| completionreturntype(c::FieldCompletion) = | ||
| shortstr(fieldtype(c.typ, c.field)) | ||
| completionreturntype(c::REPLCompletions.DictCompletion) = | ||
| completionreturntype(c::DictCompletion) = | ||
| shortstr(valtype(c.dict)) | ||
| completionreturntype(::REPLCompletions.PathCompletion) = "Path" | ||
| completionreturntype(::PathCompletion) = "Path" | ||
|
|
||
| completionurl(c) = "" | ||
| completionurl(c::REPLCompletions.ModuleCompletion) = begin | ||
| completionurl(c::ModuleCompletion) = begin | ||
| mod, name = c.parent, c.mod | ||
| val = getfield′(mod, name) | ||
| if val isa Module # module info | ||
|
|
@@ -223,46 +288,46 @@ completionurl(c::REPLCompletions.ModuleCompletion) = begin | |
| uridocs(mod, name) | ||
| end | ||
| end | ||
| completionurl(c::REPLCompletions.MethodCompletion) = uridocs(c.method.module, c.method.name) | ||
| completionurl(c::REPLCompletions.PackageCompletion) = urimoduleinfo(c.package) | ||
| completionurl(c::REPLCompletions.KeywordCompletion) = uridocs("Main", c.keyword) | ||
| completionurl(c::MethodCompletion) = uridocs(c.method.module, c.method.name) | ||
| completionurl(c::PackageCompletion) = urimoduleinfo(c.package) | ||
| completionurl(c::KeywordCompletion) = uridocs("Main", c.keyword) | ||
|
|
||
| completionmodule(mod, c) = shortstr(mod) | ||
| completionmodule(mod, c::REPLCompletions.ModuleCompletion) = shortstr(c.parent) | ||
| completionmodule(mod, c::REPLCompletions.MethodCompletion) = shortstr(c.method.module) | ||
| completionmodule(mod, c::REPLCompletions.FieldCompletion) = shortstr(c.typ) # predicted type | ||
| completionmodule(mod, ::REPLCompletions.KeywordCompletion) = "" | ||
| completionmodule(mod, ::REPLCompletions.PathCompletion) = "" | ||
| completionmodule(mod, c::ModuleCompletion) = shortstr(c.parent) | ||
| completionmodule(mod, c::MethodCompletion) = shortstr(c.method.module) | ||
| completionmodule(mod, c::FieldCompletion) = shortstr(c.typ) # predicted type | ||
| completionmodule(mod, ::KeywordCompletion) = "" | ||
| completionmodule(mod, ::PathCompletion) = "" | ||
|
|
||
| completiontype(c) = "variable" | ||
| completiontype(c::REPLCompletions.ModuleCompletion) = begin | ||
| completiontype(c::ModuleCompletion) = begin | ||
| ct = completion_text(c) | ||
| ismacro(ct) && return "snippet" | ||
| mod, name = c.parent, Symbol(ct) | ||
| val = getfield′(mod, name) | ||
| wstype(mod, name, val) | ||
| end | ||
| completiontype(::REPLCompletions.MethodCompletion) = "method" | ||
| completiontype(::REPLCompletions.PackageCompletion) = "import" | ||
| completiontype(::REPLCompletions.PropertyCompletion) = "property" | ||
| completiontype(::REPLCompletions.FieldCompletion) = "property" | ||
| completiontype(::REPLCompletions.DictCompletion) = "property" | ||
| completiontype(::REPLCompletions.KeywordCompletion) = "keyword" | ||
| completiontype(::REPLCompletions.PathCompletion) = "path" | ||
| completiontype(::MethodCompletion) = "method" | ||
| completiontype(::PackageCompletion) = "import" | ||
| completiontype(::PropertyCompletion) = "property" | ||
| completiontype(::FieldCompletion) = "property" | ||
| completiontype(::DictCompletion) = "property" | ||
| completiontype(::KeywordCompletion) = "keyword" | ||
| completiontype(::PathCompletion) = "path" | ||
|
|
||
| completionicon(c) = "" | ||
| completionicon(c::REPLCompletions.ModuleCompletion) = begin | ||
| completionicon(c::ModuleCompletion) = begin | ||
| ismacro(c.mod) && return "icon-mention" | ||
| mod, name = c.parent, Symbol(c.mod) | ||
| val = getfield′(mod, name) | ||
| wsicon(mod, name, val) | ||
| end | ||
| completionicon(::REPLCompletions.DictCompletion) = "icon-key" | ||
| completionicon(::REPLCompletions.PathCompletion) = "icon-file" | ||
| completionicon(::DictCompletion) = "icon-key" | ||
| completionicon(::PathCompletion) = "icon-file" | ||
|
|
||
| completiondetailtype(c) = "" | ||
| completiondetailtype(::REPLCompletions.ModuleCompletion) = "module" | ||
| completiondetailtype(::REPLCompletions.KeywordCompletion) = "keyword" | ||
| completiondetailtype(::ModuleCompletion) = "module" | ||
| completiondetailtype(::KeywordCompletion) = "keyword" | ||
|
|
||
| function localcompletions(context, row, col, prefix) | ||
| ls = locals(context, row, col) | ||
|
|
@@ -289,7 +354,8 @@ function localcompletion(l, prefix, lines) | |
| ) | ||
| end | ||
|
|
||
| ### completion details on selection ### | ||
| # completion details | ||
| # ------------------ | ||
|
|
||
| handle("completiondetail") do _comp | ||
| comp = Dict(Symbol(k) => v for (k, v) in _comp) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.