diff --git a/compiler/rustc_ast_pretty/src/pprust/state.rs b/compiler/rustc_ast_pretty/src/pprust/state.rs
index f14d8a693fd6e..8b7b306a54dd2 100644
--- a/compiler/rustc_ast_pretty/src/pprust/state.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/state.rs
@@ -136,11 +136,11 @@ pub fn print_crate<'a>(
     s.s.eof()
 }
 
-// This makes printed token streams look slightly nicer,
-// and also addresses some specific regressions described in #63896 and #73345.
+/// This makes printed token streams look slightly nicer,
+/// and also addresses some specific regressions described in #63896 and #73345.
 fn tt_prepend_space(tt: &TokenTree, prev: &TokenTree) -> bool {
     if let TokenTree::Token(token) = prev {
-        if matches!(token.kind, token::Dot) {
+        if matches!(token.kind, token::Dot | token::Dollar) {
             return false;
         }
         if let token::DocComment(comment_kind, ..) = token.kind {
diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs
index 829b54f1fc1cd..7b1b23ff8e6d3 100644
--- a/src/librustdoc/clean/inline.rs
+++ b/src/librustdoc/clean/inline.rs
@@ -13,10 +13,9 @@ use rustc_metadata::creader::LoadedMacro;
 use rustc_middle::ty::{self, TyCtxt};
 use rustc_span::hygiene::MacroKind;
 use rustc_span::symbol::{kw, sym, Symbol};
-use rustc_span::Span;
 
 use crate::clean::{
-    self, Attributes, AttributesExt, FakeDefId, GetDefId, NestedAttributesExt, ToSource, Type,
+    self, utils, Attributes, AttributesExt, FakeDefId, GetDefId, NestedAttributesExt, Type,
 };
 use crate::core::DocContext;
 use crate::formats::item_type::ItemType;
@@ -547,23 +546,20 @@ fn build_macro(cx: &mut DocContext<'_>, did: DefId, name: Symbol) -> clean::Item
     let imported_from = cx.tcx.crate_name(did.krate);
     match cx.enter_resolver(|r| r.cstore().load_macro_untracked(did, cx.sess())) {
         LoadedMacro::MacroDef(def, _) => {
-            let matchers: Vec<Span> = if let ast::ItemKind::MacroDef(ref def) = def.kind {
+            if let ast::ItemKind::MacroDef(ref def) = def.kind {
                 let tts: Vec<_> = def.body.inner_tokens().into_trees().collect();
-                tts.chunks(4).map(|arm| arm[0].span()).collect()
-            } else {
-                unreachable!()
-            };
+                let matchers = tts.chunks(4).map(|arm| &arm[0]);
 
-            let source = format!(
-                "macro_rules! {} {{\n{}}}",
-                name.clean(cx),
-                matchers
-                    .iter()
-                    .map(|span| { format!("    {} => {{ ... }};\n", span.to_src(cx)) })
-                    .collect::<String>()
-            );
+                let source = format!(
+                    "macro_rules! {} {{\n{}}}",
+                    name.clean(cx),
+                    utils::render_macro_arms(matchers, ";")
+                );
 
-            clean::MacroItem(clean::Macro { source, imported_from: Some(imported_from) })
+                clean::MacroItem(clean::Macro { source, imported_from: Some(imported_from) })
+            } else {
+                unreachable!()
+            }
         }
         LoadedMacro::ProcMacro(ext) => clean::ProcMacroItem(clean::ProcMacro {
             kind: ext.macro_kind(),
diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs
index 6abe1c7cd61a9..b0e75493aa4dc 100644
--- a/src/librustdoc/clean/mod.rs
+++ b/src/librustdoc/clean/mod.rs
@@ -2172,17 +2172,11 @@ impl Clean<Item> for (&hir::MacroDef<'_>, Option<Symbol>) {
         let (item, renamed) = self;
         let name = renamed.unwrap_or(item.ident.name);
         let tts = item.ast.body.inner_tokens().trees().collect::<Vec<_>>();
-        // Extract the spans of all matchers. They represent the "interface" of the macro.
-        let matchers = tts.chunks(4).map(|arm| arm[0].span()).collect::<Vec<_>>();
+        // Extract the macro's matchers. They represent the "interface" of the macro.
+        let matchers = tts.chunks(4).map(|arm| &arm[0]);
+
         let source = if item.ast.macro_rules {
-            format!(
-                "macro_rules! {} {{\n{}}}",
-                name,
-                matchers
-                    .iter()
-                    .map(|span| { format!("    {} => {{ ... }};\n", span.to_src(cx)) })
-                    .collect::<String>(),
-            )
+            format!("macro_rules! {} {{\n{}}}", name, render_macro_arms(matchers, ";"))
         } else {
             let vis = item.vis.clean(cx);
             let def_id = item.def_id.to_def_id();
@@ -2192,17 +2186,14 @@ impl Clean<Item> for (&hir::MacroDef<'_>, Option<Symbol>) {
                     "{}macro {}{} {{\n    ...\n}}",
                     vis.to_src_with_space(cx.tcx, def_id),
                     name,
-                    matchers.iter().map(|span| span.to_src(cx)).collect::<String>(),
+                    matchers.map(render_macro_matcher).collect::<String>(),
                 )
             } else {
                 format!(
                     "{}macro {} {{\n{}}}",
                     vis.to_src_with_space(cx.tcx, def_id),
                     name,
-                    matchers
-                        .iter()
-                        .map(|span| { format!("    {} => {{ ... }},\n", span.to_src(cx)) })
-                        .collect::<String>(),
+                    render_macro_arms(matchers, ","),
                 )
             }
         };
diff --git a/src/librustdoc/clean/utils.rs b/src/librustdoc/clean/utils.rs
index 3d056979780f4..9e98d0b7bbe0d 100644
--- a/src/librustdoc/clean/utils.rs
+++ b/src/librustdoc/clean/utils.rs
@@ -7,6 +7,7 @@ use crate::clean::{
 use crate::core::DocContext;
 use crate::formats::item_type::ItemType;
 
+use rustc_ast::tokenstream::TokenTree;
 use rustc_hir as hir;
 use rustc_hir::def::{DefKind, Res};
 use rustc_hir::def_id::{DefId, LOCAL_CRATE};
@@ -14,6 +15,7 @@ use rustc_middle::mir::interpret::ConstValue;
 use rustc_middle::ty::subst::{GenericArgKind, SubstsRef};
 use rustc_middle::ty::{self, DefIdTree, TyCtxt};
 use rustc_span::symbol::{kw, sym, Symbol};
+use std::fmt::Write as _;
 use std::mem;
 
 #[cfg(test)]
@@ -248,22 +250,6 @@ crate fn build_deref_target_impls(cx: &mut DocContext<'_>, items: &[Item], ret:
     }
 }
 
-crate trait ToSource {
-    fn to_src(&self, cx: &DocContext<'_>) -> String;
-}
-
-impl ToSource for rustc_span::Span {
-    fn to_src(&self, cx: &DocContext<'_>) -> String {
-        debug!("converting span {:?} to snippet", self);
-        let sn = match cx.sess().source_map().span_to_snippet(*self) {
-            Ok(x) => x,
-            Err(_) => String::new(),
-        };
-        debug!("got snippet {}", sn);
-        sn
-    }
-}
-
 crate fn name_from_pat(p: &hir::Pat<'_>) -> Symbol {
     use rustc_hir::*;
     debug!("trying to get a name from pattern: {:?}", p);
@@ -572,3 +558,22 @@ crate fn has_doc_flag(attrs: ty::Attributes<'_>, flag: Symbol) -> bool {
 ///
 /// Set by `bootstrap::Builder::doc_rust_lang_org_channel` in order to keep tests passing on beta/stable.
 crate const DOC_RUST_LANG_ORG_CHANNEL: &'static str = env!("DOC_RUST_LANG_ORG_CHANNEL");
+
+/// Render a sequence of macro arms in a format suitable for displaying to the user
+/// as part of an item declaration.
+pub(super) fn render_macro_arms<'a>(
+    matchers: impl Iterator<Item = &'a TokenTree>,
+    arm_delim: &str,
+) -> String {
+    let mut out = String::new();
+    for matcher in matchers {
+        writeln!(out, "    {} => {{ ... }}{}", render_macro_matcher(matcher), arm_delim).unwrap();
+    }
+    out
+}
+
+/// Render a macro matcher in a format suitable for displaying to the user
+/// as part of an item declaration.
+pub(super) fn render_macro_matcher(matcher: &TokenTree) -> String {
+    rustc_ast_pretty::pprust::tt_to_string(matcher)
+}
diff --git a/src/test/pretty/cast-lt.pp b/src/test/pretty/cast-lt.pp
index 4f6a924909029..e6c4d195691ca 100644
--- a/src/test/pretty/cast-lt.pp
+++ b/src/test/pretty/cast-lt.pp
@@ -8,6 +8,6 @@
 // pretty-mode:expanded
 // pp-exact:cast-lt.pp
 
-macro_rules! negative { ($ e : expr) => { $ e < 0 } }
+macro_rules! negative { ($e : expr) => { $e < 0 } }
 
 fn main() { (1 as i32) < 0; }
diff --git a/src/test/pretty/delimited-token-groups.rs b/src/test/pretty/delimited-token-groups.rs
index 125f2b0fa1687..257c032b536da 100644
--- a/src/test/pretty/delimited-token-groups.rs
+++ b/src/test/pretty/delimited-token-groups.rs
@@ -2,7 +2,7 @@
 
 #![feature(rustc_attrs)]
 
-macro_rules! mac { ($ ($ tt : tt) *) => () }
+macro_rules! mac { ($($tt : tt) *) => () }
 
 mac! {
     struct S { field1 : u8, field2 : u16, } impl Clone for S
diff --git a/src/test/pretty/macro.rs b/src/test/pretty/macro.rs
index 1e1e1dbfb3ea5..d3865d93a30d8 100644
--- a/src/test/pretty/macro.rs
+++ b/src/test/pretty/macro.rs
@@ -2,6 +2,6 @@
 
 #![feature(decl_macro)]
 
-pub(crate) macro mac { ($ arg : expr) => { $ arg + $ arg } }
+pub(crate) macro mac { ($arg : expr) => { $arg + $arg } }
 
 fn main() { }
diff --git a/src/test/pretty/macro_rules.rs b/src/test/pretty/macro_rules.rs
index da223d164f9b0..3b13f2530dd92 100644
--- a/src/test/pretty/macro_rules.rs
+++ b/src/test/pretty/macro_rules.rs
@@ -11,9 +11,9 @@ macro_rules! matcher_brackets {
 }
 
 macro_rules! all_fragments {
-    ($ b : block, $ e : expr, $ i : ident, $ it : item, $ l : lifetime, $ lit
-     : literal, $ m : meta, $ p : pat, $ pth : path, $ s : stmt, $ tt : tt, $
-     ty : ty, $ vis : vis) => { } ;
+    ($b : block, $e : expr, $i : ident, $it : item, $l : lifetime, $lit :
+     literal, $m : meta, $p : pat, $pth : path, $s : stmt, $tt : tt, $ty : ty,
+     $vis : vis) => { } ;
 }
 
 fn main() { }
diff --git a/src/test/rustdoc/decl_macro.rs b/src/test/rustdoc/decl_macro.rs
index ede3f455a2044..fe19dadbe0243 100644
--- a/src/test/rustdoc/decl_macro.rs
+++ b/src/test/rustdoc/decl_macro.rs
@@ -9,7 +9,7 @@ pub macro my_macro() {
 
 }
 
-// @has decl_macro/macro.my_macro_2.html //pre 'pub macro my_macro_2($($tok:tt)*) {'
+// @has decl_macro/macro.my_macro_2.html //pre 'pub macro my_macro_2($($tok : tt) *) {'
 // @has - //pre '...'
 // @has - //pre '}'
 pub macro my_macro_2($($tok:tt)*) {
@@ -18,8 +18,8 @@ pub macro my_macro_2($($tok:tt)*) {
 
 // @has decl_macro/macro.my_macro_multi.html //pre 'pub macro my_macro_multi {'
 // @has - //pre '(_) => { ... },'
-// @has - //pre '($foo:ident . $bar:expr) => { ... },'
-// @has - //pre '($($foo:literal),+) => { ... }'
+// @has - //pre '($foo : ident.$bar : expr) => { ... },'
+// @has - //pre '($($foo : literal), +) => { ... },'
 // @has - //pre '}'
 pub macro my_macro_multi {
     (_) => {
@@ -33,7 +33,7 @@ pub macro my_macro_multi {
     }
 }
 
-// @has decl_macro/macro.by_example_single.html //pre 'pub macro by_example_single($foo:expr) {'
+// @has decl_macro/macro.by_example_single.html //pre 'pub macro by_example_single($foo : expr) {'
 // @has - //pre '...'
 // @has - //pre '}'
 pub macro by_example_single {
@@ -42,12 +42,12 @@ pub macro by_example_single {
 
 mod a {
     mod b {
-        // @has decl_macro/a/b/macro.by_example_vis.html //pre 'pub(super) macro by_example_vis($foo:expr) {'
+        // @has decl_macro/a/b/macro.by_example_vis.html //pre 'pub(super) macro by_example_vis($foo : expr) {'
         pub(in super) macro by_example_vis {
             ($foo:expr) => {}
         }
         mod c {
-            // @has decl_macro/a/b/c/macro.by_example_vis_named.html //pre 'pub(in a) macro by_example_vis_named($foo:expr) {'
+            // @has decl_macro/a/b/c/macro.by_example_vis_named.html //pre 'pub(in a) macro by_example_vis_named($foo : expr) {'
             pub(in a) macro by_example_vis_named {
                 ($foo:expr) => {}
             }
diff --git a/src/test/rustdoc/macro_rules-matchers.rs b/src/test/rustdoc/macro_rules-matchers.rs
new file mode 100644
index 0000000000000..5f8340e7312a0
--- /dev/null
+++ b/src/test/rustdoc/macro_rules-matchers.rs
@@ -0,0 +1,45 @@
+// This is a regression test for issue #86208.
+// It is also a general test of macro_rules! display.
+
+#![crate_name = "foo"]
+
+// @has 'foo/macro.todo.html'
+// @has - '//span[@class="macro"]' 'macro_rules!'
+// @has - '//span[@class="ident"]' 'todo'
+// Note: count = 2 * ('=' + '>') + '+' = 2 * (1 + 1) + 1 = 5
+// @count - '//pre[@class="rust macro"]//span[@class="op"]' 5
+
+// @has - '{ ()'
+// @has - '//span[@class="op"]' '='
+// @has - '//span[@class="op"]' '>'
+// @has - '{ ... };'
+
+// @has - '($('
+// @has - '//span[@class="macro-nonterminal"]' '$'
+// @has - '//span[@class="macro-nonterminal"]' 'arg'
+// @has - ':'
+// @has - '//span[@class="ident"]' 'tt'
+// @has - '),'
+// @has - '//span[@class="op"]' '+'
+// @has - ')'
+pub use std::todo;
+
+mod mod1 {
+    // @has 'foo/macro.macro1.html'
+    // @has - 'macro_rules!'
+    // @has - 'macro1'
+    // @has - '{ ()'
+    // @has - '($('
+    // @has - '//span[@class="macro-nonterminal"]' '$'
+    // @has - '//span[@class="macro-nonterminal"]' 'arg'
+    // @has - ':'
+    // @has - 'expr'
+    // @has - '),'
+    // @has - '+'
+    // @has - ')'
+    #[macro_export]
+    macro_rules! macro1 {
+        () => {};
+        ($($arg:expr),+) => { stringify!($($arg),+) };
+    }
+}
diff --git a/src/test/rustdoc/macros.rs b/src/test/rustdoc/macros.rs
index ae0cf7a14789d..1cd454720e7d1 100644
--- a/src/test/rustdoc/macros.rs
+++ b/src/test/rustdoc/macros.rs
@@ -1,7 +1,7 @@
 // @has macros/macro.my_macro.html //pre 'macro_rules! my_macro {'
 // @has - //pre '() => { ... };'
-// @has - //pre '($a:tt) => { ... };'
-// @has - //pre '($e:expr) => { ... };'
+// @has - //pre '($a : tt) => { ... };'
+// @has - //pre '($e : expr) => { ... };'
 #[macro_export]
 macro_rules! my_macro {
     () => [];
@@ -12,8 +12,8 @@ macro_rules! my_macro {
 // Check that exported macro defined in a module are shown at crate root.
 // @has macros/macro.my_sub_macro.html //pre 'macro_rules! my_sub_macro {'
 // @has - //pre '() => { ... };'
-// @has - //pre '($a:tt) => { ... };'
-// @has - //pre '($e:expr) => { ... };'
+// @has - //pre '($a : tt) => { ... };'
+// @has - //pre '($e : expr) => { ... };'
 mod sub {
     #[macro_export]
     macro_rules! my_sub_macro {
diff --git a/src/test/ui/hygiene/unpretty-debug.stdout b/src/test/ui/hygiene/unpretty-debug.stdout
index e3445a4b80f7b..ae2857d505e60 100644
--- a/src/test/ui/hygiene/unpretty-debug.stdout
+++ b/src/test/ui/hygiene/unpretty-debug.stdout
@@ -8,7 +8,7 @@
 #![feature /* 0#0 */(no_core)]
 #![no_core /* 0#0 */]
 
-macro_rules! foo /* 0#0 */ { ($ x : ident) => { y + $ x } }
+macro_rules! foo /* 0#0 */ { ($x : ident) => { y + $x } }
 
 fn bar /* 0#0 */() {
     let x /* 0#0 */ = 1;
diff --git a/src/test/ui/proc-macro/meta-macro-hygiene.stdout b/src/test/ui/proc-macro/meta-macro-hygiene.stdout
index 95ef260537a8a..368326e02166e 100644
--- a/src/test/ui/proc-macro/meta-macro-hygiene.stdout
+++ b/src/test/ui/proc-macro/meta-macro-hygiene.stdout
@@ -30,7 +30,7 @@ macro_rules! produce_it
     */ {
     () =>
     {
-        meta_macro :: print_def_site! ($ crate :: dummy! ()) ;
+        meta_macro :: print_def_site! ($crate :: dummy! ()) ;
         // `print_def_site!` will respan the `$crate` identifier
         // with `Span::def_site()`. This should cause it to resolve
         // relative to `meta_macro`, *not* `make_macro` (despite
diff --git a/src/test/ui/proc-macro/nonterminal-token-hygiene.stdout b/src/test/ui/proc-macro/nonterminal-token-hygiene.stdout
index ac020cddf0874..2911707fdb03e 100644
--- a/src/test/ui/proc-macro/nonterminal-token-hygiene.stdout
+++ b/src/test/ui/proc-macro/nonterminal-token-hygiene.stdout
@@ -50,9 +50,9 @@ macro_rules! outer
     /*
     0#0
     */ {
-    ($ item : item) =>
+    ($item : item) =>
     {
-        macro inner() { print_bang! { $ item } } inner! () ;
+        macro inner() { print_bang! { $item } } inner! () ;
 
     } ;
 }