Repository: rust-analyzer/rust-analyzer
Pull request #8916 (2021-05-21 22:14:27)

ItemTree pretty-printing
This adds a printer for `ItemTree` contents, and a few tests to ensure that `ItemTree` lowering works like we expect it to. It also adds a new "Debug ItemTree" command that can be used to see the `ItemTree` of the currently open file. The pretty-printed output is usually close enough to Rust syntax that we can even use Rust syntax highlighting.

This is similar to the old `ItemTree` tests we had, but produces significantly more readable output, so these should actually carry their weight.


Patch:
diff --git a/crates/hir_def/src/attr.rs b/crates/hir_def/src/attr.rs
index aadd4e44ae..89a1ea7709 100644
--- a/crates/hir_def/src/attr.rs
+++ b/crates/hir_def/src/attr.rs
@@ -2,7 +2,7 @@
 
 use std::{
     convert::{TryFrom, TryInto},
-    ops,
+    fmt, ops,
     sync::Arc,
 };
 
@@ -648,6 +648,15 @@ pub enum AttrInput {
     TokenTree(Subtree),
 }
 
+impl fmt::Display for AttrInput {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            AttrInput::Literal(lit) => write!(f, " = \"{}\"", lit.escape_debug()),
+            AttrInput::TokenTree(subtree) => subtree.fmt(f),
+        }
+    }
+}
+
 impl Attr {
     fn from_src(
         db: &dyn DefDatabase,
diff --git a/crates/hir_def/src/item_tree.rs b/crates/hir_def/src/item_tree.rs
index 7440e7d29c..528270d49d 100644
--- a/crates/hir_def/src/item_tree.rs
+++ b/crates/hir_def/src/item_tree.rs
@@ -1,6 +1,9 @@
 //! A simplified AST that only contains items.
 
 mod lower;
+mod pretty;
+#[cfg(test)]
+mod tests;
 
 use std::{
     any::type_name,
@@ -205,6 +208,10 @@ impl ItemTree {
         }
     }
 
+    pub fn pretty_print(&self) -> String {
+        pretty::print_item_tree(self)
+    }
+
     fn data(&self) -> &ItemTreeData {
         self.data.as_ref().expect("attempted to access data of empty ItemTree")
     }
@@ -776,6 +783,10 @@ impl<T> IdRange<T> {
     fn new(range: Range<Idx<T>>) -> Self {
         Self { range: range.start.into_raw().into()..range.end.into_raw().into(), _p: PhantomData }
     }
+
+    fn is_empty(&self) -> bool {
+        self.range.is_empty()
+    }
 }
 
 impl<T> Iterator for IdRange<T> {
diff --git a/crates/hir_def/src/item_tree/pretty.rs b/crates/hir_def/src/item_tree/pretty.rs
new file mode 100644
index 0000000000..5ec02d1be7
--- /dev/null
+++ b/crates/hir_def/src/item_tree/pretty.rs
@@ -0,0 +1,525 @@
+//! `ItemTree` debug printer.
+
+use std::fmt::{self, Write};
+
+use crate::{attr::RawAttrs, visibility::RawVisibility};
+
+use super::*;
+
+pub(super) fn print_item_tree(tree: &ItemTree) -> String {
+    let mut p = Printer { tree, buf: String::new(), indent_level: 0, needs_indent: true };
+
+    if let Some(attrs) = tree.attrs.get(&AttrOwner::TopLevel) {
+        p.print_attrs(attrs, true);
+    }
+    p.blank();
+
+    for item in tree.top_level_items() {
+        p.print_mod_item(*item);
+    }
+
+    let mut s = p.buf.trim_end_matches('\n').to_string();
+    s.push('\n');
+    s
+}
+
+macro_rules! w {
+    ($dst:expr, $($arg:tt)*) => {
+        drop(write!($dst, $($arg)*))
+    };
+}
+
+macro_rules! wln {
+    ($dst:expr) => {
+        drop(writeln!($dst))
+    };
+    ($dst:expr, $($arg:tt)*) => {
+        drop(writeln!($dst, $($arg)*))
+    };
+}
+
+struct Printer<'a> {
+    tree: &'a ItemTree,
+    buf: String,
+    indent_level: usize,
+    needs_indent: bool,
+}
+
+impl<'a> Printer<'a> {
+    fn indented(&mut self, f: impl FnOnce(&mut Self)) {
+        self.indent_level += 1;
+        wln!(self);
+        f(self);
+        self.indent_level -= 1;
+        self.buf = self.buf.trim_end_matches('\n').to_string();
+    }
+
+    /// Ensures that a blank line is output before the next text.
+    fn blank(&mut self) {
+        let mut iter = self.buf.chars().rev().fuse();
+        match (iter.next(), iter.next()) {
+            (Some('\n'), Some('\n')) | (Some('\n'), None) | (None, None) => {}
+            (Some('\n'), Some(_)) => {
+                self.buf.push('\n');
+            }
+            (Some(_), _) => {
+                self.buf.push('\n');
+                self.buf.push('\n');
+            }
+            (None, Some(_)) => unreachable!(),
+        }
+    }
+
+    fn print_attrs(&mut self, attrs: &RawAttrs, inner: bool) {
+        let inner = if inner { "!" } else { "" };
+        for attr in &**attrs {
+            wln!(
+                self,
+                "#{}[{}{}]  // {:?}",
+                inner,
+                attr.path,
+                attr.input.as_ref().map(|it| it.to_string()).unwrap_or_default(),
+                attr.id,
+            );
+        }
+    }
+
+    fn print_attrs_of(&mut self, of: impl Into<AttrOwner>) {
+        if let Some(attrs) = self.tree.attrs.get(&of.into()) {
+            self.print_attrs(attrs, false);
+        }
+    }
+
+    fn print_visibility(&mut self, vis: RawVisibilityId) {
+        match &self.tree[vis] {
+            RawVisibility::Module(path) => w!(self, "pub({}) ", path),
+            RawVisibility::Public => w!(self, "pub "),
+        };
+    }
+
+    fn print_fields(&mut self, fields: &Fields) {
+        match fields {
+            Fields::Record(fields) => {
+                w!(self, " {{");
+                self.indented(|this| {
+                    for field in fields.clone() {
+                        let Field { visibility, name, type_ref } = &this.tree[field];
+                        this.print_attrs_of(field);
+                        this.print_visibility(*visibility);
+                        w!(this, "{}: ", name);
+                        this.print_type_ref(type_ref);
+                        wln!(this, ",");
+                    }
+                });
+                w!(self, "}}");
+            }
+            Fields::Tuple(fields) => {
+                w!(self, "(");
+                self.indented(|this| {
+                    for field in fields.clone() {
+                        let Field { visibility, name, type_ref } = &this.tree[field];
+                        this.print_attrs_of(field);
+                        this.print_visibility(*visibility);
+                        w!(this, "{}: ", name);
+                        this.print_type_ref(type_ref);
+                        wln!(this, ",");
+                    }
+                });
+                w!(self, ")");
+            }
+            Fields::Unit => {}
+        }
+    }
+
+    fn print_mod_item(&mut self, item: ModItem) {
+        self.print_attrs_of(item);
+
+        match item {
+            ModItem::Import(it) => {
+                let Import { visibility, path, is_glob, alias, ast_id: _, index } = &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "use {}", path);
+                if *is_glob {
+                    w!(self, "::*");
+                }
+                if let Some(alias) = alias {
+                    w!(self, " as {}", alias);
+                }
+                wln!(self, ";  // {}", index);
+            }
+            ModItem::ExternCrate(it) => {
+                let ExternCrate { name, alias, visibility, ast_id: _ } = &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "extern crate {}", name);
+                if let Some(alias) = alias {
+                    w!(self, " as {}", alias);
+                }
+                wln!(self, ";");
+            }
+            ModItem::ExternBlock(it) => {
+                let ExternBlock { abi, ast_id: _, children } = &self.tree[it];
+                w!(self, "extern ");
+                if let Some(abi) = abi {
+                    w!(self, "\"{}\" ", abi);
+                }
+                w!(self, "{{");
+                self.indented(|this| {
+                    for child in &**children {
+                        this.print_mod_item(*child);
+                    }
+                });
+                wln!(self, "}}");
+            }
+            ModItem::Function(it) => {
+                let Function {
+                    name,
+                    visibility,
+                    generic_params: _, // FIXME print these somehow
+                    abi,
+                    params,
+                    ret_type,
+                    ast_id: _,
+                    flags,
+                } = &self.tree[it];
+                if flags.bits != 0 {
+                    wln!(self, "// flags = 0x{:X}", flags.bits);
+                }
+                self.print_visibility(*visibility);
+                if let Some(abi) = abi {
+                    w!(self, "extern \"{}\" ", abi);
+                }
+                w!(self, "fn {}(", name);
+                if !params.is_empty() {
+                    self.indented(|this| {
+                        for param in params.clone() {
+                            this.print_attrs_of(param);
+                            match &this.tree[param] {
+                                Param::Normal(ty) => {
+                                    w!(this, "_: ");
+                                    this.print_type_ref(ty);
+                                    wln!(this, ",");
+                                }
+                                Param::Varargs => {
+                                    wln!(this, "...");
+                                }
+                            };
+                        }
+                    });
+                }
+                w!(self, ") -> ");
+                self.print_type_ref(ret_type);
+                wln!(self, ";");
+            }
+            ModItem::Struct(it) => {
+                let Struct { visibility, name, fields, generic_params: _, ast_id: _ } =
+                    &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "struct {}", name);
+                self.print_fields(fields);
+                if matches!(fields, Fields::Record(_)) {
+                    wln!(self);
+                } else {
+                    wln!(self, ";");
+                }
+            }
+            ModItem::Union(it) => {
+                let Union { name, visibility, fields, generic_params: _, ast_id: _ } =
+                    &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "union {}", name);
+                self.print_fields(fields);
+                if matches!(fields, Fields::Record(_)) {
+                    wln!(self);
+                } else {
+                    wln!(self, ";");
+                }
+            }
+            ModItem::Enum(it) => {
+                let Enum { name, visibility, variants, generic_params: _, ast_id: _ } =
+                    &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "enum {} {{", name);
+                self.indented(|this| {
+                    for variant in variants.clone() {
+                        let Variant { name, fields } = &this.tree[variant];
+                        this.print_attrs_of(variant);
+                        w!(this, "{}", name);
+                        this.print_fields(fields);
+                        wln!(this, ",");
+                    }
+                });
+                wln!(self, "}}");
+            }
+            ModItem::Const(it) => {
+                let Const { name, visibility, type_ref, ast_id: _ } = &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "const ");
+                match name {
+                    Some(name) => w!(self, "{}", name),
+                    None => w!(self, "_"),
+                }
+                w!(self, ": ");
+                self.print_type_ref(type_ref);
+                wln!(self, " = _;");
+            }
+            ModItem::Static(it) => {
+                let Static { name, visibility, mutable, is_extern, type_ref, ast_id: _ } =
+                    &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "static ");
+                if *mutable {
+                    w!(self, "mut ");
+                }
+                w!(self, "{}: ", name);
+                self.print_type_ref(type_ref);
+                w!(self, " = _;");
+                if *is_extern {
+                    w!(self, "  // extern");
+                }
+                wln!(self);
+            }
+            ModItem::Trait(it) => {
+                let Trait {
+                    name,
+                    visibility,
+                    is_auto,
+                    is_unsafe,
+                    bounds,
+                    items,
+                    generic_params: _,
+                    ast_id: _,
+                } = &self.tree[it];
+                self.print_visibility(*visibility);
+                if *is_unsafe {
+                    w!(self, "unsafe ");
+                }
+                if *is_auto {
+                    w!(self, "auto ");
+                }
+                w!(self, "trait {}", name);
+                if !bounds.is_empty() {
+                    w!(self, ": ");
+                    self.print_type_bounds(bounds);
+                }
+                w!(self, " {{");
+                self.indented(|this| {
+                    for item in &**items {
+                        this.print_mod_item((*item).into());
+                    }
+                });
+                wln!(self, "}}");
+            }
+            ModItem::Impl(it) => {
+                let Impl {
+                    target_trait,
+                    self_ty,
+                    is_negative,
+                    items,
+                    generic_params: _,
+                    ast_id: _,
+                } = &self.tree[it];
+                w!(self, "impl ");
+                if *is_negative {
+                    w!(self, "!");
+                }
+                if let Some(tr) = target_trait {
+                    self.print_path(&tr.path);
+                    w!(self, " for ");
+                }
+                self.print_type_ref(self_ty);
+                w!(self, " {{");
+                self.indented(|this| {
+                    for item in &**items {
+                        this.print_mod_item((*item).into());
+                    }
+                });
+                wln!(self, "}}");
+            }
+            ModItem::TypeAlias(it) => {
+                let TypeAlias {
+                    name,
+                    visibility,
+                    bounds,
+                    type_ref,
+                    is_extern,
+                    generic_params: _,
+                    ast_id: _,
+                } = &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "type {}", name);
+                if !bounds.is_empty() {
+                    w!(self, ": ");
+                    self.print_type_bounds(bounds);
+                }
+                if let Some(ty) = type_ref {
+                    w!(self, " = ");
+                    self.print_type_ref(ty);
+                }
+                w!(self, ";");
+                if *is_extern {
+                    w!(self, "  // extern");
+                }
+                wln!(self);
+            }
+            ModItem::Mod(it) => {
+                let Mod { name, visibility, kind, ast_id: _ } = &self.tree[it];
+                self.print_visibility(*visibility);
+                w!(self, "mod {}", name);
+                match kind {
+                    ModKind::Inline { items } => {
+                        w!(self, " {{");
+                        self.indented(|this| {
+                            for item in &**items {
+                                this.print_mod_item((*item).into());
+                            }
+                        });
+                        wln!(self, "}}");
+                    }
+                    ModKind::Outline {} => {
+                        wln!(self, ";");
+                    }
+                }
+            }
+            ModItem::MacroCall(it) => {
+                let MacroCall { path, ast_id: _, fragment: _ } = &self.tree[it];
+                wln!(self, "{}!(...);", path);
+            }
+            ModItem::MacroRules(it) => {
+                let MacroRules { name, ast_id: _ } = &self.tree[it];
+                wln!(self, "macro_rules! {} {{ ... }}", name);
+            }
+            ModItem::MacroDef(it) => {
+                let MacroDef { name, visibility, ast_id: _ } = &self.tree[it];
+                self.print_visibility(*visibility);
+                wln!(self, "macro {} {{ ... }}", name);
+            }
+        }
+
+        self.blank();
+    }
+
+    fn print_type_ref(&mut self, type_ref: &TypeRef) {
+        // FIXME: deduplicate with `HirDisplay` impl
+        match type_ref {
+            TypeRef::Never => w!(self, "!"),
+            TypeRef::Placeholder => w!(self, "_"),
+            TypeRef::Tuple(fields) => {
+                w!(self, "(");
+                for (i, field) in fields.iter().enumerate() {
+                    if i != 0 {
+                        w!(self, ", ");
+                    }
+                    self.print_type_ref(field);
+                }
+                w!(self, ")");
+            }
+            TypeRef::Path(path) => self.print_path(path),
+            TypeRef::RawPtr(pointee, mtbl) => {
+                let mtbl = match mtbl {
+                    Mutability::Shared => "*const",
+                    Mutability::Mut => "*mut",
+                };
+                w!(self, "{} ", mtbl);
+                self.print_type_ref(pointee);
+            }
+            TypeRef::Reference(pointee, lt, mtbl) => {
+                let mtbl = match mtbl {
+                    Mutability::Shared => "",
+                    Mutability::Mut => "mut ",
+                };
+                w!(self, "&");
+                if let Some(lt) = lt {
+                    w!(self, "{} ", lt.name);
+                }
+                w!(self, "{}", mtbl);
+                self.print_type_ref(pointee);
+            }
+            TypeRef::Array(elem, len) => {
+                w!(self, "[");
+                self.print_type_ref(elem);
+                w!(self, "; {}]", len);
+            }
+            TypeRef::Slice(elem) => {
+                w!(self, "[");
+                self.print_type_ref(elem);
+                w!(self, "]");
+            }
+            TypeRef::Fn(args_and_ret, varargs) => {
+                let (ret, args) =
+                    args_and_ret.split_last().expect("TypeRef::Fn is missing return type");
+                w!(self, "fn(");
+                for (i, arg) in args.iter().enumerate() {
+                    if i != 0 {
+                        w!(self, ", ");
+                    }
+                    self.print_type_ref(arg);
+                }
+                if *varargs {
+                    if !args.is_empty() {
+                        w!(self, ", ");
+                    }
+                    w!(self, "...");
+                }
+                w!(self, ") -> ");
+                self.print_type_ref(ret);
+            }
+            TypeRef::Macro(_ast_id) => {
+                w!(self, "<macro>");
+            }
+            TypeRef::Error => drop(write!(self, "{{unknown}}")),
+            TypeRef::ImplTrait(bounds) => {
+                w!(self, "impl ");
+                self.print_type_bounds(bounds);
+            }
+            TypeRef::DynTrait(bounds) => {
+                w!(self, "dyn ");
+                self.print_type_bounds(bounds);
+            }
+        }
+    }
+
+    fn print_type_bounds(&mut self, bounds: &[TypeBound]) {
+        for (i, bound) in bounds.iter().enumerate() {
+            if i != 0 {
+                w!(self, " + ");
+            }
+
+            match bound {
+                TypeBound::Path(path) => self.print_path(path),
+                TypeBound::Lifetime(lt) => w!(self, "{}", lt.name),
+                TypeBound::Error => w!(self, "{{unknown}}"),
+            }
+        }
+    }
+
+    fn print_path(&mut self, path: &Path) {
+        if path.type_anchor().is_none()
+            && path.segments().iter().all(|seg| seg.args_and_bindings.is_none())
+        {
+            w!(self, "{}", path.mod_path());
+        } else {
+            // too complicated, just use `Debug`
+            w!(self, "{:?}", path);
+        }
+    }
+}
+
+impl<'a> Write for Printer<'a> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        for line in s.split_inclusive('\n') {
+            if self.needs_indent {
+                match self.buf.chars().last() {
+                    Some('\n') | None => {}
+                    _ => self.buf.push('\n'),
+                }
+                self.buf.push_str(&"    ".repeat(self.indent_level));
+                self.needs_indent = false;
+            }
+
+            self.buf.push_str(line);
+            self.needs_indent = line.ends_with('\n');
+        }
+
+        Ok(())
+    }
+}
diff --git a/crates/hir_def/src/path.rs b/crates/hir_def/src/path.rs
index a43441b1c2..9b8873fd25 100644
--- a/crates/hir_def/src/path.rs
+++ b/crates/hir_def/src/path.rs
@@ -46,6 +46,15 @@ pub enum ImportAlias {
     Alias(Name),
 }
 
+impl Display for ImportAlias {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            ImportAlias::Underscore => f.write_str("_"),
+            ImportAlias::Alias(name) => f.write_str(&name.to_string()),
+        }
+    }
+}
+
 impl ModPath {
     pub fn from_src(db: &dyn DefDatabase, path: ast::Path, hygiene: &Hygiene) -> Option<ModPath> {
         let ctx = LowerCtx::with_hygiene(db, hygiene);
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index f4b90db3a0..ff2a54117b 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -50,6 +50,7 @@ mod typing;
 mod markdown_remove;
 mod doc_links;
 mod view_crate_graph;
+mod view_item_tree;
 
 use std::sync::Arc;
 
@@ -288,6 +289,10 @@ impl Analysis {
         self.with_db(|db| view_hir::view_hir(&db, position))
     }
 
+    pub fn view_item_tree(&self, file_id: FileId) -> Cancelable<String> {
+        self.with_db(|db| view_item_tree::view_item_tree(&db, file_id))
+    }
+
     /// Renders the crate graph to GraphViz "dot" syntax.
     pub fn view_crate_graph(&self) -> Cancelable<Result<String, String>> {
         self.with_db(|db| view_crate_graph::view_crate_graph(&db))
diff --git a/crates/ide/src/view_item_tree.rs b/crates/ide/src/view_item_tree.rs
new file mode 100644
index 0000000000..3dc03085d6
--- /dev/null
+++ b/crates/ide/src/view_item_tree.rs
@@ -0,0 +1,16 @@
+use hir::db::DefDatabase;
+use ide_db::base_db::FileId;
+use ide_db::RootDatabase;
+
+// Feature: Debug ItemTree
+//
+// Displays the ItemTree of the currently open file, for debugging.
+//
+// |===
+// | Editor  | Action Name
+//
+// | VS Code | **Rust Analyzer: Debug ItemTree**
+// |===
+pub(crate) fn view_item_tree(db: &RootDatabase, file_id: FileId) -> String {
+    db.file_item_tree(file_id.into()).pretty_print()
+}
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 51041d7a0c..aa12fd94bd 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -117,6 +117,16 @@ pub(crate) fn handle_view_hir(
     Ok(res)
 }
 
+pub(crate) fn handle_view_item_tree(
+    snap: GlobalStateSnapshot,
+    params: lsp_ext::ViewItemTreeParams,
+) -> Result<String> {
+    let _p = profile::span("handle_view_item_tree");
+    let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
+    let res = snap.analysis.view_item_tree(file_id)?;
+    Ok(res)
+}
+
 pub(crate) fn handle_view_crate_graph(snap: GlobalStateSnapshot, (): ()) -> Result<String> {
     let _p = profile::span("handle_view_crate_graph");
     let dot = snap.analysis.view_crate_graph()??;
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 34b53a7a80..9050487930 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -70,6 +70,20 @@ impl Request for ViewCrateGraph {
     const METHOD: &'static str = "rust-analyzer/viewCrateGraph";
 }
 
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct ViewItemTreeParams {
+    pub text_document: TextDocumentIdentifier,
+}
+
+pub enum ViewItemTree {}
+
+impl Request for ViewItemTree {
+    type Params = ViewItemTreeParams;
+    type Result = String;
+    const METHOD: &'static str = "rust-analyzer/viewItemTree";
+}
+
 pub enum ExpandMacro {}
 
 impl Request for ExpandMacro {
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 4e07916113..f837b89ddc 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -514,6 +514,7 @@ impl GlobalState {
             .on::<lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
             .on::<lsp_ext::ViewHir>(handlers::handle_view_hir)
             .on::<lsp_ext::ViewCrateGraph>(handlers::handle_view_crate_graph)
+            .on::<lsp_ext::ViewItemTree>(handlers::handle_view_item_tree)
             .on::<lsp_ext::ExpandMacro>(handlers::handle_expand_macro)
             .on::<lsp_ext::ParentModule>(handlers::handle_parent_module)
             .on::<lsp_ext::Runnables>(handlers::handle_runnables)
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 3c4eacfeb5..fbe2ce1c93 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
 <!---
-lsp_ext.rs hash: 10a8988e6893e6b2
+lsp_ext.rs hash: 49f253e4a9307d4f
 
 If you need to change the above hash to make the test pass, please check if you
 need to adjust this doc as well and ping this issue:
@@ -464,7 +464,7 @@ Clients are discouraged from but are allowed to use the `health` status to decid
 **Request:**
 
 ```typescript
-interface SyntaxTeeParams {
+interface SyntaxTreeParams {
     textDocument: TextDocumentIdentifier,
     range?: Range,
 }
@@ -486,6 +486,22 @@ Primarily for debugging, but very useful for all people working on rust-analyzer
 Returns a textual representation of the HIR of the function containing the cursor.
 For debugging or when working on rust-analyzer itself.
 
+## View ItemTree
+
+**Method:** `rust-analyzer/viewItemTree`
+
+**Request:**
+
+```typescript
+interface ViewItemTreeParams {
+    textDocument: TextDocumentIdentifier,
+}
+```
+
+**Response:** `string`
+
+Returns a textual representation of the `ItemTree` of the currently open file, for debugging.
+
 ## View Crate Graph
 
 **Method:** `rust-analyzer/viewCrateGraph`
diff --git a/editors/code/package.json b/editors/code/package.json
index 1743b374c0..17d9281ff7 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -109,6 +109,11 @@
                 "title": "View Hir",
                 "category": "Rust Analyzer"
             },
+            {
+                "command": "rust-analyzer.viewItemTree",
+                "title": "Debug ItemTree",
+                "category": "Rust Analyzer"
+            },
             {
                 "command": "rust-analyzer.viewCrateGraph",
                 "title": "View Crate Graph",
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 8ab259af22..0fdb9fe058 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -429,6 +429,56 @@ export function viewHir(ctx: Ctx): Cmd {
     };
 }
 
+export function viewItemTree(ctx: Ctx): Cmd {
+    const tdcp = new class implements vscode.TextDocumentContentProvider {
+        readonly uri = vscode.Uri.parse('rust-analyzer://viewItemTree/itemtree.rs');
+        readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
+        constructor() {
+            vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
+            vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
+        }
+
+        private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
+            if (isRustDocument(event.document)) {
+                // We need to order this after language server updates, but there's no API for that.
+                // Hence, good old sleep().
+                void sleep(10).then(() => this.eventEmitter.fire(this.uri));
+            }
+        }
+        private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
+            if (editor && isRustEditor(editor)) {
+                this.eventEmitter.fire(this.uri);
+            }
+        }
+
+        provideTextDocumentContent(_uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
+            const rustEditor = ctx.activeRustEditor;
+            const client = ctx.client;
+            if (!rustEditor || !client) return '';
+
+            const params = {
+                textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(rustEditor.document),
+            };
+            return client.sendRequest(ra.viewItemTree, params, ct);
+        }
+
+        get onDidChange(): vscode.Event<vscode.Uri> {
+            return this.eventEmitter.event;
+        }
+    };
+
+    ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider('rust-analyzer', tdcp));
+
+    return async () => {
+        const document = await vscode.workspace.openTextDocument(tdcp.uri);
+        tdcp.eventEmitter.fire(tdcp.uri);
+        void await vscode.window.showTextDocument(document, {
+            viewColumn: vscode.ViewColumn.Two,
+            preserveFocus: true
+        });
+    };
+}
+
 export function viewCrateGraph(ctx: Ctx): Cmd {
     return async () => {
         const panel = vscode.window.createWebviewPanel("rust-analyzer.crate-graph", "rust-analyzer crate graph", vscode.ViewColumn.Two);
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index aa745a65ce..6d5c2ea72d 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -27,6 +27,12 @@ export const syntaxTree = new lc.RequestType<SyntaxTreeParams, string, void>("ru
 
 export const viewHir = new lc.RequestType<lc.TextDocumentPositionParams, string, void>("rust-analyzer/viewHir");
 
+export interface ViewItemTreeParams {
+    textDocument: lc.TextDocumentIdentifier;
+}
+
+export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void>("rust-analyzer/viewItemTree");
+
 export const viewCrateGraph = new lc.RequestType0<string, void>("rust-analyzer/viewCrateGraph");
 
 export interface ExpandMacroParams {
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 516322d035..92c797d47f 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -106,6 +106,7 @@ async function tryActivate(context: vscode.ExtensionContext) {
     ctx.registerCommand('parentModule', commands.parentModule);
     ctx.registerCommand('syntaxTree', commands.syntaxTree);
     ctx.registerCommand('viewHir', commands.viewHir);
+    ctx.registerCommand('viewItemTree', commands.viewItemTree);
     ctx.registerCommand('viewCrateGraph', commands.viewCrateGraph);
     ctx.registerCommand('expandMacro', commands.expandMacro);
     ctx.registerCommand('run', commands.run);


Test patch:
diff --git a/crates/hir_def/src/item_tree/tests.rs b/crates/hir_def/src/item_tree/tests.rs
new file mode 100644
index 0000000000..100ae9b97a
--- /dev/null
+++ b/crates/hir_def/src/item_tree/tests.rs
@@ -0,0 +1,244 @@
+use base_db::fixture::WithFixture;
+use expect_test::{expect, Expect};
+
+use crate::{db::DefDatabase, test_db::TestDB};
+
+fn check(ra_fixture: &str, expect: Expect) {
+    let (db, file_id) = TestDB::with_single_file(ra_fixture);
+    let item_tree = db.file_item_tree(file_id.into());
+    let pretty = item_tree.pretty_print();
+    expect.assert_eq(&pretty);
+}
+
+#[test]
+fn imports() {
+    check(
+        r#"
+//! file comment
+#![no_std]
+//! another file comment
+
+extern crate self as renamed;
+pub(super) extern crate bli;
+
+pub use crate::path::{nested, items as renamed, Trait as _};
+use globs::*;
+
+/// docs on import
+use crate::{A, B};
+        "#,
+        expect![[r##"
+            #![doc = " file comment"]  // AttrId { is_doc_comment: true, ast_index: 0 }
+            #![no_std]  // AttrId { is_doc_comment: false, ast_index: 0 }
+            #![doc = " another file comment"]  // AttrId { is_doc_comment: true, ast_index: 1 }
+
+            pub(self) extern crate self as renamed;
+
+            pub(super) extern crate bli;
+
+            pub use crate::path::nested;  // 0
+
+            pub use crate::path::items as renamed;  // 1
+
+            pub use crate::path::Trait as _;  // 2
+
+            pub(self) use globs::*;  // 0
+
+            #[doc = " docs on import"]  // AttrId { is_doc_comment: true, ast_index: 0 }
+            pub(self) use crate::A;  // 0
+
+            #[doc = " docs on import"]  // AttrId { is_doc_comment: true, ast_index: 0 }
+            pub(self) use crate::B;  // 1
+        "##]],
+    );
+}
+
+#[test]
+fn extern_blocks() {
+    check(
+        r#"
+#[on_extern_block]
+extern "C" {
+    #[on_extern_type]
+    type ExType;
+
+    #[on_extern_static]
+    static EX_STATIC: u8;
+
+    #[on_extern_fn]
+    fn ex_fn();
+}
+        "#,
+        expect![[r##"
+            #[on_extern_block]  // AttrId { is_doc_comment: false, ast_index: 0 }
+            extern "C" {
+                #[on_extern_type]  // AttrId { is_doc_comment: false, ast_index: 0 }
+                pub(self) type ExType;  // extern
+
+                #[on_extern_static]  // AttrId { is_doc_comment: false, ast_index: 0 }
+                pub(self) static EX_STATIC: u8 = _;  // extern
+
+                #[on_extern_fn]  // AttrId { is_doc_comment: false, ast_index: 0 }
+                // flags = 0x60
+                pub(self) fn ex_fn() -> ();
+            }
+        "##]],
+    );
+}
+
+#[test]
+fn adts() {
+    check(
+        r#"
+struct Unit;
+
+#[derive(Debug)]
+struct Struct {
+    /// fld docs
+    fld: (),
+}
+
+struct Tuple(#[attr] u8);
+
+union Ize {
+    a: (),
+    b: (),
+}
+
+enum E {
+    /// comment on Unit
+    Unit,
+    /// comment on Tuple
+    Tuple(u8),
+    Struct {
+        /// comment on a: u8
+        a: u8,
+    }
+}
+        "#,
+        expect![[r##"
+            pub(self) struct Unit;
+
+            #[derive(Debug)]  // AttrId { is_doc_comment: false, ast_index: 0 }
+            pub(self) struct Struct {
+                #[doc = " fld docs"]  // AttrId { is_doc_comment: true, ast_index: 0 }
+                pub(self) fld: (),
+            }
+
+            pub(self) struct Tuple(
+                #[attr]  // AttrId { is_doc_comment: false, ast_index: 0 }
+                pub(self) 0: u8,
+            );
+
+            pub(self) union Ize {
+                pub(self) a: (),
+                pub(self) b: (),
+            }
+
+            pub(self) enum E {
+                #[doc = " comment on Unit"]  // AttrId { is_doc_comment: true, ast_index: 0 }
+                Unit,
+                #[doc = " comment on Tuple"]  // AttrId { is_doc_comment: true, ast_index: 0 }
+                Tuple(
+                    pub(self) 0: u8,
+                ),
+                Struct {
+                    #[doc = " comment on a: u8"]  // AttrId { is_doc_comment: true, ast_index: 0 }
+                    pub(self) a: u8,
+                },
+            }
+        "##]],
+    );
+}
+
+#[test]
+fn misc() {
+    check(
+        r#"
+pub static mut ST: () = ();
+
+const _: Anon = ();
+
+#[attr]
+fn f(#[attr] arg: u8, _: ()) {
+    #![inner_attr_in_fn]
+}
+
+trait Tr: SuperTrait + 'lifetime {
+    type Assoc: AssocBound = Default;
+    fn method(&self);
+}
+        "#,
+        expect![[r##"
+            pub static mut ST: () = _;
+
+            pub(self) const _: Anon = _;
+
+            #[attr]  // AttrId { is_doc_comment: false, ast_index: 0 }
+            #[inner_attr_in_fn]  // AttrId { is_doc_comment: false, ast_index: 1 }
+            // flags = 0x2
+            pub(self) fn f(
+                #[attr]  // AttrId { is_doc_comment: false, ast_index: 0 }
+                _: u8,
+                _: (),
+            ) -> ();
+
+            pub(self) trait Tr: SuperTrait + 'lifetime {
+                pub(self) type Assoc: AssocBound = Default;
+
+                // flags = 0x1
+                pub(self) fn method(
+                    _: &Self,
+                ) -> ();
+            }
+        "##]],
+    );
+}
+
+#[test]
+fn modules() {
+    check(
+        r#"
+/// outer
+mod inline {
+    //! inner
+
+    use super::*;
+
+    fn fn_in_module() {}
+}
+        "#,
+        expect![[r##"
+            #[doc = " outer"]  // AttrId { is_doc_comment: true, ast_index: 0 }
+            #[doc = " inner"]  // AttrId { is_doc_comment: true, ast_index: 1 }
+            pub(self) mod inline {
+                pub(self) use super::*;  // 0
+
+                // flags = 0x2
+                pub(self) fn fn_in_module() -> ();
+            }
+        "##]],
+    );
+}
+
+#[test]
+fn macros() {
+    check(
+        r#"
+macro_rules! m {
+    () => {};
+}
+
+pub macro m2() {}
+
+m!();
+        "#,
+        expect![[r#"
+            macro_rules! m { ... }
+
+            pub macro m2 { ... }
+
+            m!(...);
+        "#]],
+    );
+}