Skip to content

Commit 6396345

Browse files
authored
Merge pull request #1824 from Glamhoth/suggestions
Make suggestions based on Damerau-Levenshtein distance
2 parents 1b7875e + d4c1eb8 commit 6396345

File tree

7 files changed

+225
-4
lines changed

7 files changed

+225
-4
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ same-file = "1"
4242
scopeguard = "1"
4343
semver = "0.9"
4444
sha2 = "0.8"
45+
strsim = "0.9.1"
4546
tar = "0.4"
4647
tempdir = "0.3.4"
4748
# FIXME(issue #1788)

src/dist/manifest.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,4 +448,11 @@ impl Component {
448448
pkg.to_string()
449449
}
450450
}
451+
pub fn target(&self) -> String {
452+
if let Some(ref t) = self.target {
453+
format!("{}", t)
454+
} else {
455+
format!("")
456+
}
457+
}
451458
}

src/errors.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,13 @@ error_chain! {
303303
description("toolchain does not support components")
304304
display("toolchain '{}' does not support components", t)
305305
}
306-
UnknownComponent(t: String, c: String) {
306+
UnknownComponent(t: String, c: String, s: Option<String>) {
307307
description("toolchain does not contain component")
308-
display("toolchain '{}' does not contain component {}", t, c)
308+
display("toolchain '{}' does not contain component {}{}", t, c, if let Some(suggestion) = s {
309+
format!("; did you mean '{}'?", suggestion)
310+
} else {
311+
"".to_string()
312+
})
309313
}
310314
AddingRequiredComponent(t: String, c: String) {
311315
description("required component cannot be added")

src/toolchain.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::config::Cfg;
22
use crate::dist::dist::ToolchainDesc;
33
use crate::dist::download::DownloadCfg;
44
use crate::dist::manifest::Component;
5+
use crate::dist::manifest::Manifest;
56
use crate::dist::manifestation::{Changes, Manifestation};
67
use crate::dist::prefix::InstallPrefix;
78
use crate::env_var;
@@ -556,6 +557,89 @@ impl<'a> Toolchain<'a> {
556557
}
557558
}
558559

560+
fn get_component_suggestion(
561+
&self,
562+
component: &Component,
563+
manifest: &Manifest,
564+
only_installed: bool,
565+
) -> Option<String> {
566+
use strsim::damerau_levenshtein;
567+
568+
// Suggest only for very small differences
569+
// High number can result in innacurate suggestions for short queries e.g. `rls`
570+
const MAX_DISTANCE: usize = 3;
571+
572+
let components = self.list_components();
573+
if let Ok(components) = components {
574+
let short_name_distance = components
575+
.iter()
576+
.filter(|c| !only_installed || c.installed)
577+
.map(|c| {
578+
(
579+
damerau_levenshtein(
580+
&c.component.name(manifest)[..],
581+
&component.name(manifest)[..],
582+
),
583+
c,
584+
)
585+
})
586+
.min_by_key(|t| t.0)
587+
.expect("There should be always at least one component");
588+
589+
let long_name_distance = components
590+
.iter()
591+
.filter(|c| !only_installed || c.installed)
592+
.map(|c| {
593+
(
594+
damerau_levenshtein(
595+
&c.component.name_in_manifest()[..],
596+
&component.name(manifest)[..],
597+
),
598+
c,
599+
)
600+
})
601+
.min_by_key(|t| t.0)
602+
.expect("There should be always at least one component");
603+
604+
let mut closest_distance = short_name_distance;
605+
let mut closest_match = short_name_distance.1.component.short_name(manifest);
606+
607+
// Find closer suggestion
608+
if short_name_distance.0 > long_name_distance.0 {
609+
closest_distance = long_name_distance;
610+
611+
// Check if only targets differ
612+
if closest_distance.1.component.short_name_in_manifest()
613+
== component.short_name_in_manifest()
614+
{
615+
closest_match = long_name_distance.1.component.target();
616+
} else {
617+
closest_match = long_name_distance
618+
.1
619+
.component
620+
.short_name_in_manifest()
621+
.to_string();
622+
}
623+
} else {
624+
// Check if only targets differ
625+
if closest_distance.1.component.short_name(manifest)
626+
== component.short_name(manifest)
627+
{
628+
closest_match = short_name_distance.1.component.target().to_string();
629+
}
630+
}
631+
632+
// If suggestion is too different don't suggest anything
633+
if closest_distance.0 > MAX_DISTANCE {
634+
return None;
635+
} else {
636+
return Some(closest_match.to_string());
637+
}
638+
} else {
639+
return None;
640+
}
641+
}
642+
559643
pub fn add_component(&self, mut component: Component) -> Result<()> {
560644
if !self.exists() {
561645
return Err(ErrorKind::ToolchainNotInstalled(self.name.to_owned()).into());
@@ -604,6 +688,7 @@ impl<'a> Toolchain<'a> {
604688
return Err(ErrorKind::UnknownComponent(
605689
self.name.to_string(),
606690
component.description(&manifest),
691+
self.get_component_suggestion(&component, &manifest, false),
607692
)
608693
.into());
609694
}
@@ -674,6 +759,7 @@ impl<'a> Toolchain<'a> {
674759
return Err(ErrorKind::UnknownComponent(
675760
self.name.to_string(),
676761
component.description(&manifest),
762+
self.get_component_suggestion(&component, &manifest, true),
677763
)
678764
.into());
679765
}

tests/cli-v2.rs

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
pub mod mock;
55

66
use crate::mock::clitools::{
7-
self, expect_err, expect_not_stdout_ok, expect_ok, expect_stderr_ok, expect_stdout_ok,
8-
set_current_dist_date, this_host_triple, Config, Scenario,
7+
self, expect_err, expect_not_stderr_ok, expect_not_stdout_ok, expect_ok, expect_stderr_ok,
8+
expect_stdout_ok, set_current_dist_date, this_host_triple, Config, Scenario,
99
};
1010
use std::fs;
1111
use std::io::Write;
@@ -920,3 +920,109 @@ fn update_unavailable_force() {
920920
);
921921
});
922922
}
923+
924+
#[test]
925+
fn add_component_suggest_best_match() {
926+
setup(&|config| {
927+
expect_ok(config, &["rustup", "default", "nightly"]);
928+
expect_err(
929+
config,
930+
&["rustup", "component", "add", "rsl"],
931+
"did you mean 'rls'?",
932+
);
933+
expect_err(
934+
config,
935+
&["rustup", "component", "add", "rsl-preview"],
936+
"did you mean 'rls-preview'?",
937+
);
938+
expect_err(
939+
config,
940+
&["rustup", "component", "add", "rustd"],
941+
"did you mean 'rustc'?",
942+
);
943+
expect_not_stderr_ok(
944+
config,
945+
&["rustup", "component", "add", "potato"],
946+
"did you mean",
947+
);
948+
});
949+
}
950+
951+
#[test]
952+
fn remove_component_suggest_best_match() {
953+
setup(&|config| {
954+
expect_ok(config, &["rustup", "default", "nightly"]);
955+
expect_not_stderr_ok(
956+
config,
957+
&["rustup", "component", "remove", "rsl"],
958+
"did you mean 'rls'?",
959+
);
960+
expect_ok(config, &["rustup", "component", "add", "rls"]);
961+
expect_err(
962+
config,
963+
&["rustup", "component", "remove", "rsl"],
964+
"did you mean 'rls'?",
965+
);
966+
expect_ok(config, &["rustup", "component", "add", "rls-preview"]);
967+
expect_err(
968+
config,
969+
&["rustup", "component", "add", "rsl-preview"],
970+
"did you mean 'rls-preview'?",
971+
);
972+
expect_err(
973+
config,
974+
&["rustup", "component", "remove", "rustd"],
975+
"did you mean 'rustc'?",
976+
);
977+
});
978+
}
979+
980+
#[test]
981+
fn add_target_suggest_best_match() {
982+
setup(&|config| {
983+
expect_ok(config, &["rustup", "default", "nightly"]);
984+
expect_err(
985+
config,
986+
&[
987+
"rustup",
988+
"target",
989+
"add",
990+
&format!("{}a", clitools::CROSS_ARCH1)[..],
991+
],
992+
&format!("did you mean '{}'", clitools::CROSS_ARCH1),
993+
);
994+
expect_not_stderr_ok(
995+
config,
996+
&["rustup", "target", "add", "potato"],
997+
"did you mean",
998+
);
999+
});
1000+
}
1001+
1002+
#[test]
1003+
fn remove_target_suggest_best_match() {
1004+
setup(&|config| {
1005+
expect_ok(config, &["rustup", "default", "nightly"]);
1006+
expect_not_stderr_ok(
1007+
config,
1008+
&[
1009+
"rustup",
1010+
"target",
1011+
"remove",
1012+
&format!("{}a", clitools::CROSS_ARCH1)[..],
1013+
],
1014+
&format!("did you mean '{}'", clitools::CROSS_ARCH1),
1015+
);
1016+
expect_ok(config, &["rustup", "target", "add", clitools::CROSS_ARCH1]);
1017+
expect_err(
1018+
config,
1019+
&[
1020+
"rustup",
1021+
"target",
1022+
"remove",
1023+
&format!("{}a", clitools::CROSS_ARCH1)[..],
1024+
],
1025+
&format!("did you mean '{}'", clitools::CROSS_ARCH1),
1026+
);
1027+
});
1028+
}

tests/mock/clitools.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ pub fn expect_not_stdout_ok(config: &Config, args: &[&str], expected: &str) {
207207
}
208208
}
209209

210+
pub fn expect_not_stderr_ok(config: &Config, args: &[&str], expected: &str) {
211+
let out = run(config, args[0], &args[1..], &[]);
212+
if out.ok || out.stderr.contains(expected) {
213+
print_command(args, &out);
214+
println!("expected.ok: false");
215+
print_indented("expected.stderr.does_not_contain", expected);
216+
panic!();
217+
}
218+
}
219+
210220
pub fn expect_stderr_ok(config: &Config, args: &[&str], expected: &str) {
211221
let out = run(config, args[0], &args[1..], &[]);
212222
if !out.ok || !out.stderr.contains(expected) {

0 commit comments

Comments
 (0)