Repository: rust-analyzer/rust-analyzer
Pull request #10441 (2021-10-03 13:31:52)

feat: Hide type inlay hints for constructors
Fixes https://github.com/rust-analyzer/rust-analyzer/issues/3022
bors r+


Patch:
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 401feffff4..3dcb510ab0 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -2,6 +2,7 @@ use either::Either;
 use hir::{known, Callable, HasVisibility, HirDisplay, Semantics, TypeInfo};
 use ide_db::RootDatabase;
 use ide_db::{base_db::FileRange, helpers::FamousDefs};
+use itertools::Itertools;
 use stdx::to_lower_snake_case;
 use syntax::{
     ast::{self, AstNode, HasArgList, HasName},
@@ -198,28 +199,95 @@ fn get_bind_pat_hints(
 
     let descended = sema.descend_node_into_attributes(pat.clone()).pop();
     let desc_pat = descended.as_ref().unwrap_or(pat);
-    let krate = sema.scope(desc_pat.syntax()).module().map(|it| it.krate());
-    let famous_defs = FamousDefs(sema, krate);
-
     let ty = sema.type_of_pat(&desc_pat.clone().into())?.original;
 
     if should_not_display_type_hint(sema, &pat, &ty) {
         return None;
     }
 
+    let krate = sema.scope(desc_pat.syntax()).module().map(|it| it.krate());
+    let famous_defs = FamousDefs(sema, krate);
+    let label = hint_iterator(sema, &famous_defs, config, &ty);
+
+    let label = match label {
+        Some(label) => label,
+        None => {
+            let ty_name = ty.display_truncated(sema.db, config.max_length).to_string();
+            if is_named_constructor(sema, pat, &ty_name).is_some() {
+                return None;
+            }
+            ty_name.into()
+        }
+    };
+
     acc.push(InlayHint {
         range: match pat.name() {
             Some(name) => name.syntax().text_range(),
             None => pat.syntax().text_range(),
         },
         kind: InlayKind::TypeHint,
-        label: hint_iterator(sema, &famous_defs, config, &ty)
-            .unwrap_or_else(|| ty.display_truncated(sema.db, config.max_length).to_string().into()),
+        label,
     });
 
     Some(())
 }
 
+fn is_named_constructor(
+    sema: &Semantics<RootDatabase>,
+    pat: &ast::IdentPat,
+    ty_name: &str,
+) -> Option<()> {
+    let let_node = pat.syntax().parent()?;
+    let expr = match_ast! {
+        match let_node {
+            ast::LetStmt(it) => it.initializer(),
+            ast::Condition(it) => it.expr(),
+            _ => None,
+        }
+    }?;
+
+    let expr = sema.descend_node_into_attributes(expr.clone()).pop().unwrap_or(expr);
+    // unwrap postfix expressions
+    let expr = match expr {
+        ast::Expr::TryExpr(it) => it.expr(),
+        ast::Expr::AwaitExpr(it) => it.expr(),
+        expr => Some(expr),
+    }?;
+    let expr = match expr {
+        ast::Expr::CallExpr(call) => match call.expr()? {
+            ast::Expr::PathExpr(p) => p,
+            _ => return None,
+        },
+        _ => return None,
+    };
+    let path = expr.path()?;
+
+    // Check for tuple-struct or tuple-variant in which case we can check the last segment
+    let callable = sema.type_of_expr(&ast::Expr::PathExpr(expr))?.original.as_callable(sema.db);
+    let callable_kind = callable.map(|it| it.kind());
+    if let Some(hir::CallableKind::TupleStruct(_) | hir::CallableKind::TupleEnumVariant(_)) =
+        callable_kind
+    {
+        if let Some(ctor) = path.segment() {
+            return (&ctor.to_string() == ty_name).then(|| ());
+        }
+    }
+
+    // otherwise use the qualifying segment as the constructor name
+    let qual_seg = path.qualifier()?.segment()?;
+    let ctor_name = match qual_seg.kind()? {
+        ast::PathSegmentKind::Name(name_ref) => {
+            match qual_seg.generic_arg_list().map(|it| it.generic_args()) {
+                Some(generics) => format!("{}<{}>", name_ref, generics.format(", ")),
+                None => name_ref.to_string(),
+            }
+        }
+        ast::PathSegmentKind::Type { type_ref: Some(ty), trait_ref: None } => ty.to_string(),
+        _ => return None,
+    };
+    (&ctor_name == ty_name).then(|| ())
+}
+
 /// Checks if the type is an Iterator from std::iter and replaces its hint with an `impl Iterator<Item = Ty>`.
 fn hint_iterator(
     sema: &Semantics<RootDatabase>,
@@ -470,10 +538,12 @@ mod tests {
         max_length: None,
     };
 
+    #[track_caller]
     fn check(ra_fixture: &str) {
         check_with_config(TEST_CONFIG, ra_fixture);
     }
 
+    #[track_caller]
     fn check_params(ra_fixture: &str) {
         check_with_config(
             InlayHintsConfig {
@@ -486,6 +556,7 @@ mod tests {
         );
     }
 
+    #[track_caller]
     fn check_types(ra_fixture: &str) {
         check_with_config(
             InlayHintsConfig {
@@ -498,6 +569,7 @@ mod tests {
         );
     }
 
+    #[track_caller]
     fn check_chains(ra_fixture: &str) {
         check_with_config(
             InlayHintsConfig {
@@ -510,6 +582,7 @@ mod tests {
         );
     }
 
+    #[track_caller]
     fn check_with_config(config: InlayHintsConfig, ra_fixture: &str) {
         let (analysis, file_id) = fixture::file(&ra_fixture);
         let expected = extract_annotations(&*analysis.file_text(file_id).unwrap());
@@ -519,6 +592,7 @@ mod tests {
         assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual);
     }
 
+    #[track_caller]
     fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
         let (analysis, file_id) = fixture::file(&ra_fixture);
         let inlay_hints = analysis.inlay_hints(&config, file_id).unwrap();
@@ -1191,11 +1265,12 @@ trait Display {}
 trait Sync {}
 
 fn main() {
-    let _v = Vec::<Box<&(dyn Display + Sync)>>::new();
+    // The block expression wrapping disables the constructor hint hiding logic
+    let _v = { Vec::<Box<&(dyn Display + Sync)>>::new() };
       //^^ Vec<Box<&(dyn Display + Sync)>>
-    let _v = Vec::<Box<*const (dyn Display + Sync)>>::new();
+    let _v = { Vec::<Box<*const (dyn Display + Sync)>>::new() };
       //^^ Vec<Box<*const (dyn Display + Sync)>>
-    let _v = Vec::<Box<dyn Display + Sync>>::new();
+    let _v = { Vec::<Box<dyn Display + Sync>>::new() };
       //^^ Vec<Box<dyn Display + Sync>>
 }
 "#,
@@ -1234,6 +1309,48 @@ fn main() {
         );
     }
 
+    #[test]
+    fn skip_constructor_type_hints() {
+        check_types(
+            r#"
+//- minicore: try
+use core::ops::ControlFlow;
+
+struct Struct;
+struct TupleStruct();
+
+impl Struct {
+    fn new() -> Self {
+        Struct
+    }
+    fn try_new() -> ControlFlow<(), Self> {
+        ControlFlow::Continue(Struct)
+    }
+}
+
+struct Generic<T>(T);
+impl Generic<i32> {
+    fn new() -> Self {
+        Generic(0)
+    }
+}
+
+fn main() {
+    let strukt = Struct::new();
+    let tuple_struct = TupleStruct();
+    let generic0 = Generic::new();
+     // ^^^^^^^^ Generic<i32>
+    let generic1 = Generic::<i32>::new();
+    let generic2 = <Generic<i32>>::new();
+}
+
+fn fallible() -> ControlFlow<()> {
+    let strukt = Struct::try_new()?;
+}
+"#,
+        );
+    }
+
     #[test]
     fn closures() {
         check(
diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs
index cce342272f..a495e6c543 100644
--- a/crates/ide/src/rename.rs
+++ b/crates/ide/src/rename.rs
@@ -1899,51 +1899,6 @@ fn func$0() {
 fn function() {
     function();
 }
-"#,
-        )
-    }
-
-    #[test]
-    fn in_macro_multi_mapping() {
-        check(
-            "a",
-            r#"
-fn foo() {
-    macro_rules! match_ast2 {
-        ($node:ident {
-            $( $res:expr, )*
-        }) => {{
-            $( if $node { $res } else )*
-            { loop {} }
-        }};
-    }
-    let $0d = 3;
-    match_ast2! {
-        d {
-            d,
-            d,
-        }
-    };
-}
-"#,
-            r#"
-fn foo() {
-    macro_rules! match_ast2 {
-        ($node:ident {
-            $( $res:expr, )*
-        }) => {{
-            $( if $node { $res } else )*
-            { loop {} }
-        }};
-    }
-    let a = 3;
-    match_ast2! {
-        a {
-            a,
-            a,
-        }
-    };
-}
 "#,
         )
     }
diff --git a/crates/ide_assists/src/handlers/extract_function.rs b/crates/ide_assists/src/handlers/extract_function.rs
index 2e91f8d4ab..585ef31daf 100644
--- a/crates/ide_assists/src/handlers/extract_function.rs
+++ b/crates/ide_assists/src/handlers/extract_function.rs
@@ -1090,7 +1090,7 @@ fn make_call(ctx: &AssistContext, fun: &Function, indent: IndentLevel) -> String
 
     let args = make::arg_list(fun.params.iter().map(|param| param.to_arg(ctx)));
     let name = fun.name.clone();
-    let mut call_expr = if fun.self_param.is_some() {
+    let call_expr = if fun.self_param.is_some() {
         let self_arg = make::expr_path(make::ext::ident_path("self"));
         make::expr_method_call(self_arg, name, args)
     } else {
@@ -1100,9 +1100,6 @@ fn make_call(ctx: &AssistContext, fun: &Function, indent: IndentLevel) -> String
 
     let handler = FlowHandler::from_ret_ty(fun, &ret_ty);
 
-    if fun.control_flow.is_async {
-        call_expr = make::expr_await(call_expr);
-    }
     let expr = handler.make_call_expr(call_expr).indent(indent);
 
     let mut_modifier = |var: &OutlivedLocal| if var.mut_usage_outside_body { "mut " } else { "" };
@@ -1122,8 +1119,10 @@ fn make_call(ctx: &AssistContext, fun: &Function, indent: IndentLevel) -> String
             buf.push_str(") = ");
         }
     }
-
     format_to!(buf, "{}", expr);
+    if fun.control_flow.is_async {
+        buf.push_str(".await");
+    }
     let insert_comma = fun
         .body
         .parent()
@@ -3871,70 +3870,6 @@ async fn $0fun_name() {
 
 async fn some_function() {
 
-}
-"#,
-        );
-    }
-
-    #[test]
-    fn extract_with_await_and_result_not_producing_match_expr() {
-        check_assist(
-            extract_function,
-            r#"
-async fn foo() -> Result<(), ()> {
-    $0async {}.await;
-    Err(())?$0
-}
-"#,
-            r#"
-async fn foo() -> Result<(), ()> {
-    fun_name().await?
-}
-
-async fn $0fun_name() -> _ {
-    async {}.await;
-    Err(())?
-}
-"#,
-        );
-    }
-
-    #[test]
-    fn extract_with_await_and_result_producing_match_expr() {
-        check_assist(
-            extract_function,
-            r#"
-async fn foo() -> i32 {
-    loop {
-        let n = 1;$0
-        let k = async { 1 }.await;
-        if k == 42 {
-            break 3;
-        }
-        let m = k + 1;$0
-        let h = 1 + m;
-    }
-}
-"#,
-            r#"
-async fn foo() -> i32 {
-    loop {
-        let n = 1;
-        let m = match fun_name().await {
-            Ok(value) => value,
-            Err(value) => break value,
-        };
-        let h = 1 + m;
-    }
-}
-
-async fn $0fun_name() -> Result<i32, i32> {
-    let k = async { 1 }.await;
-    if k == 42 {
-        return Err(3);
-    }
-    let m = k + 1;
-    Ok(m)
 }
 "#,
         );
diff --git a/crates/ide_assists/src/handlers/extract_variable.rs b/crates/ide_assists/src/handlers/extract_variable.rs
index 55e5da7fe7..9b4e1380a4 100644
--- a/crates/ide_assists/src/handlers/extract_variable.rs
+++ b/crates/ide_assists/src/handlers/extract_variable.rs
@@ -1,7 +1,6 @@
 use stdx::format_to;
 use syntax::{
     ast::{self, AstNode},
-    NodeOrToken,
     SyntaxKind::{
         BLOCK_EXPR, BREAK_EXPR, CLOSURE_EXPR, COMMENT, LOOP_EXPR, MATCH_ARM, MATCH_GUARD,
         PATH_EXPR, RETURN_EXPR,
@@ -31,26 +30,20 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext) -> Option
     if ctx.frange.range.is_empty() {
         return None;
     }
-    let node = match ctx.covering_element() {
-        NodeOrToken::Node(it) => it,
-        NodeOrToken::Token(it) if it.kind() == COMMENT => {
-            cov_mark::hit!(extract_var_in_comment_is_not_applicable);
-            return None;
-        }
-        NodeOrToken::Token(it) => it.parent()?,
-    };
-    let node = node.ancestors().take_while(|anc| anc.text_range() == node.text_range()).last()?;
+    let node = ctx.covering_element();
+    if node.kind() == COMMENT {
+        cov_mark::hit!(extract_var_in_comment_is_not_applicable);
+        return None;
+    }
     let to_extract = node
-        .descendants()
-        .take_while(|it| ctx.frange.range.contains_range(it.text_range()))
+        .ancestors()
+        .take_while(|it| it.text_range().contains_range(ctx.frange.range))
         .find_map(valid_target_expr)?;
-
     if let Some(ty_info) = ctx.sema.type_of_expr(&to_extract) {
         if ty_info.adjusted().is_unit() {
             return None;
         }
     }
-
     let anchor = Anchor::from(&to_extract)?;
     let indent = anchor.syntax().prev_sibling_or_token()?.as_token()?.clone();
     let target = to_extract.syntax().text_range();
@@ -153,11 +146,8 @@ enum Anchor {
 
 impl Anchor {
     fn from(to_extract: &ast::Expr) -> Option<Anchor> {
-        to_extract
-            .syntax()
-            .ancestors()
-            .take_while(|it| !ast::Item::can_cast(it.kind()) || ast::MacroCall::can_cast(it.kind()))
-            .find_map(|node| {
+        to_extract.syntax().ancestors().take_while(|it| !ast::Item::can_cast(it.kind())).find_map(
+            |node| {
                 if let Some(expr) =
                     node.parent().and_then(ast::StmtList::cast).and_then(|it| it.tail_expr())
                 {
@@ -191,7 +181,8 @@ impl Anchor {
                     return Some(Anchor::Before(node));
                 }
                 None
-            })
+            },
+        )
     }
 
     fn syntax(&self) -> &SyntaxNode {
diff --git a/crates/ide_assists/src/handlers/replace_if_let_with_match.rs b/crates/ide_assists/src/handlers/replace_if_let_with_match.rs
index 22675fbee0..3a89c2b996 100644
--- a/crates/ide_assists/src/handlers/replace_if_let_with_match.rs
+++ b/crates/ide_assists/src/handlers/replace_if_let_with_match.rs
@@ -210,7 +210,11 @@ pub(crate) fn replace_match_with_if_let(acc: &mut Assists, ctx: &AssistContext)
                 ast::Expr::BlockExpr(block) => block,
                 expr => make::block_expr(iter::empty(), Some(expr)),
             };
-            let else_expr = if is_empty_expr(&else_expr) { None } else { Some(else_expr) };
+            let else_expr = match else_expr {
+                ast::Expr::BlockExpr(block) if block.is_empty() => None,
+                ast::Expr::TupleExpr(tuple) if tuple.fields().next().is_none() => None,
+                expr => Some(expr),
+            };
             let if_let_expr = make::expr_if(
                 condition,
                 then_block,
@@ -253,10 +257,7 @@ fn pick_pattern_and_expr_order(
 
 fn is_empty_expr(expr: &ast::Expr) -> bool {
     match expr {
-        ast::Expr::BlockExpr(expr) => match expr.stmt_list() {
-            Some(it) => it.statements().next().is_none() && it.tail_expr().is_none(),
-            None => true,
-        },
+        ast::Expr::BlockExpr(expr) => expr.is_empty(),
         ast::Expr::TupleExpr(expr) => expr.fields().next().is_none(),
         _ => false,
     }
diff --git a/crates/ide_db/src/rename.rs b/crates/ide_db/src/rename.rs
index fcf1a654b0..0ca0dc8c6b 100644
--- a/crates/ide_db/src/rename.rs
+++ b/crates/ide_db/src/rename.rs
@@ -291,26 +291,23 @@ pub fn source_edit_from_references(
     new_name: &str,
 ) -> TextEdit {
     let mut edit = TextEdit::builder();
-    // macros can cause multiple refs to occur for the same text range, so keep track of what we have edited so far
-    let mut edited_ranges = Vec::new();
-    for &FileReference { range, ref name, .. } in references {
-        let has_emitted_edit = match name {
+    for reference in references {
+        let has_emitted_edit = match &reference.name {
             // if the ranges differ then the node is inside a macro call, we can't really attempt
             // to make special rewrites like shorthand syntax and such, so just rename the node in
             // the macro input
-            ast::NameLike::NameRef(name_ref) if name_ref.syntax().text_range() == range => {
+            ast::NameLike::NameRef(name_ref)
+                if name_ref.syntax().text_range() == reference.range =>
+            {
                 source_edit_from_name_ref(&mut edit, name_ref, new_name, def)
             }
-            ast::NameLike::Name(name) if name.syntax().text_range() == range => {
+            ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => {
                 source_edit_from_name(&mut edit, name, new_name)
             }
             _ => false,
         };
         if !has_emitted_edit {
-            if !edited_ranges.contains(&range.start()) {
-                edit.replace(range, new_name.to_string());
-                edited_ranges.push(range.start());
-            }
+            edit.replace(reference.range, new_name.to_string());
         }
     }
 
diff --git a/crates/mbe/src/expander/matcher.rs b/crates/mbe/src/expander/matcher.rs
index 06c21dfd70..7d8f97c775 100644
--- a/crates/mbe/src/expander/matcher.rs
+++ b/crates/mbe/src/expander/matcher.rs
@@ -63,7 +63,7 @@ use std::rc::Rc;
 
 use crate::{
     expander::{Binding, Bindings, Fragment},
-    parser::{Op, RepeatKind, Separator},
+    parser::{Op, OpDelimited, OpDelimitedIter, RepeatKind, Separator},
     tt_iter::TtIter,
     ExpandError, MetaTemplate,
 };
@@ -750,64 +750,6 @@ fn collect_vars(buf: &mut Vec<SmolStr>, pattern: &MetaTemplate) {
     }
 }
 
-impl MetaTemplate {
-    fn iter_delimited<'a>(&'a self, delimited: Option<&'a tt::Delimiter>) -> OpDelimitedIter<'a> {
-        OpDelimitedIter { inner: &self.0, idx: 0, delimited }
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-enum OpDelimited<'a> {
-    Op(&'a Op),
-    Open,
-    Close,
-}
-
-#[derive(Debug, Clone, Copy)]
-struct OpDelimitedIter<'a> {
-    inner: &'a [Op],
-    delimited: Option<&'a tt::Delimiter>,
-    idx: usize,
-}
-
-impl<'a> OpDelimitedIter<'a> {
-    fn is_eof(&self) -> bool {
-        let len = self.inner.len() + if self.delimited.is_some() { 2 } else { 0 };
-        self.idx >= len
-    }
-
-    fn peek(&self) -> Option<OpDelimited<'a>> {
-        match self.delimited {
-            None => self.inner.get(self.idx).map(OpDelimited::Op),
-            Some(_) => match self.idx {
-                0 => Some(OpDelimited::Open),
-                i if i == self.inner.len() + 1 => Some(OpDelimited::Close),
-                i => self.inner.get(i - 1).map(OpDelimited::Op),
-            },
-        }
-    }
-
-    fn reset(&self) -> Self {
-        Self { inner: self.inner, idx: 0, delimited: self.delimited }
-    }
-}
-
-impl<'a> Iterator for OpDelimitedIter<'a> {
-    type Item = OpDelimited<'a>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        let res = self.peek();
-        self.idx += 1;
-        res
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        let len = self.inner.len() + if self.delimited.is_some() { 2 } else { 0 };
-        let remain = len.saturating_sub(self.idx);
-        (remain, Some(remain))
-    }
-}
-
 impl<'a> TtIter<'a> {
     fn expect_separator(&mut self, separator: &Separator, idx: usize) -> bool {
         let mut fork = self.clone();
diff --git a/crates/mbe/src/lib.rs b/crates/mbe/src/lib.rs
index 8470ea0aaf..0a1b7cc848 100644
--- a/crates/mbe/src/lib.rs
+++ b/crates/mbe/src/lib.rs
@@ -19,7 +19,7 @@ mod token_map;
 use std::fmt;
 
 use crate::{
-    parser::{MetaTemplate, Op},
+    parser::{parse_pattern, parse_template, MetaTemplate, Op},
     tt_iter::TtIter,
 };
 
@@ -275,8 +275,8 @@ impl Rule {
             .expect_subtree()
             .map_err(|()| ParseError::Expected("expected subtree".to_string()))?;
 
-        let lhs = MetaTemplate::parse_pattern(lhs)?;
-        let rhs = MetaTemplate::parse_template(rhs)?;
+        let lhs = MetaTemplate(parse_pattern(lhs)?);
+        let rhs = MetaTemplate(parse_template(rhs)?);
 
         Ok(crate::Rule { lhs, rhs })
     }
diff --git a/crates/mbe/src/parser.rs b/crates/mbe/src/parser.rs
index 4ce818f9b0..deed884d2d 100644
--- a/crates/mbe/src/parser.rs
+++ b/crates/mbe/src/parser.rs
@@ -3,48 +3,75 @@
 
 use smallvec::SmallVec;
 use syntax::SmolStr;
+use tt::Delimiter;
 
 use crate::{tt_iter::TtIter, ParseError};
 
-/// Consider
-///
-/// ```
-/// macro_rules! an_macro {
-///     ($x:expr + $y:expr) => ($y * $x)
-/// }
-/// ```
-///
-/// Stuff to the left of `=>` is a [`MetaTemplate`] pattern (which is matched
-/// with input).
-///
-/// Stuff to the right is a [`MetaTemplate`] template which is used to produce
-/// output.
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub(crate) struct MetaTemplate(pub(crate) Vec<Op>);
 
-impl MetaTemplate {
-    pub(crate) fn parse_pattern(pattern: &tt::Subtree) -> Result<MetaTemplate, ParseError> {
-        MetaTemplate::parse(pattern, Mode::Pattern)
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum OpDelimited<'a> {
+    Op(&'a Op),
+    Open,
+    Close,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) struct OpDelimitedIter<'a> {
+    inner: &'a Vec<Op>,
+    delimited: Option<&'a Delimiter>,
+    idx: usize,
+}
+
+impl<'a> OpDelimitedIter<'a> {
+    pub(crate) fn is_eof(&self) -> bool {
+        let len = self.inner.len() + if self.delimited.is_some() { 2 } else { 0 };
+        self.idx >= len
     }
 
-    pub(crate) fn parse_template(template: &tt::Subtree) -> Result<MetaTemplate, ParseError> {
-        MetaTemplate::parse(template, Mode::Template)
+    pub(crate) fn peek(&self) -> Option<OpDelimited<'a>> {
+        match self.delimited {
+            None => self.inner.get(self.idx).map(OpDelimited::Op),
+            Some(_) => match self.idx {
+                0 => Some(OpDelimited::Open),
+                i if i == self.inner.len() + 1 => Some(OpDelimited::Close),
+                i => self.inner.get(i - 1).map(OpDelimited::Op),
+            },
+        }
     }
 
-    pub(crate) fn iter(&self) -> impl Iterator<Item = &Op> {
-        self.0.iter()
+    pub(crate) fn reset(&self) -> Self {
+        Self { inner: self.inner, idx: 0, delimited: self.delimited }
     }
+}
 
-    fn parse(tt: &tt::Subtree, mode: Mode) -> Result<MetaTemplate, ParseError> {
-        let mut src = TtIter::new(tt);
+impl<'a> Iterator for OpDelimitedIter<'a> {
+    type Item = OpDelimited<'a>;
 
-        let mut res = Vec::new();
-        while let Some(first) = src.next() {
-            let op = next_op(first, &mut src, mode)?;
-            res.push(op)
-        }
+    fn next(&mut self) -> Option<Self::Item> {
+        let res = self.peek();
+        self.idx += 1;
+        res
+    }
 
-        Ok(MetaTemplate(res))
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        let len = self.inner.len() + if self.delimited.is_some() { 2 } else { 0 };
+        let remain = len.saturating_sub(self.idx);
+        (remain, Some(remain))
+    }
+}
+
+impl<'a> MetaTemplate {
+    pub(crate) fn iter(&self) -> impl Iterator<Item = &Op> {
+        self.0.iter()
+    }
+
+    pub(crate) fn iter_delimited(
+        &'a self,
+        delimited: Option<&'a Delimiter>,
+    ) -> OpDelimitedIter<'a> {
+        OpDelimitedIter { inner: &self.0, idx: 0, delimited }
     }
 }
 
@@ -53,7 +80,7 @@ pub(crate) enum Op {
     Var { name: SmolStr, kind: Option<SmolStr>, id: tt::TokenId },
     Repeat { tokens: MetaTemplate, kind: RepeatKind, separator: Option<Separator> },
     Leaf(tt::Leaf),
-    Subtree { tokens: MetaTemplate, delimiter: Option<tt::Delimiter> },
+    Subtree { tokens: MetaTemplate, delimiter: Option<Delimiter> },
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -98,12 +125,29 @@ impl Separator {
     }
 }
 
+pub(crate) fn parse_template(template: &tt::Subtree) -> Result<Vec<Op>, ParseError> {
+    parse_inner(template, Mode::Template).into_iter().collect()
+}
+
+pub(crate) fn parse_pattern(pattern: &tt::Subtree) -> Result<Vec<Op>, ParseError> {
+    parse_inner(pattern, Mode::Pattern).into_iter().collect()
+}
+
 #[derive(Clone, Copy)]
 enum Mode {
     Pattern,
     Template,
 }
 
+fn parse_inner(tt: &tt::Subtree, mode: Mode) -> Vec<Result<Op, ParseError>> {
+    let mut src = TtIter::new(tt);
+    std::iter::from_fn(move || {
+        let first = src.next()?;
+        Some(next_op(first, &mut src, mode))
+    })
+    .collect()
+}
+
 macro_rules! err {
     ($($tt:tt)*) => {
         ParseError::UnexpectedToken(($($tt)*).to_string())
@@ -127,8 +171,10 @@ fn next_op<'a>(first: &tt::TokenTree, src: &mut TtIter<'a>, mode: Mode) -> Resul
             match second {
                 tt::TokenTree::Subtree(subtree) => {
                     let (separator, kind) = parse_repeat(src)?;
-                    let tokens = MetaTemplate::parse(subtree, mode)?;
-                    Op::Repeat { tokens, separator, kind }
+                    let tokens = parse_inner(subtree, mode)
+                        .into_iter()
+                        .collect::<Result<Vec<Op>, ParseError>>()?;
+                    Op::Repeat { tokens: MetaTemplate(tokens), separator, kind }
                 }
                 tt::TokenTree::Leaf(leaf) => match leaf {
                     tt::Leaf::Punct(_) => {
@@ -159,8 +205,9 @@ fn next_op<'a>(first: &tt::TokenTree, src: &mut TtIter<'a>, mode: Mode) -> Resul
         }
         tt::TokenTree::Leaf(tt) => Op::Leaf(tt.clone()),
         tt::TokenTree::Subtree(subtree) => {
-            let tokens = MetaTemplate::parse(subtree, mode)?;
-            Op::Subtree { tokens, delimiter: subtree.delimiter }
+            let tokens =
+                parse_inner(subtree, mode).into_iter().collect::<Result<Vec<Op>, ParseError>>()?;
+            Op::Subtree { tokens: MetaTemplate(tokens), delimiter: subtree.delimiter }
         }
     };
     Ok(res)
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs
index 40c0cd7a37..dc13e916c8 100644
--- a/crates/syntax/src/ast/make.rs
+++ b/crates/syntax/src/ast/make.rs
@@ -299,9 +299,6 @@ pub fn expr_return(expr: Option<ast::Expr>) -> ast::Expr {
 pub fn expr_try(expr: ast::Expr) -> ast::Expr {
     expr_from_text(&format!("{}?", expr))
 }
-pub fn expr_await(expr: ast::Expr) -> ast::Expr {
-    expr_from_text(&format!("{}.await", expr))
-}
 pub fn expr_match(expr: ast::Expr, match_arm_list: ast::MatchArmList) -> ast::Expr {
     expr_from_text(&format!("match {} {}", expr, match_arm_list))
 }
diff --git a/crates/syntax/src/ast/node_ext.rs b/crates/syntax/src/ast/node_ext.rs
index 800befb04b..a07f02e445 100644
--- a/crates/syntax/src/ast/node_ext.rs
+++ b/crates/syntax/src/ast/node_ext.rs
@@ -10,7 +10,10 @@ use parser::SyntaxKind;
 use rowan::{GreenNodeData, GreenTokenData};
 
 use crate::{
-    ast::{self, support, AstNode, AstToken, HasAttrs, HasGenericParams, HasName, SyntaxNode},
+    ast::{
+        self, support, AstNode, AstToken, HasAttrs, HasGenericParams, HasModuleItem, HasName,
+        SyntaxNode,
+    },
     NodeOrToken, SmolStr, SyntaxElement, SyntaxToken, TokenText, T,
 };
 
@@ -51,6 +54,13 @@ impl ast::HasModuleItem for ast::StmtList {}
 
 impl ast::BlockExpr {
     // FIXME: remove all these methods, they belong to ast::StmtList
+    pub fn items(&self) -> impl Iterator<Item = ast::Item> {
+        self.stmt_list().into_iter().flat_map(|it| it.items())
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.statements().next().is_none() && self.tail_expr().is_none()
+    }
     pub fn statements(&self) -> impl Iterator<Item = ast::Stmt> {
         self.stmt_list().into_iter().flat_map(|it| it.statements())
     }
diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json
index 0b226a985d..3970f4ebb3 100644
--- a/editors/code/package-lock.json
+++ b/editors/code/package-lock.json
@@ -9,11 +9,11 @@
             "version": "0.4.0-dev",
             "license": "MIT OR Apache-2.0",
             "dependencies": {
-                "d3": "^7.1.0",
+                "d3": "^7.0.0",
                 "d3-graphviz": "^4.0.0",
                 "https-proxy-agent": "^5.0.0",
                 "node-fetch": "^2.6.1",
-                "vscode-languageclient": "8.0.0-next.2"
+                "vscode-languageclient": "^7.1.0-next.5"
             },
             "devDependencies": {
                 "@types/glob": "^7.1.4",
@@ -895,9 +895,9 @@
             }
         },
         "node_modules/d3": {
-            "version": "7.1.0",
-            "resolved": "https://registry.npmjs.org/d3/-/d3-7.1.0.tgz",
-            "integrity": "sha512-gfc489DnwSwd6Bh+BMhR7lzfFj+Mw+HCXn4OFQwCVQFz6H8jbEt/vWqR5oClpj4uxehJsA7ecRKEVUiKHNL1bg==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/d3/-/d3-7.0.0.tgz",
+            "integrity": "sha512-t+jEKGO2jQiSBLJYYq6RFc500tsCeXBB4x41oQaSnZD3Som95nQrlw9XJGrFTMUOQOkwSMauWy9+8Tz1qm9UZw==",
             "dependencies": {
                 "d3-array": "3",
                 "d3-axis": "3",
@@ -3753,39 +3753,39 @@
             }
         },
         "node_modules/vscode-jsonrpc": {
-            "version": "8.0.0-next.2",
-            "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.2.tgz",
-            "integrity": "sha512-gxUyTBAjmwGkiHW/UaRScre2s4i98P8M7gnc3VB4DrVQUm3vQ0idi2cN9nbkfcjATx+uEt8C22j+MLN/8UzsJA==",
+            "version": "7.0.0-next.1",
+            "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-7.0.0-next.1.tgz",
+            "integrity": "sha512-dEmliPZGbSyIcEeKRGzosCy7y7zsc8FXg1l5BBOGgMUbemlo3vUnsa2GFqpILJwJvlbvkRcF2QASNwIlKe9J7g==",
             "engines": {
                 "node": ">=8.0.0 || >=10.0.0"
             }
         },
         "node_modules/vscode-languageclient": {
-            "version": "8.0.0-next.2",
-            "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.0-next.2.tgz",
-            "integrity": "sha512-ZmWnfZCwxV/FAci9jVMBnhwtdt9qkItXKqlmAU69pBSBgcr0fRrjEHOnzWG7BcuhxE0kti6/Y9bOEIn1L+OoDQ==",
+            "version": "7.1.0-next.5",
+            "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.1.0-next.5.tgz",
+            "integrity": "sha512-TpzpAhpdCNJHaLzptFRs54xsU6xTmaiVPCse0W0rRB5jJBPjOBKilrFPMMm/sJA0y8Yxa9sOvZaNu6WPg3dYAw==",
             "dependencies": {
                 "minimatch": "^3.0.4",
                 "semver": "^7.3.4",
-                "vscode-languageserver-protocol": "3.17.0-next.8"
+                "vscode-languageserver-protocol": "3.17.0-next.6"
             },
             "engines": {
-                "vscode": "^1.58.0"
+                "vscode": "^1.53.0"
             }
         },
         "node_modules/vscode-languageserver-protocol": {
-            "version": "3.17.0-next.8",
-            "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.8.tgz",
-            "integrity": "sha512-P89vSuJ+FA5JzFmcOoZN13Ig1yd6LsiPOig0O5m5BSGuO/rplQegCd9J0wKpaTy7trf/SYHRoypnbUBdzy14sg==",
+            "version": "3.17.0-next.6",
+            "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.6.tgz",
+            "integrity": "sha512-f1kGsoOpISB5jSqQNeMDl2446enxVahyux2e5vZap6pu/TC+2UlvPT4DCR0gPph95KOQZweL9zq1SzLoPdqhuA==",
             "dependencies": {
-                "vscode-jsonrpc": "8.0.0-next.2",
-                "vscode-languageserver-types": "3.17.0-next.3"
+                "vscode-jsonrpc": "7.0.0-next.1",
+                "vscode-languageserver-types": "3.17.0-next.2"
             }
         },
         "node_modules/vscode-languageserver-types": {
-            "version": "3.17.0-next.3",
-            "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.3.tgz",
-            "integrity": "sha512-VQcXnhKYxUW6OiRMhG++SzmZYMJwusXknJGd+FfdOnS1yHAo734OHyR0e2eEHDlv0/oWc8RZPgx/VKSKyondVg=="
+            "version": "3.17.0-next.2",
+            "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.2.tgz",
+            "integrity": "sha512-L5S2kNLCgYJMVWgsZjBaorMM/6+itAfvOyl6Kv1bgFzDNaUKm9HsnUlehjpWPdV5DqnfJhJ5E03Z+/3Mw8ii+Q=="
         },
         "node_modules/vscode-test": {
             "version": "1.5.2",
@@ -4645,9 +4645,9 @@
             "dev": true
         },
         "d3": {
-            "version": "7.1.0",
-            "resolved": "https://registry.npmjs.org/d3/-/d3-7.1.0.tgz",
-            "integrity": "sha512-gfc489DnwSwd6Bh+BMhR7lzfFj+Mw+HCXn4OFQwCVQFz6H8jbEt/vWqR5oClpj4uxehJsA7ecRKEVUiKHNL1bg==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/d3/-/d3-7.0.0.tgz",
+            "integrity": "sha512-t+jEKGO2jQiSBLJYYq6RFc500tsCeXBB4x41oQaSnZD3Som95nQrlw9XJGrFTMUOQOkwSMauWy9+8Tz1qm9UZw==",
             "requires": {
                 "d3-array": "3",
                 "d3-axis": "3",
@@ -6818,33 +6818,33 @@
             }
         },
         "vscode-jsonrpc": {
-            "version": "8.0.0-next.2",
-            "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.2.tgz",
-            "integrity": "sha512-gxUyTBAjmwGkiHW/UaRScre2s4i98P8M7gnc3VB4DrVQUm3vQ0idi2cN9nbkfcjATx+uEt8C22j+MLN/8UzsJA=="
+            "version": "7.0.0-next.1",
+            "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-7.0.0-next.1.tgz",
+            "integrity": "sha512-dEmliPZGbSyIcEeKRGzosCy7y7zsc8FXg1l5BBOGgMUbemlo3vUnsa2GFqpILJwJvlbvkRcF2QASNwIlKe9J7g=="
         },
         "vscode-languageclient": {
-            "version": "8.0.0-next.2",
-            "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.0-next.2.tgz",
-            "integrity": "sha512-ZmWnfZCwxV/FAci9jVMBnhwtdt9qkItXKqlmAU69pBSBgcr0fRrjEHOnzWG7BcuhxE0kti6/Y9bOEIn1L+OoDQ==",
+            "version": "7.1.0-next.5",
+            "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.1.0-next.5.tgz",
+            "integrity": "sha512-TpzpAhpdCNJHaLzptFRs54xsU6xTmaiVPCse0W0rRB5jJBPjOBKilrFPMMm/sJA0y8Yxa9sOvZaNu6WPg3dYAw==",
             "requires": {
                 "minimatch": "^3.0.4",
                 "semver": "^7.3.4",
-                "vscode-languageserver-protocol": "3.17.0-next.8"
+                "vscode-languageserver-protocol": "3.17.0-next.6"
             }
         },
         "vscode-languageserver-protocol": {
-            "version": "3.17.0-next.8",
-            "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.8.tgz",
-            "integrity": "sha512-P89vSuJ+FA5JzFmcOoZN13Ig1yd6LsiPOig0O5m5BSGuO/rplQegCd9J0wKpaTy7trf/SYHRoypnbUBdzy14sg==",
+            "version": "3.17.0-next.6",
+            "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.6.tgz",
+            "integrity": "sha512-f1kGsoOpISB5jSqQNeMDl2446enxVahyux2e5vZap6pu/TC+2UlvPT4DCR0gPph95KOQZweL9zq1SzLoPdqhuA==",
             "requires": {
-                "vscode-jsonrpc": "8.0.0-next.2",
-                "vscode-languageserver-types": "3.17.0-next.3"
+                "vscode-jsonrpc": "7.0.0-next.1",
+                "vscode-languageserver-types": "3.17.0-next.2"
             }
         },
         "vscode-languageserver-types": {
-            "version": "3.17.0-next.3",
-            "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.3.tgz",
-            "integrity": "sha512-VQcXnhKYxUW6OiRMhG++SzmZYMJwusXknJGd+FfdOnS1yHAo734OHyR0e2eEHDlv0/oWc8RZPgx/VKSKyondVg=="
+            "version": "3.17.0-next.2",
+            "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.2.tgz",
+            "integrity": "sha512-L5S2kNLCgYJMVWgsZjBaorMM/6+itAfvOyl6Kv1bgFzDNaUKm9HsnUlehjpWPdV5DqnfJhJ5E03Z+/3Mw8ii+Q=="
         },
         "vscode-test": {
             "version": "1.5.2",
diff --git a/editors/code/package.json b/editors/code/package.json
index 5f38d1c126..a9593024d4 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -38,8 +38,8 @@
     "dependencies": {
         "https-proxy-agent": "^5.0.0",
         "node-fetch": "^2.6.1",
-        "vscode-languageclient": "8.0.0-next.2",
-        "d3": "^7.1.0",
+        "vscode-languageclient": "^7.1.0-next.5",
+        "d3": "^7.0.0",
         "d3-graphviz": "^4.0.0"
     },
     "devDependencies": {
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 726620578c..f69b7fdf56 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -66,7 +66,7 @@ async function tryActivate(context: vscode.ExtensionContext) {
     ctx.pushCleanup(configureLanguage());
 
     vscode.workspace.onDidChangeConfiguration(
-        _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }).catch(log.error),
+        _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
         null,
         ctx.subscriptions,
     );


Test patch:
diff --git a/crates/test_utils/src/minicore.rs b/crates/test_utils/src/minicore.rs
index 5e17047a40..045b4898e5 100644
--- a/crates/test_utils/src/minicore.rs
+++ b/crates/test_utils/src/minicore.rs
@@ -300,6 +300,17 @@ pub mod ops {
             #[lang = "branch"]
             fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
         }
+
+        impl<B, C> Try for ControlFlow<B, C> {
+            type Output = C;
+            type Residual = ControlFlow<B, convert::Infallible>;
+            fn from_output(output: Self::Output) -> Self {}
+            fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {}
+        }
+
+        impl<B, C> FromResidual for ControlFlow<B, C> {
+            fn from_residual(residual: ControlFlow<B, convert::Infallible>) -> Self {}
+        }
     }
     pub use self::try_::{ControlFlow, FromResidual, Try};
     // endregion:try