Repository: rust-analyzer/rust-analyzer
Pull request #6111 (2020-10-01 15:58:32)

Add assist for converting the base of integer literals.
This PR adds an assist similar to Intellij's [convert number to](https://i.imgur.com/JH6wstP.png). It also does a small refactor to [assists/src/tests.rs](https://github.com/rust-analyzer/rust-analyzer/blob/fc34403018079ea053f26d0a31b7517053c7dd8c/crates/assists/src/tests.rs) to add the ability to specify the resolved assist for a specific action within an assist group.

## Demo
![Demo of the assist in action](https://i.imgur.com/MBhdPFH.gif)


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5b69e3cab4..74e5a82736 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -82,9 +82,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
 name = "backtrace"
-version = "0.3.51"
+version = "0.3.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec1931848a574faa8f7c71a12ea00453ff5effbb5f51afe7f77d7a48cace6ac1"
+checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293"
 dependencies = [
  "addr2line",
  "cfg-if",
@@ -453,9 +453,9 @@ dependencies = [
 
 [[package]]
 name = "hashbrown"
-version = "0.9.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
+checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7"
 
 [[package]]
 name = "heck"
@@ -1069,12 +1069,6 @@ version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "28b9b4df73455c861d7cbf8be42f01d3b373ed7f02e378d55fa84eafc6f638b1"
 
-[[package]]
-name = "pin-project-lite"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fe74897791e156a0cd8cce0db31b9b2198e67877316bf3086c3acd187f719f0"
-
 [[package]]
 name = "plain"
 version = "0.2.3"
@@ -1186,9 +1180,9 @@ dependencies = [
 
 [[package]]
 name = "rayon"
-version = "1.4.1"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf6960dc9a5b4ee8d3e4c5787b4a112a8818e0290a42ff664ad60692fdf2032"
+checksum = "cfd016f0c045ad38b5251be2c9c0ab806917f82da4d36b2a327e5166adad9270"
 dependencies = [
  "autocfg",
  "crossbeam-deque",
@@ -1629,12 +1623,11 @@ dependencies = [
 
 [[package]]
 name = "tracing"
-version = "0.1.21"
+version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27"
+checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c"
 dependencies = [
  "cfg-if",
- "pin-project-lite",
  "tracing-attributes",
  "tracing-core",
 ]
@@ -1652,9 +1645,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-core"
-version = "0.1.17"
+version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f"
+checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a"
 dependencies = [
  "lazy_static",
 ]
@@ -1703,9 +1696,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-tree"
-version = "0.1.6"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43aac8afb493b08e1e1904956f7407c1e671b9c83b26a17e1bd83d6a3520e350"
+checksum = "e1a3dc4774db3a6b2d66a4f8d8de670e874ec3ed55615860c994927419b32c5f"
 dependencies = [
  "ansi_term",
  "atty",
diff --git a/crates/assists/src/handlers/convert_integer_literal.rs b/crates/assists/src/handlers/convert_integer_literal.rs
new file mode 100644
index 0000000000..ea35e833a9
--- /dev/null
+++ b/crates/assists/src/handlers/convert_integer_literal.rs
@@ -0,0 +1,701 @@
+use syntax::{ast, AstNode, SmolStr};
+
+use crate::{AssistContext, AssistId, AssistKind, Assists, GroupLabel};
+
+// Assist: convert_integer_literal
+//
+// Converts the base of integer literals to other bases.
+//
+// ```
+// const _: i32 = 10<|>;
+// ```
+// ->
+// ```
+// const _: i32 = 0b1010;
+// ```
+pub(crate) fn convert_integer_literal(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+    let literal = ctx.find_node_at_offset::<ast::Literal>()?;
+    let range = literal.syntax().text_range();
+    let group_id = GroupLabel("Convert integer base".into());
+
+    let suffix = match literal.kind() {
+        ast::LiteralKind::IntNumber { suffix } => suffix,
+        _ => return None,
+    };
+    let suffix_len = suffix.as_ref().map(|s| s.len()).unwrap_or(0);
+    let raw_literal_text = literal.syntax().to_string();
+
+    // Gets the literal's text without the type suffix and without underscores.
+    let literal_text = raw_literal_text
+        .chars()
+        .take(raw_literal_text.len() - suffix_len)
+        .filter(|c| *c != '_')
+        .collect::<SmolStr>();
+    let literal_base = IntegerLiteralBase::identify(&literal_text)?;
+
+    for base in IntegerLiteralBase::bases() {
+        if *base == literal_base {
+            continue;
+        }
+
+        let mut converted = literal_base.convert(&literal_text, base);
+
+        let label = if let Some(suffix) = &suffix {
+            format!("Convert {} ({}) to {}", &literal_text, suffix, &converted)
+        } else {
+            format!("Convert {} to {}", &literal_text, &converted)
+        };
+
+        // Appends the type suffix back into the new literal if it exists.
+        if let Some(suffix) = &suffix {
+            converted.push_str(&suffix);
+        }
+
+        acc.add_group(
+            &group_id,
+            AssistId("convert_integer_literal", AssistKind::RefactorInline),
+            label,
+            range,
+            |builder| builder.replace(range, converted),
+        );
+    }
+
+    Some(())
+}
+
+#[derive(Debug, PartialEq, Eq)]
+enum IntegerLiteralBase {
+    Binary,
+    Octal,
+    Decimal,
+    Hexadecimal,
+}
+
+impl IntegerLiteralBase {
+    fn identify(literal_text: &str) -> Option<Self> {
+        // We cannot express a literal in anything other than decimal in under 3 characters, so we return here if possible.
+        if literal_text.len() < 3 && literal_text.chars().all(|c| c.is_digit(10)) {
+            return Some(Self::Decimal);
+        }
+
+        let base = match &literal_text[..2] {
+            "0b" => Self::Binary,
+            "0o" => Self::Octal,
+            "0x" => Self::Hexadecimal,
+            _ => Self::Decimal,
+        };
+
+        // Checks that all characters after the base prefix are all valid digits for that base.
+        if literal_text[base.prefix_len()..].chars().all(|c| c.is_digit(base.base())) {
+            Some(base)
+        } else {
+            None
+        }
+    }
+
+    fn convert(&self, literal_text: &str, to: &IntegerLiteralBase) -> String {
+        let digits = &literal_text[self.prefix_len()..];
+        let value = u128::from_str_radix(digits, self.base()).unwrap();
+
+        match to {
+            Self::Binary => format!("0b{:b}", value),
+            Self::Octal => format!("0o{:o}", value),
+            Self::Decimal => value.to_string(),
+            Self::Hexadecimal => format!("0x{:X}", value),
+        }
+    }
+
+    const fn base(&self) -> u32 {
+        match self {
+            Self::Binary => 2,
+            Self::Octal => 8,
+            Self::Decimal => 10,
+            Self::Hexadecimal => 16,
+        }
+    }
+
+    const fn prefix_len(&self) -> usize {
+        match self {
+            Self::Decimal => 0,
+            _ => 2,
+        }
+    }
+
+    const fn bases() -> &'static [IntegerLiteralBase] {
+        &[
+            IntegerLiteralBase::Binary,
+            IntegerLiteralBase::Octal,
+            IntegerLiteralBase::Decimal,
+            IntegerLiteralBase::Hexadecimal,
+        ]
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+    use crate::tests::{check_assist_by_label, check_assist_target};
+
+    #[test]
+    fn binary_target() {
+        check_assist_target(convert_integer_literal, "const _: i32 = 0b1010<|>;", "0b1010");
+    }
+
+    #[test]
+    fn octal_target() {
+        check_assist_target(convert_integer_literal, "const _: i32 = 0o12<|>;", "0o12");
+    }
+
+    #[test]
+    fn decimal_target() {
+        check_assist_target(convert_integer_literal, "const _: i32 = 10<|>;", "10");
+    }
+
+    #[test]
+    fn hexadecimal_target() {
+        check_assist_target(convert_integer_literal, "const _: i32 = 0xA<|>;", "0xA");
+    }
+
+    #[test]
+    fn binary_target_with_underscores() {
+        check_assist_target(convert_integer_literal, "const _: i32 = 0b10_10<|>;", "0b10_10");
+    }
+
+    #[test]
+    fn octal_target_with_underscores() {
+        check_assist_target(convert_integer_literal, "const _: i32 = 0o1_2<|>;", "0o1_2");
+    }
+
+    #[test]
+    fn decimal_target_with_underscores() {
+        check_assist_target(convert_integer_literal, "const _: i32 = 1_0<|>;", "1_0");
+    }
+
+    #[test]
+    fn hexadecimal_target_with_underscores() {
+        check_assist_target(convert_integer_literal, "const _: i32 = 0x_A<|>;", "0x_A");
+    }
+
+    #[test]
+    fn convert_decimal_integer() {
+        let before = "const _: i32 = 1000<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b1111101000;",
+            "Convert 1000 to 0b1111101000",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o1750;",
+            "Convert 1000 to 0o1750",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0x3E8;",
+            "Convert 1000 to 0x3E8",
+        );
+    }
+
+    // Decimal numbers under 3 digits have a special case where they return early because we can't fit a
+    // other base's prefix, so we have a separate test for that.
+    #[test]
+    fn convert_small_decimal_integer() {
+        let before = "const _: i32 = 10<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b1010;",
+            "Convert 10 to 0b1010",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o12;",
+            "Convert 10 to 0o12",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xA;",
+            "Convert 10 to 0xA",
+        );
+    }
+
+    #[test]
+    fn convert_hexadecimal_integer() {
+        let before = "const _: i32 = 0xFF<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b11111111;",
+            "Convert 0xFF to 0b11111111",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o377;",
+            "Convert 0xFF to 0o377",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255;",
+            "Convert 0xFF to 255",
+        );
+    }
+
+    #[test]
+    fn convert_binary_integer() {
+        let before = "const _: i32 = 0b11111111<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o377;",
+            "Convert 0b11111111 to 0o377",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255;",
+            "Convert 0b11111111 to 255",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xFF;",
+            "Convert 0b11111111 to 0xFF",
+        );
+    }
+
+    #[test]
+    fn convert_octal_integer() {
+        let before = "const _: i32 = 0o377<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b11111111;",
+            "Convert 0o377 to 0b11111111",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255;",
+            "Convert 0o377 to 255",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xFF;",
+            "Convert 0o377 to 0xFF",
+        );
+    }
+
+    #[test]
+    fn convert_decimal_integer_with_underscores() {
+        let before = "const _: i32 = 1_00_0<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b1111101000;",
+            "Convert 1000 to 0b1111101000",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o1750;",
+            "Convert 1000 to 0o1750",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0x3E8;",
+            "Convert 1000 to 0x3E8",
+        );
+    }
+
+    #[test]
+    fn convert_small_decimal_integer_with_underscores() {
+        let before = "const _: i32 = 1_0<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b1010;",
+            "Convert 10 to 0b1010",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o12;",
+            "Convert 10 to 0o12",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xA;",
+            "Convert 10 to 0xA",
+        );
+    }
+
+    #[test]
+    fn convert_hexadecimal_integer_with_underscores() {
+        let before = "const _: i32 = 0x_F_F<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b11111111;",
+            "Convert 0xFF to 0b11111111",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o377;",
+            "Convert 0xFF to 0o377",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255;",
+            "Convert 0xFF to 255",
+        );
+    }
+
+    #[test]
+    fn convert_binary_integer_with_underscores() {
+        let before = "const _: i32 = 0b1111_1111<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o377;",
+            "Convert 0b11111111 to 0o377",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255;",
+            "Convert 0b11111111 to 255",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xFF;",
+            "Convert 0b11111111 to 0xFF",
+        );
+    }
+
+    #[test]
+    fn convert_octal_integer_with_underscores() {
+        let before = "const _: i32 = 0o3_77<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b11111111;",
+            "Convert 0o377 to 0b11111111",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255;",
+            "Convert 0o377 to 255",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xFF;",
+            "Convert 0o377 to 0xFF",
+        );
+    }
+
+    #[test]
+    fn convert_decimal_integer_with_suffix() {
+        let before = "const _: i32 = 1000i32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b1111101000i32;",
+            "Convert 1000 (i32) to 0b1111101000",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o1750i32;",
+            "Convert 1000 (i32) to 0o1750",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0x3E8i32;",
+            "Convert 1000 (i32) to 0x3E8",
+        );
+    }
+
+    #[test]
+    fn convert_small_decimal_integer_with_suffix() {
+        let before = "const _: i32 = 10i32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b1010i32;",
+            "Convert 10 (i32) to 0b1010",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o12i32;",
+            "Convert 10 (i32) to 0o12",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xAi32;",
+            "Convert 10 (i32) to 0xA",
+        );
+    }
+
+    #[test]
+    fn convert_hexadecimal_integer_with_suffix() {
+        let before = "const _: i32 = 0xFFi32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b11111111i32;",
+            "Convert 0xFF (i32) to 0b11111111",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o377i32;",
+            "Convert 0xFF (i32) to 0o377",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255i32;",
+            "Convert 0xFF (i32) to 255",
+        );
+    }
+
+    #[test]
+    fn convert_binary_integer_with_suffix() {
+        let before = "const _: i32 = 0b11111111i32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o377i32;",
+            "Convert 0b11111111 (i32) to 0o377",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255i32;",
+            "Convert 0b11111111 (i32) to 255",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xFFi32;",
+            "Convert 0b11111111 (i32) to 0xFF",
+        );
+    }
+
+    #[test]
+    fn convert_octal_integer_with_suffix() {
+        let before = "const _: i32 = 0o377i32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b11111111i32;",
+            "Convert 0o377 (i32) to 0b11111111",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255i32;",
+            "Convert 0o377 (i32) to 255",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xFFi32;",
+            "Convert 0o377 (i32) to 0xFF",
+        );
+    }
+
+    #[test]
+    fn convert_decimal_integer_with_underscores_and_suffix() {
+        let before = "const _: i32 = 1_00_0i32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b1111101000i32;",
+            "Convert 1000 (i32) to 0b1111101000",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o1750i32;",
+            "Convert 1000 (i32) to 0o1750",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0x3E8i32;",
+            "Convert 1000 (i32) to 0x3E8",
+        );
+    }
+
+    #[test]
+    fn convert_small_decimal_integer_with_underscores_and_suffix() {
+        let before = "const _: i32 = 1_0i32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b1010i32;",
+            "Convert 10 (i32) to 0b1010",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o12i32;",
+            "Convert 10 (i32) to 0o12",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xAi32;",
+            "Convert 10 (i32) to 0xA",
+        );
+    }
+
+    #[test]
+    fn convert_hexadecimal_integer_with_underscores_and_suffix() {
+        let before = "const _: i32 = 0x_F_Fi32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b11111111i32;",
+            "Convert 0xFF (i32) to 0b11111111",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o377i32;",
+            "Convert 0xFF (i32) to 0o377",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255i32;",
+            "Convert 0xFF (i32) to 255",
+        );
+    }
+
+    #[test]
+    fn convert_binary_integer_with_underscores_and_suffix() {
+        let before = "const _: i32 = 0b1111_1111i32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0o377i32;",
+            "Convert 0b11111111 (i32) to 0o377",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255i32;",
+            "Convert 0b11111111 (i32) to 255",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xFFi32;",
+            "Convert 0b11111111 (i32) to 0xFF",
+        );
+    }
+
+    #[test]
+    fn convert_octal_integer_with_underscores_and_suffix() {
+        let before = "const _: i32 = 0o3_77i32<|>;";
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0b11111111i32;",
+            "Convert 0o377 (i32) to 0b11111111",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 255i32;",
+            "Convert 0o377 (i32) to 255",
+        );
+
+        check_assist_by_label(
+            convert_integer_literal,
+            before,
+            "const _: i32 = 0xFFi32;",
+            "Convert 0o377 (i32) to 0xFF",
+        );
+    }
+}
diff --git a/crates/assists/src/lib.rs b/crates/assists/src/lib.rs
index cbac53e711..a2bec818c1 100644
--- a/crates/assists/src/lib.rs
+++ b/crates/assists/src/lib.rs
@@ -128,6 +128,7 @@ mod handlers {
     mod auto_import;
     mod change_return_type_to_result;
     mod change_visibility;
+    mod convert_integer_literal;
     mod early_return;
     mod expand_glob_import;
     mod extract_struct_from_enum_variant;
@@ -172,6 +173,7 @@ mod handlers {
             auto_import::auto_import,
             change_return_type_to_result::change_return_type_to_result,
             change_visibility::change_visibility,
+            convert_integer_literal::convert_integer_literal,
             early_return::convert_to_guarded_return,
             expand_glob_import::expand_glob_import,
             extract_struct_from_enum_variant::extract_struct_from_enum_variant,
diff --git a/crates/assists/src/utils/insert_use.rs b/crates/assists/src/utils/insert_use.rs
index 8dd4fe6074..5719b06af2 100644
--- a/crates/assists/src/utils/insert_use.rs
+++ b/crates/assists/src/utils/insert_use.rs
@@ -174,7 +174,7 @@ pub(crate) fn try_merge_trees(
     let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?;
     let lhs = lhs.split_prefix(&lhs_prefix);
     let rhs = rhs.split_prefix(&rhs_prefix);
-    recursive_merge(&lhs, &rhs, merge)
+    recursive_merge(&lhs, &rhs, merge).map(|(merged, _)| merged)
 }
 
 /// Recursively "zips" together lhs and rhs.
@@ -182,22 +182,20 @@ fn recursive_merge(
     lhs: &ast::UseTree,
     rhs: &ast::UseTree,
     merge: MergeBehaviour,
-) -> Option<ast::UseTree> {
+) -> Option<(ast::UseTree, bool)> {
     let mut use_trees = lhs
         .use_tree_list()
         .into_iter()
         .flat_map(|list| list.use_trees())
-        // we use Option here to early return from this function(this is not the same as a `filter` op)
-        .map(|tree| match merge.is_tree_allowed(&tree) {
-            true => Some(tree),
-            false => None,
+        // check if any of the use trees are nested, if they are and the behaviour is `last` we are not allowed to merge this
+        // so early exit the iterator by using Option's Intoiterator impl
+        .map(|tree| match merge == MergeBehaviour::Last && tree.use_tree_list().is_some() {
+            true => None,
+            false => Some(tree),
         })
         .collect::<Option<Vec<_>>>()?;
     use_trees.sort_unstable_by(|a, b| path_cmp_opt(a.path(), b.path()));
     for rhs_t in rhs.use_tree_list().into_iter().flat_map(|list| list.use_trees()) {
-        if !merge.is_tree_allowed(&rhs_t) {
-            return None;
-        }
         let rhs_path = rhs_t.path();
         match use_trees.binary_search_by(|p| path_cmp_opt(p.path(), rhs_path.clone())) {
             Ok(idx) => {
@@ -241,9 +239,17 @@ fn recursive_merge(
                 }
                 let lhs = lhs_t.split_prefix(&lhs_prefix);
                 let rhs = rhs_t.split_prefix(&rhs_prefix);
+                let this_has_children = use_trees.len() > 0;
                 match recursive_merge(&lhs, &rhs, merge) {
-                    Some(use_tree) => use_trees[idx] = use_tree,
-                    None => return None,
+                    Some((_, has_multiple_children))
+                        if merge == MergeBehaviour::Last
+                            && this_has_children
+                            && has_multiple_children =>
+                    {
+                        return None
+                    }
+                    Some((use_tree, _)) => use_trees[idx] = use_tree,
+                    None => use_trees.insert(idx, rhs_t),
                 }
             }
             Err(_)
@@ -258,7 +264,8 @@ fn recursive_merge(
             }
         }
     }
-    Some(lhs.with_use_tree_list(make::use_tree_list(use_trees)))
+    let has_multiple_children = use_trees.len() > 1;
+    Some((lhs.with_use_tree_list(make::use_tree_list(use_trees)), has_multiple_children))
 }
 
 /// Traverses both paths until they differ, returning the common prefix of both.
@@ -301,10 +308,6 @@ fn segment_iter(path: &ast::Path) -> impl Iterator<Item = ast::PathSegment> + Cl
     successors(first_segment(path), |p| p.parent_path().parent_path().and_then(|p| p.segment()))
 }
 
-fn path_len(path: ast::Path) -> usize {
-    segment_iter(&path).count()
-}
-
 /// Orders paths in the following way:
 /// the sole self token comes first, after that come uppercase identifiers, then lowercase identifiers
 // FIXME: rustfmt sort lowercase idents before uppercase, in general we want to have the same ordering rustfmt has
@@ -349,19 +352,6 @@ pub enum MergeBehaviour {
     Last,
 }
 
-impl MergeBehaviour {
-    #[inline]
-    fn is_tree_allowed(&self, tree: &ast::UseTree) -> bool {
-        match self {
-            MergeBehaviour::Full => true,
-            // only simple single segment paths are allowed
-            MergeBehaviour::Last => {
-                tree.use_tree_list().is_none() && tree.path().map(path_len) <= Some(1)
-            }
-        }
-    }
-}
-
 #[derive(Eq, PartialEq, PartialOrd, Ord)]
 enum ImportGroup {
     // the order here defines the order of new group inserts
@@ -685,11 +675,6 @@ use std::io;",
         )
     }
 
-    #[test]
-    fn merge_last_into_self() {
-        check_last("foo::bar::baz", r"use foo::bar;", r"use foo::bar::{self, baz};");
-    }
-
     #[test]
     fn merge_groups_full() {
         check_full(
@@ -834,24 +819,8 @@ use std::io;",
     }
 
     #[test]
-    fn skip_merge_last_too_long() {
-        check_last(
-            "foo::bar",
-            r"use foo::bar::baz::Qux;",
-            r"use foo::bar;
-use foo::bar::baz::Qux;",
-        );
-    }
-
-    #[test]
-    #[ignore] // FIXME: Order changes when switching lhs and rhs
-    fn skip_merge_last_too_long2() {
-        check_last(
-            "foo::bar::baz::Qux",
-            r"use foo::bar;",
-            r"use foo::bar;
-use foo::bar::baz::Qux;",
-        );
+    fn merge_last_too_long() {
+        check_last("foo::bar", r"use foo::bar::baz::Qux;", r"use foo::bar::{self, baz::Qux};");
     }
 
     #[test]
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 6d5fd1a557..31f2bcba35 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -144,6 +144,10 @@ impl AnalysisHost {
         self.db.apply_change(change)
     }
 
+    pub fn maybe_collect_garbage(&mut self) {
+        self.db.maybe_collect_garbage();
+    }
+
     pub fn collect_garbage(&mut self) {
         self.db.collect_garbage();
     }
@@ -216,8 +220,8 @@ impl Analysis {
     }
 
     /// Debug info about the current state of the analysis.
-    pub fn status(&self, file_id: Option<FileId>) -> Cancelable<String> {
-        self.with_db(|db| status::status(&*db, file_id))
+    pub fn status(&self) -> Cancelable<String> {
+        self.with_db(|db| status::status(&*db))
     }
 
     pub fn prime_caches(&self, files: Vec<FileId>) -> Cancelable<()> {
diff --git a/crates/ide/src/status.rs b/crates/ide/src/status.rs
index 0af84daa0a..c237081818 100644
--- a/crates/ide/src/status.rs
+++ b/crates/ide/src/status.rs
@@ -2,19 +2,19 @@ use std::{fmt, iter::FromIterator, sync::Arc};
 
 use base_db::{
     salsa::debug::{DebugQueryTable, TableEntry},
-    CrateId, FileId, FileTextQuery, SourceDatabase, SourceRootId,
+    FileTextQuery, SourceRootId,
 };
 use hir::MacroFile;
 use ide_db::{
     symbol_index::{LibrarySymbolsQuery, SymbolIndex},
     RootDatabase,
 };
-use itertools::Itertools;
 use profile::{memory_usage, Bytes};
 use rustc_hash::FxHashMap;
-use stdx::format_to;
 use syntax::{ast, Parse, SyntaxNode};
 
+use crate::FileId;
+
 fn syntax_tree_stats(db: &RootDatabase) -> SyntaxTreeStats {
     base_db::ParseQuery.in_db(db).entries::<SyntaxTreeStats>()
 }
@@ -31,36 +31,20 @@ fn macro_syntax_tree_stats(db: &RootDatabase) -> SyntaxTreeStats {
 //
 // | VS Code | **Rust Analyzer: Status**
 // |===
-pub(crate) fn status(db: &RootDatabase, file_id: Option<FileId>) -> String {
-    let mut buf = String::new();
-    format_to!(buf, "{}\n", FileTextQuery.in_db(db).entries::<FilesStats>());
-    format_to!(buf, "{}\n", LibrarySymbolsQuery.in_db(db).entries::<LibrarySymbolsStats>());
-    format_to!(buf, "{}\n", syntax_tree_stats(db));
-    format_to!(buf, "{} (macros)\n", macro_syntax_tree_stats(db));
-    format_to!(buf, "{} total\n", memory_usage());
-
-    if let Some(file_id) = file_id {
-        format_to!(buf, "\nfile info:\n");
-        let krate = crate::parent_module::crate_for(db, file_id).pop();
-        match krate {
-            Some(krate) => {
-                let crate_graph = db.crate_graph();
-                let display_crate = |krate: CrateId| match &crate_graph[krate].display_name {
-                    Some(it) => format!("{}({:?})", it, krate),
-                    None => format!("{:?}", krate),
-                };
-                format_to!(buf, "crate: {}\n", display_crate(krate));
-                let deps = crate_graph[krate]
-                    .dependencies
-                    .iter()
-                    .map(|dep| format!("{}={:?}", dep.name, dep.crate_id))
-                    .format(", ");
-                format_to!(buf, "deps: {}\n", deps);
-            }
-            None => format_to!(buf, "does not belong to any crate"),
-        }
-    }
-    buf
+pub(crate) fn status(db: &RootDatabase) -> String {
+    let files_stats = FileTextQuery.in_db(db).entries::<FilesStats>();
+    let syntax_tree_stats = syntax_tree_stats(db);
+    let macro_syntax_tree_stats = macro_syntax_tree_stats(db);
+    let symbols_stats = LibrarySymbolsQuery.in_db(db).entries::<LibrarySymbolsStats>();
+    format!(
+        "{}\n{}\n{}\n{} (macros)\n\n\nmemory:\n{}\ngc {:?} seconds ago",
+        files_stats,
+        symbols_stats,
+        syntax_tree_stats,
+        macro_syntax_tree_stats,
+        memory_usage(),
+        db.last_gc.elapsed().as_secs(),
+    )
 }
 
 #[derive(Default)]
@@ -137,7 +121,7 @@ struct LibrarySymbolsStats {
 
 impl fmt::Display for LibrarySymbolsStats {
     fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
-        write!(fmt, "{} ({}) index symbols", self.total, self.size)
+        write!(fmt, "{} ({}) symbols", self.total, self.size)
     }
 }
 
diff --git a/crates/ide_db/src/change.rs b/crates/ide_db/src/change.rs
index 7f98111c48..8b4fd7ab84 100644
--- a/crates/ide_db/src/change.rs
+++ b/crates/ide_db/src/change.rs
@@ -1,7 +1,7 @@
 //! Defines a unit of change that can applied to a state of IDE to get the next
 //! state. Changes are transactional.
 
-use std::{fmt, sync::Arc};
+use std::{fmt, sync::Arc, time};
 
 use base_db::{
     salsa::{Database, Durability, SweepStrategy},
@@ -81,6 +81,8 @@ impl fmt::Debug for RootChange {
     }
 }
 
+const GC_COOLDOWN: time::Duration = time::Duration::from_millis(100);
+
 impl RootDatabase {
     pub fn request_cancellation(&mut self) {
         let _p = profile::span("RootDatabase::request_cancellation");
@@ -124,12 +126,23 @@ impl RootDatabase {
         }
     }
 
+    pub fn maybe_collect_garbage(&mut self) {
+        if cfg!(feature = "wasm") {
+            return;
+        }
+
+        if self.last_gc_check.elapsed() > GC_COOLDOWN {
+            self.last_gc_check = crate::wasm_shims::Instant::now();
+        }
+    }
+
     pub fn collect_garbage(&mut self) {
         if cfg!(feature = "wasm") {
             return;
         }
 
         let _p = profile::span("RootDatabase::collect_garbage");
+        self.last_gc = crate::wasm_shims::Instant::now();
 
         let sweep = SweepStrategy::default().discard_values().sweep_all_revisions();
 
diff --git a/crates/ide_db/src/lib.rs b/crates/ide_db/src/lib.rs
index 0d209c6ec0..70ada02f31 100644
--- a/crates/ide_db/src/lib.rs
+++ b/crates/ide_db/src/lib.rs
@@ -10,6 +10,7 @@ pub mod defs;
 pub mod search;
 pub mod imports_locator;
 pub mod source_change;
+mod wasm_shims;
 
 use std::{fmt, sync::Arc};
 
@@ -35,6 +36,8 @@ use crate::{line_index::LineIndex, symbol_index::SymbolsDatabase};
 )]
 pub struct RootDatabase {
     storage: salsa::Storage<RootDatabase>,
+    pub last_gc: crate::wasm_shims::Instant,
+    pub last_gc_check: crate::wasm_shims::Instant,
 }
 
 impl fmt::Debug for RootDatabase {
@@ -96,7 +99,11 @@ impl Default for RootDatabase {
 
 impl RootDatabase {
     pub fn new(lru_capacity: Option<usize>) -> RootDatabase {
-        let mut db = RootDatabase { storage: salsa::Storage::default() };
+        let mut db = RootDatabase {
+            storage: salsa::Storage::default(),
+            last_gc: crate::wasm_shims::Instant::now(),
+            last_gc_check: crate::wasm_shims::Instant::now(),
+        };
         db.set_crate_graph_with_durability(Default::default(), Durability::HIGH);
         db.set_local_roots_with_durability(Default::default(), Durability::HIGH);
         db.set_library_roots_with_durability(Default::default(), Durability::HIGH);
@@ -114,7 +121,11 @@ impl RootDatabase {
 
 impl salsa::ParallelDatabase for RootDatabase {
     fn snapshot(&self) -> salsa::Snapshot<RootDatabase> {
-        salsa::Snapshot::new(RootDatabase { storage: self.storage.snapshot() })
+        salsa::Snapshot::new(RootDatabase {
+            storage: self.storage.snapshot(),
+            last_gc: self.last_gc,
+            last_gc_check: self.last_gc_check,
+        })
     }
 }
 
diff --git a/crates/ide_db/src/wasm_shims.rs b/crates/ide_db/src/wasm_shims.rs
new file mode 100644
index 0000000000..7af9f9d9bb
--- /dev/null
+++ b/crates/ide_db/src/wasm_shims.rs
@@ -0,0 +1,19 @@
+//! A version of `std::time::Instant` that doesn't panic in WASM.
+
+#[cfg(not(feature = "wasm"))]
+pub use std::time::Instant;
+
+#[cfg(feature = "wasm")]
+#[derive(Clone, Copy, Debug)]
+pub struct Instant;
+
+#[cfg(feature = "wasm")]
+impl Instant {
+    pub fn now() -> Self {
+        Self
+    }
+
+    pub fn elapsed(&self) -> std::time::Duration {
+        std::time::Duration::new(0, 0)
+    }
+}
diff --git a/crates/rust-analyzer/src/dispatch.rs b/crates/rust-analyzer/src/dispatch.rs
index 9c8815e29e..891fdb96d1 100644
--- a/crates/rust-analyzer/src/dispatch.rs
+++ b/crates/rust-analyzer/src/dispatch.rs
@@ -1,5 +1,5 @@
 //! A visitor for downcasting arbitrary request (JSON) into a specific type.
-use std::{fmt, panic};
+use std::panic;
 
 use serde::{de::DeserializeOwned, Serialize};
 
@@ -23,7 +23,7 @@ impl<'a> RequestDispatcher<'a> {
     ) -> Result<&mut Self>
     where
         R: lsp_types::request::Request + 'static,
-        R::Params: DeserializeOwned + panic::UnwindSafe + fmt::Debug + 'static,
+        R::Params: DeserializeOwned + panic::UnwindSafe + 'static,
         R::Result: Serialize + 'static,
     {
         let (id, params) = match self.parse::<R>() {
@@ -34,7 +34,6 @@ impl<'a> RequestDispatcher<'a> {
         };
         let world = panic::AssertUnwindSafe(&mut *self.global_state);
         let response = panic::catch_unwind(move || {
-            stdx::panic_context::enter(format!("request: {} {:#?}", R::METHOD, params));
             let result = f(world.0, params);
             result_to_response::<R>(id, result)
         })
@@ -50,7 +49,7 @@ impl<'a> RequestDispatcher<'a> {
     ) -> Result<&mut Self>
     where
         R: lsp_types::request::Request + 'static,
-        R::Params: DeserializeOwned + Send + fmt::Debug + 'static,
+        R::Params: DeserializeOwned + Send + 'static,
         R::Result: Serialize + 'static,
     {
         let (id, params) = match self.parse::<R>() {
@@ -62,10 +61,7 @@ impl<'a> RequestDispatcher<'a> {
 
         self.global_state.task_pool.handle.spawn({
             let world = self.global_state.snapshot();
-
             move || {
-                let _ctx =
-                    stdx::panic_context::enter(format!("request: {} {:#?}", R::METHOD, params));
                 let result = f(world, params);
                 Task::Response(result_to_response::<R>(id, result))
             }
@@ -160,7 +156,6 @@ impl<'a> NotificationDispatcher<'a> {
                 return Ok(self);
             }
         };
-        stdx::panic_context::enter(format!("notification: {}", N::METHOD));
         f(self.global_state, params)?;
         Ok(self)
     }
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index afcec63ad1..7ac1a30f6c 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -38,22 +38,10 @@ use crate::{
     to_proto, LspError, Result,
 };
 
-pub(crate) fn handle_analyzer_status(
-    snap: GlobalStateSnapshot,
-    params: lsp_ext::AnalyzerStatusParams,
-) -> Result<String> {
+pub(crate) fn handle_analyzer_status(snap: GlobalStateSnapshot, _: ()) -> Result<String> {
     let _p = profile::span("handle_analyzer_status");
 
     let mut buf = String::new();
-
-    let mut file_id = None;
-    if let Some(tdi) = params.text_document {
-        match from_proto::file_id(&snap, &tdi.uri) {
-            Ok(it) => file_id = Some(it),
-            Err(_) => format_to!(buf, "file {} not found in vfs", tdi.uri),
-        }
-    }
-
     if snap.workspaces.is_empty() {
         buf.push_str("no workspaces\n")
     } else {
@@ -64,10 +52,7 @@ pub(crate) fn handle_analyzer_status(
     }
     buf.push_str("\nanalysis:\n");
     buf.push_str(
-        &snap
-            .analysis
-            .status(file_id)
-            .unwrap_or_else(|_| "Analysis retrieval was cancelled".to_owned()),
+        &snap.analysis.status().unwrap_or_else(|_| "Analysis retrieval was cancelled".to_owned()),
     );
     format_to!(buf, "\n\nrequests:\n");
     let requests = snap.latest_requests.read();
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 43ff191dab..e1a28b1b4b 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -11,17 +11,11 @@ use serde::{Deserialize, Serialize};
 pub enum AnalyzerStatus {}
 
 impl Request for AnalyzerStatus {
-    type Params = AnalyzerStatusParams;
+    type Params = ();
     type Result = String;
     const METHOD: &'static str = "rust-analyzer/analyzerStatus";
 }
 
-#[derive(Deserialize, Serialize, Debug)]
-#[serde(rename_all = "camelCase")]
-pub struct AnalyzerStatusParams {
-    pub text_document: Option<TextDocumentIdentifier>,
-}
-
 pub enum MemoryUsage {}
 
 impl Request for MemoryUsage {
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index c2d0ac791b..06ab9d508d 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -189,16 +189,19 @@ impl GlobalState {
                 }
                 lsp_server::Message::Response(resp) => self.complete_request(resp),
             },
-            Event::Task(task) => match task {
-                Task::Response(response) => self.respond(response),
-                Task::Diagnostics(diagnostics_per_file) => {
-                    for (file_id, diagnostics) in diagnostics_per_file {
-                        self.diagnostics.set_native_diagnostics(file_id, diagnostics)
+            Event::Task(task) => {
+                match task {
+                    Task::Response(response) => self.respond(response),
+                    Task::Diagnostics(diagnostics_per_file) => {
+                        for (file_id, diagnostics) in diagnostics_per_file {
+                            self.diagnostics.set_native_diagnostics(file_id, diagnostics)
+                        }
                     }
+                    Task::Workspaces(workspaces) => self.switch_workspaces(workspaces),
+                    Task::Unit => (),
                 }
-                Task::Workspaces(workspaces) => self.switch_workspaces(workspaces),
-                Task::Unit => (),
-            },
+                self.analysis_host.maybe_collect_garbage();
+            }
             Event::Vfs(mut task) => {
                 let _p = profile::span("GlobalState::handle_event/vfs");
                 loop {
diff --git a/crates/stdx/src/lib.rs b/crates/stdx/src/lib.rs
index 011935cade..273b0f55b2 100644
--- a/crates/stdx/src/lib.rs
+++ b/crates/stdx/src/lib.rs
@@ -5,7 +5,6 @@ use std::{
 };
 
 mod macros;
-pub mod panic_context;
 
 #[inline(always)]
 pub fn is_ci() -> bool {
diff --git a/crates/stdx/src/panic_context.rs b/crates/stdx/src/panic_context.rs
deleted file mode 100644
index fd232e0cce..0000000000
--- a/crates/stdx/src/panic_context.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-//! A micro-crate to enhance panic messages with context info.
-//!
-//! FIXME: upstream to https://github.com/kriomant/panic-context ?
-
-use std::{cell::RefCell, panic, sync::Once};
-
-pub fn enter(context: String) -> impl Drop {
-    static ONCE: Once = Once::new();
-    ONCE.call_once(PanicContext::init);
-
-    with_ctx(|ctx| ctx.push(context));
-    PanicContext { _priv: () }
-}
-
-#[must_use]
-struct PanicContext {
-    _priv: (),
-}
-
-impl PanicContext {
-    fn init() {
-        let default_hook = panic::take_hook();
-        let hook = move |panic_info: &panic::PanicInfo<'_>| {
-            with_ctx(|ctx| {
-                if !ctx.is_empty() {
-                    eprintln!("Panic context:");
-                    for frame in ctx.iter() {
-                        eprintln!("> {}\n", frame)
-                    }
-                }
-                default_hook(panic_info)
-            })
-        };
-        panic::set_hook(Box::new(hook))
-    }
-}
-
-impl Drop for PanicContext {
-    fn drop(&mut self) {
-        with_ctx(|ctx| assert!(ctx.pop().is_some()))
-    }
-}
-
-fn with_ctx(f: impl FnOnce(&mut Vec<String>)) {
-    thread_local! {
-        static CTX: RefCell<Vec<String>> = RefCell::new(Vec::new());
-    }
-    CTX.with(|ctx| f(&mut *ctx.borrow_mut()))
-}
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index f1160bb1cc..2e3133449f 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -390,14 +390,7 @@ rust-analyzer supports only one `kind`, `"cargo"`. The `args` for `"cargo"` look
 
 **Method:** `rust-analyzer/analyzerStatus`
 
-**Request:**
-
-```typescript
-interface AnalyzerStatusParams {
-    /// If specified, show dependencies of the current file.
-    textDocument?: TextDocumentIdentifier;
-}
-```
+**Request:** `null`
 
 **Response:** `string`
 
diff --git a/docs/user/manual.adoc b/docs/user/manual.adoc
index c1a7788520..7d85b36cb3 100644
--- a/docs/user/manual.adoc
+++ b/docs/user/manual.adoc
@@ -113,8 +113,8 @@ Note that installing via `xtask install` does not work for VS Code Remote, inste
 
 Here are some useful self-diagnostic commands:
 
-* **Rust Analyzer: Show RA Version** shows the version of `rust-analyzer` binary.
-* **Rust Analyzer: Status** prints some statistics about the server, and dependency information for the current file.
+* **Rust Analyzer: Show RA Version** shows the version of `rust-analyzer` binary
+* **Rust Analyzer: Status** prints some statistics about the server, like the few latest LSP requests
 * To enable server-side logging, run with `env RA_LOG=info` and see `Output > Rust Analyzer Language Server` in VS Code's panel.
 * To log all LSP requests, add `"rust-analyzer.trace.server": "verbose"` to the settings and look for `Rust Analyzer Language Server Trace` in the panel.
 * To enable client-side logging, add `"rust-analyzer.trace.extension": true` to the settings and open `Output > Rust Analyzer Client` in the panel.
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 1a90f1b7d9..e9581a9b59 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -21,12 +21,7 @@ export function analyzerStatus(ctx: Ctx): Cmd {
         provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
             if (!vscode.window.activeTextEditor) return '';
 
-            const params: ra.AnalyzerStatusParams = {};
-            const doc = ctx.activeRustEditor?.document;
-            if (doc != null) {
-                params.textDocument = ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(doc);
-            }
-            return ctx.client.sendRequest(ra.analyzerStatus, params);
+            return ctx.client.sendRequest(ra.analyzerStatus);
         }
 
         get onDidChange(): vscode.Event<vscode.Uri> {
@@ -99,7 +94,7 @@ export function matchingBrace(ctx: Ctx): Cmd {
         if (!editor || !client) return;
 
         const response = await client.sendRequest(ra.matchingBrace, {
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
+            textDocument: { uri: editor.document.uri.toString() },
             positions: editor.selections.map(s =>
                 client.code2ProtocolConverter.asPosition(s.active),
             ),
@@ -123,7 +118,7 @@ export function joinLines(ctx: Ctx): Cmd {
 
         const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
             ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
+            textDocument: { uri: editor.document.uri.toString() },
         });
         editor.edit((builder) => {
             client.protocol2CodeConverter.asTextEdits(items).forEach((edit: any) => {
@@ -141,7 +136,7 @@ export function onEnter(ctx: Ctx): Cmd {
         if (!editor || !client) return false;
 
         const lcEdits = await client.sendRequest(ra.onEnter, {
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
+            textDocument: { uri: editor.document.uri.toString() },
             position: client.code2ProtocolConverter.asPosition(
                 editor.selection.active,
             ),
@@ -170,7 +165,7 @@ export function parentModule(ctx: Ctx): Cmd {
         if (!editor || !client) return;
 
         const response = await client.sendRequest(ra.parentModule, {
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
+            textDocument: { uri: editor.document.uri.toString() },
             position: client.code2ProtocolConverter.asPosition(
                 editor.selection.active,
             ),
@@ -196,7 +191,7 @@ export function ssr(ctx: Ctx): Cmd {
 
         const position = editor.selection.active;
         const selections = editor.selections;
-        const textDocument = ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document);
+        const textDocument = { uri: editor.document.uri.toString() };
 
         const options: vscode.InputBoxOptions = {
             value: "() ==>> ()",
@@ -344,7 +339,7 @@ export function expandMacro(ctx: Ctx): Cmd {
             const position = editor.selection.active;
 
             const expanded = await client.sendRequest(ra.expandMacro, {
-                textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
+                textDocument: { uri: editor.document.uri.toString() },
                 position,
             });
 
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index eb422d3e75..d167041c4c 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -4,10 +4,7 @@
 
 import * as lc from "vscode-languageclient";
 
-export interface AnalyzerStatusParams {
-    textDocument?: lc.TextDocumentIdentifier;
-}
-export const analyzerStatus = new lc.RequestType<AnalyzerStatusParams, string, void>("rust-analyzer/analyzerStatus");
+export const analyzerStatus = new lc.RequestType0<string, void>("rust-analyzer/analyzerStatus");
 export const memoryUsage = new lc.RequestType0<string, void>("rust-analyzer/memoryUsage");
 
 export type Status = "loading" | "ready" | "invalid" | "needsReload";


Test patch:
diff --git a/crates/assists/src/tests.rs b/crates/assists/src/tests.rs
index ba1fb543b8..2b687decf3 100644
--- a/crates/assists/src/tests.rs
+++ b/crates/assists/src/tests.rs
@@ -15,18 +15,30 @@ pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
 
 pub(crate) fn check_assist(assist: Handler, ra_fixture_before: &str, ra_fixture_after: &str) {
     let ra_fixture_after = trim_indent(ra_fixture_after);
-    check(assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after));
+    check(assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after), None);
+}
+
+// There is no way to choose what assist within a group you want to test against,
+// so this is here to allow you choose.
+pub(crate) fn check_assist_by_label(
+    assist: Handler,
+    ra_fixture_before: &str,
+    ra_fixture_after: &str,
+    label: &str,
+) {
+    let ra_fixture_after = trim_indent(ra_fixture_after);
+    check(assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after), Some(label));
 }
 
 // FIXME: instead of having a separate function here, maybe use
 // `extract_ranges` and mark the target as `<target> </target>` in the
 // fixture?
 pub(crate) fn check_assist_target(assist: Handler, ra_fixture: &str, target: &str) {
-    check(assist, ra_fixture, ExpectedResult::Target(target));
+    check(assist, ra_fixture, ExpectedResult::Target(target), None);
 }
 
 pub(crate) fn check_assist_not_applicable(assist: Handler, ra_fixture: &str) {
-    check(assist, ra_fixture, ExpectedResult::NotApplicable);
+    check(assist, ra_fixture, ExpectedResult::NotApplicable, None);
 }
 
 fn check_doc_test(assist_id: &str, before: &str, after: &str) {
@@ -65,7 +77,7 @@ enum ExpectedResult<'a> {
     Target(&'a str),
 }
 
-fn check(handler: Handler, before: &str, expected: ExpectedResult) {
+fn check(handler: Handler, before: &str, expected: ExpectedResult, assist_label: Option<&str>) {
     let (db, file_with_caret_id, range_or_offset) = RootDatabase::with_range_or_offset(before);
     let text_without_caret = db.file_text(file_with_caret_id).to_string();
 
@@ -77,7 +89,12 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult) {
     let mut acc = Assists::new_resolved(&ctx);
     handler(&mut acc, &ctx);
     let mut res = acc.finish_resolved();
-    let assist = res.pop();
+
+    let assist = match assist_label {
+        Some(label) => res.into_iter().find(|resolved| resolved.assist.label == label),
+        None => res.pop(),
+    };
+
     match (assist, expected) {
         (Some(assist), ExpectedResult::After(after)) => {
             let mut source_change = assist.source_change;
diff --git a/crates/assists/src/tests/generated.rs b/crates/assists/src/tests/generated.rs
index 27d15adb08..7f6e98a548 100644
--- a/crates/assists/src/tests/generated.rs
+++ b/crates/assists/src/tests/generated.rs
@@ -203,6 +203,19 @@ pub(crate) fn frobnicate() {}
     )
 }
 
+#[test]
+fn doctest_convert_integer_literal() {
+    check_doc_test(
+        "convert_integer_literal",
+        r#####"
+const _: i32 = 10<|>;
+"#####,
+        r#####"
+const _: i32 = 0b1010;
+"#####,
+    )
+}
+
 #[test]
 fn doctest_convert_to_guarded_return() {
     check_doc_test(