Skip to content

Commit 3306ed5

Browse files
authored
Deduplicate suggestions in package completions (#4431)
1 parent 3523760 commit 3306ed5

File tree

2 files changed

+79
-20
lines changed

2 files changed

+79
-20
lines changed

ext/REPLExt/completions.jl

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function _shared_envs()
1111
return possible
1212
end
1313

14-
function complete_activate(options, partial, i1, i2; hint::Bool)
14+
function complete_activate(options, partial, i1, i2; hint::Bool, arguments = [])
1515
shared = get(options, :shared, false)
1616
if shared
1717
return _shared_envs()
@@ -54,6 +54,21 @@ end
5454

5555

5656
const JULIA_UUID = UUID("1222c4b2-2114-5bfd-aeef-88e4692bbb3e")
57+
58+
# Helper function to extract already-specified package names from arguments
59+
# Used for deduplicating completion suggestions (issue #4098)
60+
function extract_specified_names(arguments)
61+
specified_names = Set{String}()
62+
# Exclude the last argument, which is the one currently being completed
63+
for i in 1:(length(arguments) - 1)
64+
arg = arguments[i]
65+
arg_str = arg isa String ? arg : arg.raw
66+
# Extract package name (before any @, #, =, or : specifiers)
67+
pkg_name = first(split(arg_str, ['@', '#', '=', ':']))
68+
push!(specified_names, pkg_name)
69+
end
70+
return specified_names
71+
end
5772
function complete_remote_package!(comps, partial; hint::Bool)
5873
isempty(partial) && return true # true means returned early
5974
found_match = !isempty(comps)
@@ -95,38 +110,46 @@ function complete_remote_package!(comps, partial; hint::Bool)
95110
return false # false means performed full search
96111
end
97112

98-
function complete_help(options, partial; hint::Bool)
113+
function complete_help(options, partial; hint::Bool, arguments = [])
99114
names = String[]
100115
for cmds in values(SPECS)
101116
append!(names, [spec.canonical_name for spec in values(cmds)])
102117
end
103118
return sort!(unique!(append!(names, collect(keys(SPECS)))))
104119
end
105120

106-
function complete_installed_packages(options, partial; hint::Bool)
121+
function complete_installed_packages(options, partial; hint::Bool, arguments = [])
107122
env = try
108123
EnvCache()
109124
catch err
110125
err isa PkgError || rethrow()
111126
return String[]
112127
end
113128
mode = get(options, :mode, PKGMODE_PROJECT)
114-
return mode == PKGMODE_PROJECT ?
129+
packages = mode == PKGMODE_PROJECT ?
115130
collect(keys(env.project.deps)) :
116131
unique!([entry.name for (uuid, entry) in env.manifest])
132+
133+
# Filter out already-specified packages
134+
specified_names = extract_specified_names(arguments)
135+
return filter(pkg -> !(pkg in specified_names), packages)
117136
end
118137

119-
function complete_all_installed_packages(options, partial; hint::Bool)
138+
function complete_all_installed_packages(options, partial; hint::Bool, arguments = [])
120139
env = try
121140
EnvCache()
122141
catch err
123142
err isa PkgError || rethrow()
124143
return String[]
125144
end
126-
return unique!([entry.name for (uuid, entry) in env.manifest])
145+
packages = unique!([entry.name for (uuid, entry) in env.manifest])
146+
147+
# Filter out already-specified packages
148+
specified_names = extract_specified_names(arguments)
149+
return filter(pkg -> !(pkg in specified_names), packages)
127150
end
128151

129-
function complete_installed_packages_and_compat(options, partial; hint::Bool)
152+
function complete_installed_packages_and_compat(options, partial; hint::Bool, arguments = [])
130153
env = try
131154
EnvCache()
132155
catch err
@@ -139,17 +162,21 @@ function complete_installed_packages_and_compat(options, partial; hint::Bool)
139162
end
140163
end
141164

142-
function complete_fixed_packages(options, partial; hint::Bool)
165+
function complete_fixed_packages(options, partial; hint::Bool, arguments = [])
143166
env = try
144167
EnvCache()
145168
catch err
146169
err isa PkgError || rethrow()
147170
return String[]
148171
end
149-
return unique!([entry.name for (uuid, entry) in env.manifest.deps if Operations.isfixed(entry)])
172+
packages = unique!([entry.name for (uuid, entry) in env.manifest.deps if Operations.isfixed(entry)])
173+
174+
# Filter out already-specified packages
175+
specified_names = extract_specified_names(arguments)
176+
return filter(pkg -> !(pkg in specified_names), packages)
150177
end
151178

152-
function complete_add_dev(options, partial, i1, i2; hint::Bool)
179+
function complete_add_dev(options, partial, i1, i2; hint::Bool, arguments = [])
153180
comps, idx, _ = complete_local_dir(partial, i1, i2)
154181
if occursin(Base.Filesystem.path_separator_re, partial)
155182
return comps, idx, !isempty(comps)
@@ -159,12 +186,17 @@ function complete_add_dev(options, partial, i1, i2; hint::Bool)
159186
if !returned_early
160187
append!(comps, filter!(startswith(partial), [info.name for info in values(Types.stdlib_infos())]))
161188
end
189+
190+
# Filter out already-specified packages
191+
specified_names = extract_specified_names(arguments)
192+
filter!(pkg -> !(pkg in specified_names), comps)
193+
162194
return comps, idx, !isempty(comps)
163195
end
164196

165197
# TODO: Move
166198
import Pkg: Operations, Types, Apps
167-
function complete_installed_apps(options, partial; hint)
199+
function complete_installed_apps(options, partial; hint, arguments = [])
168200
manifest = try
169201
Types.read_manifest(joinpath(Apps.app_env_folder(), "AppManifest.toml"))
170202
catch err
@@ -176,7 +208,11 @@ function complete_installed_apps(options, partial; hint)
176208
append!(apps, keys(entry.apps))
177209
push!(apps, entry.name)
178210
end
179-
return unique!(apps)
211+
apps = unique!(apps)
212+
213+
# Filter out already-specified packages
214+
specified_names = extract_specified_names(arguments, partial)
215+
return filter(app -> !(app in specified_names), apps)
180216
end
181217

182218
########################
@@ -215,7 +251,7 @@ complete_opt(opt_specs) =
215251
)
216252

217253
function complete_argument(
218-
spec::CommandSpec, options::Vector{String},
254+
spec::CommandSpec, options::Vector{String}, arguments::Vector,
219255
partial::AbstractString, offset::Int,
220256
index::Int; hint::Bool
221257
)
@@ -228,10 +264,15 @@ function complete_argument(
228264
@error "REPLMode indicates a completion function called :$(spec.completions) that cannot be found in REPLExt"
229265
rethrow()
230266
end
231-
spec.completions = function (opts, partial, offset, index; hint::Bool)
232-
return applicable(completions, opts, partial, offset, index) ?
233-
completions(opts, partial, offset, index; hint) :
234-
completions(opts, partial; hint)
267+
spec.completions = function (opts, partial, offset, index; hint::Bool, arguments = [])
268+
# Wrapper that normalizes completion function calls.
269+
if applicable(completions, opts, partial, offset, index)
270+
# Function takes 4 positional args: (opts, partial, offset, index; hint, arguments)
271+
return completions(opts, partial, offset, index; hint, arguments)
272+
else
273+
# Function takes 2 positional args: (opts, partial; hint, arguments)
274+
return completions(opts, partial; hint, arguments)
275+
end
235276
end
236277
end
237278
spec.completions === nothing && return String[]
@@ -243,7 +284,7 @@ function complete_argument(
243284
e isa PkgError && return String[]
244285
rethrow()
245286
end
246-
return spec.completions(opts, partial, offset, index; hint)
287+
return spec.completions(opts, partial, offset, index; hint, arguments)
247288
end
248289

249290
function _completions(input, final, offset, index; hint::Bool)
@@ -269,11 +310,11 @@ function _completions(input, final, offset, index; hint::Bool)
269310
command_is_focused() && return String[], 0:-1, false
270311

271312
if final # complete arg by default
272-
x = complete_argument(statement.spec, statement.options, partial, offset, index; hint)
313+
x = complete_argument(statement.spec, statement.options, statement.arguments, partial, offset, index; hint)
273314
else # complete arg or opt depending on last token
274315
x = is_opt(partial) ?
275316
complete_opt(statement.spec.option_specs) :
276-
complete_argument(statement.spec, statement.options, partial, offset, index; hint)
317+
complete_argument(statement.spec, statement.options, statement.arguments, partial, offset, index; hint)
277318
end
278319
end
279320

test/repl.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,24 @@ temp_pkg_dir() do project_path
403403
@test "Example" in c
404404
pkg"free Example"
405405

406+
# Test deduplication of already-specified packages (issue #4098)
407+
# After typing "rm Example ", typing "E" should not suggest Example again
408+
c, r = test_complete("rm Example E")
409+
@test !("Example" in c) # Example already specified, should not suggest again
410+
411+
# Test with package@version syntax - should still deduplicate
412+
c, r = test_complete("rm [email protected] Exam")
413+
@test !("Example" in c) # Example already specified with version
414+
415+
# Test with multiple packages already specified
416+
c, r = test_complete("rm Example PackageWithDependency E")
417+
@test !("Example" in c) # Both already specified
418+
@test !("PackageWithDependency" in c)
419+
420+
# Test deduplication works for add as well
421+
c, r = test_complete("add Example E")
422+
@test !("Example" in c) # Example already specified for add command
423+
406424
# help mode
407425
@test apply_completion("?ad") == "?add"
408426
@test apply_completion("?act") == "?activate"

0 commit comments

Comments
 (0)