diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs
index 7ee302306f5..5960b3cc1c2 100644
--- a/crates/hir/src/lib.rs
+++ b/crates/hir/src/lib.rs
@@ -1599,6 +1599,21 @@ impl ItemInNs {
             ItemInNs::Macros(_) => None,
         }
     }
+
+    /// Returns the crate defining this item (or `None` if `self` is built-in).
+    pub fn krate(&self, db: &dyn HirDatabase) -> Option<Crate> {
+        match self {
+            ItemInNs::Types(did) | ItemInNs::Values(did) => did.module(db).map(|m| m.krate()),
+            ItemInNs::Macros(id) => id.module(db).map(|m| m.krate()),
+        }
+    }
+
+    pub fn attrs(&self, db: &dyn HirDatabase) -> Option<AttrsWithOwner> {
+        match self {
+            ItemInNs::Types(it) | ItemInNs::Values(it) => it.attrs(db),
+            ItemInNs::Macros(it) => Some(it.attrs(db)),
+        }
+    }
 }
 
 /// Invariant: `inner.as_assoc_item(db).is_some()`
diff --git a/crates/ide_completion/src/completions/flyimport.rs b/crates/ide_completion/src/completions/flyimport.rs
index 75b5daca1f8..0118d94641f 100644
--- a/crates/ide_completion/src/completions/flyimport.rs
+++ b/crates/ide_completion/src/completions/flyimport.rs
@@ -138,6 +138,10 @@ pub(crate) fn import_on_the_fly(acc: &mut Completions, ctx: &CompletionContext)
         import_assets
             .search_for_imports(&ctx.sema, ctx.config.insert_use.prefix_kind)
             .into_iter()
+            .filter(|import| {
+                !ctx.is_item_hidden(&import.item_to_import)
+                    && !ctx.is_item_hidden(&import.original_item)
+            })
             .sorted_by_key(|located_import| {
                 compute_fuzzy_completion_order_key(
                     &located_import.import_path,
@@ -1147,4 +1151,42 @@ mod bar {
             expect![[r#""#]],
         );
     }
+
+    #[test]
+    fn respects_doc_hidden() {
+        check(
+            r#"
+//- /lib.rs crate:lib deps:dep
+fn f() {
+    ().fro$0
+}
+
+//- /dep.rs crate:dep
+#[doc(hidden)]
+pub trait Private {
+    fn frob(&self) {}
+}
+
+impl<T> Private for T {}
+            "#,
+            expect![[r#""#]],
+        );
+        check(
+            r#"
+//- /lib.rs crate:lib deps:dep
+fn f() {
+    ().fro$0
+}
+
+//- /dep.rs crate:dep
+pub trait Private {
+    #[doc(hidden)]
+    fn frob(&self) {}
+}
+
+impl<T> Private for T {}
+            "#,
+            expect![[r#""#]],
+        );
+    }
 }
diff --git a/crates/ide_completion/src/context.rs b/crates/ide_completion/src/context.rs
index e4455b69416..36611cfc2b7 100644
--- a/crates/ide_completion/src/context.rs
+++ b/crates/ide_completion/src/context.rs
@@ -378,6 +378,15 @@ impl<'a> CompletionContext<'a> {
         false
     }
 
+    pub(crate) fn is_item_hidden(&self, item: &hir::ItemInNs) -> bool {
+        let attrs = item.attrs(self.db);
+        let krate = item.krate(self.db);
+        match (attrs, krate) {
+            (Some(attrs), Some(krate)) => self.is_doc_hidden(&attrs, krate),
+            _ => false,
+        }
+    }
+
     /// A version of [`SemanticsScope::process_all_names`] that filters out `#[doc(hidden)]` items.
     pub(crate) fn process_all_names(&self, f: &mut dyn FnMut(Name, ScopeDef)) {
         self.scope.process_all_names(&mut |name, def| {