diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index 69e5a5adbec29..eae8301945551 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -776,3 +776,7 @@ will be split as follows: "you today?", ] ``` + +## `--generate-macro-expansion`: Generate macros expansion toggles in source code + +This flag enables the generation of toggles to expand macros in the HTML source code pages. diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index f93aa8ffd0de9..5914926394138 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -305,6 +305,8 @@ pub(crate) struct RenderOptions { pub(crate) parts_out_dir: Option, /// disable minification of CSS/JS pub(crate) disable_minification: bool, + /// If `true`, HTML source pages will generate the possibility to expand macros. + pub(crate) generate_macro_expansion: bool, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -783,6 +785,7 @@ impl Options { let show_type_layout = matches.opt_present("show-type-layout"); let nocapture = matches.opt_present("nocapture"); let generate_link_to_definition = matches.opt_present("generate-link-to-definition"); + let generate_macro_expansion = matches.opt_present("generate-macro-expansion"); let extern_html_root_takes_precedence = matches.opt_present("extern-html-root-takes-precedence"); let html_no_source = matches.opt_present("html-no-source"); @@ -798,6 +801,13 @@ impl Options { .with_note("`--generate-link-to-definition` option will be ignored") .emit(); } + if generate_macro_expansion && (show_coverage || output_format != OutputFormat::Html) { + dcx.struct_warn( + "`--generate-macro-expansion` option can only be used with HTML output format", + ) + .with_note("`--generate-macro-expansion` option will be ignored") + .emit(); + } let scrape_examples_options = ScrapeExamplesOptions::new(matches, dcx); let with_examples = matches.opt_strs("with-examples"); @@ -878,6 +888,7 @@ impl Options { unstable_features, emit, generate_link_to_definition, + generate_macro_expansion, call_locations, no_emit_shared: false, html_no_source, diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs index 9d1c9ff00b143..694c12b7662ea 100644 --- a/src/librustdoc/core.rs +++ b/src/librustdoc/core.rs @@ -31,6 +31,7 @@ use crate::clean::inline::build_trait; use crate::clean::{self, ItemId}; use crate::config::{Options as RustdocOptions, OutputFormat, RenderOptions}; use crate::formats::cache::Cache; +use crate::html::macro_expansion::{ExpandedCode, source_macro_expansion}; use crate::passes; use crate::passes::Condition::*; use crate::passes::collect_intra_doc_links::LinkCollector; @@ -335,11 +336,19 @@ pub(crate) fn run_global_ctxt( show_coverage: bool, render_options: RenderOptions, output_format: OutputFormat, -) -> (clean::Crate, RenderOptions, Cache) { +) -> (clean::Crate, RenderOptions, Cache, FxHashMap>) { // Certain queries assume that some checks were run elsewhere // (see https://github.com/rust-lang/rust/pull/73566#issuecomment-656954425), // so type-check everything other than function bodies in this crate before running lints. + let expanded_macros = { + // We need for these variables to be removed to ensure that the `Crate` won't be "stolen" + // anymore. + let (_resolver, krate) = &*tcx.resolver_for_lowering().borrow(); + + source_macro_expansion(&krate, &render_options, output_format, tcx.sess.source_map()) + }; + // NOTE: this does not call `tcx.analysis()` so that we won't // typeck function bodies or run the default rustc lints. // (see `override_queries` in the `config`) @@ -453,7 +462,7 @@ pub(crate) fn run_global_ctxt( tcx.dcx().abort_if_errors(); - (krate, ctxt.render_options, ctxt.cache) + (krate, ctxt.render_options, ctxt.cache, expanded_macros) } /// Due to , diff --git a/src/librustdoc/formats/renderer.rs b/src/librustdoc/formats/renderer.rs index 5e4e6f27a1541..1703ffad214cc 100644 --- a/src/librustdoc/formats/renderer.rs +++ b/src/librustdoc/formats/renderer.rs @@ -31,15 +31,6 @@ pub(crate) trait FormatRenderer<'tcx>: Sized { /// reset the information between each call to `item` by using `restore_module_data`. type ModuleData; - /// Sets up any state required for the renderer. When this is called the cache has already been - /// populated. - fn init( - krate: clean::Crate, - options: RenderOptions, - cache: Cache, - tcx: TyCtxt<'tcx>, - ) -> Result<(Self, clean::Crate), Error>; - /// This method is called right before call [`Self::item`]. This method returns a type /// containing information that needs to be reset after the [`Self::item`] method has been /// called with the [`Self::restore_module_data`] method. @@ -107,18 +98,23 @@ fn run_format_inner<'tcx, T: FormatRenderer<'tcx>>( } /// Main method for rendering a crate. -pub(crate) fn run_format<'tcx, T: FormatRenderer<'tcx>>( +pub(crate) fn run_format< + 'tcx, + T: FormatRenderer<'tcx>, + F: FnOnce(clean::Crate, RenderOptions, Cache, TyCtxt<'tcx>) -> Result<(T, clean::Crate), Error>, +>( krate: clean::Crate, options: RenderOptions, cache: Cache, tcx: TyCtxt<'tcx>, + init: F, ) -> Result<(), Error> { let prof = &tcx.sess.prof; let emit_crate = options.should_emit_crate(); let (mut format_renderer, krate) = prof .verbose_generic_activity_with_arg("create_renderer", T::descr()) - .run(|| T::init(krate, options, cache, tcx))?; + .run(|| init(krate, options, cache, tcx))?; if !emit_crate { return Ok(()); diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index 2db1ea8450ce1..791cef64d5a21 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -5,6 +5,7 @@ //! //! Use the `render_with_highlighting` to highlight some rust code. +use std::borrow::Cow; use std::collections::VecDeque; use std::fmt::{Display, Write}; @@ -17,6 +18,7 @@ use rustc_span::{BytePos, DUMMY_SP, Span}; use super::format::{self, write_str}; use crate::clean::PrimitiveType; use crate::html::escape::EscapeBodyText; +use crate::html::macro_expansion::ExpandedCode; use crate::html::render::{Context, LinkFromSrc}; /// This type is needed in case we want to render links on items to allow to go to their definition. @@ -156,11 +158,22 @@ struct TokenHandler<'a, 'tcx, F: Write> { current_class: Option, /// We need to keep the `Class` for each element because it could contain a `Span` which is /// used to generate links. - pending_elems: Vec<(&'a str, Option)>, + pending_elems: Vec<(Cow<'a, str>, Option)>, href_context: Option>, write_line_number: fn(&mut F, u32, &'static str), } +impl std::fmt::Debug for TokenHandler<'_, '_, F> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TokenHandler") + .field("closing_tags", &self.closing_tags) + .field("pending_exit_span", &self.pending_exit_span) + .field("current_class", &self.current_class) + .field("pending_elems", &self.pending_elems) + .finish() + } +} + impl TokenHandler<'_, '_, F> { fn handle_exit_span(&mut self) { // We can't get the last `closing_tags` element using `pop()` because `closing_tags` is @@ -213,6 +226,12 @@ impl TokenHandler<'_, '_, F> { } else { None }; + let mut last_pending = None; + // To prevent opening a macro expansion span being closed right away because + // the currently open item is replaced by a new class. + if let Some((_, Some(Class::Expansion))) = self.pending_elems.last() { + last_pending = self.pending_elems.pop(); + } for (text, class) in self.pending_elems.iter() { string( self.out, @@ -226,6 +245,16 @@ impl TokenHandler<'_, '_, F> { if let Some(close_tag) = close_tag { exit_span(self.out, close_tag); } + if let Some((text, class)) = last_pending { + string( + self.out, + EscapeBodyText(&text), + class, + &self.href_context, + close_tag.is_none(), + self.write_line_number, + ); + } } self.pending_elems.clear(); true @@ -264,6 +293,104 @@ fn empty_line_number(out: &mut impl Write, _: u32, extra: &'static str) { out.write_str(extra).unwrap(); } +fn get_next_expansion<'a>( + expanded_codes: Option<&'a Vec>, + line: u32, + span: Span, +) -> Option<&'a ExpandedCode> { + if let Some(expanded_codes) = expanded_codes { + expanded_codes.iter().find(|code| code.start_line == line && code.span.lo() > span.lo()) + } else { + None + } +} + +fn get_expansion<'a, W: Write>( + token_handler: &mut TokenHandler<'_, '_, W>, + expanded_codes: Option<&'a Vec>, + line: u32, + span: Span, +) -> Option<&'a ExpandedCode> { + if let Some(expanded_code) = get_next_expansion(expanded_codes, line, span) { + let (closing, reopening) = if let Some(current_class) = token_handler.current_class + && let class = current_class.as_html() + && !class.is_empty() + { + ("", format!("")) + } else { + ("", String::new()) + }; + let id = format!("expand-{line}"); + token_handler.pending_elems.push(( + Cow::Owned(format!( + "{closing}\ +\ + {reopening}", + )), + Some(Class::Expansion), + )); + Some(expanded_code) + } else { + None + } +} + +fn start_expansion(out: &mut Vec<(Cow<'_, str>, Option)>, expanded_code: &ExpandedCode) { + out.push(( + Cow::Owned(format!( + "{}", + expanded_code.code, + )), + Some(Class::Expansion), + )); +} + +fn end_expansion<'a, W: Write>( + token_handler: &mut TokenHandler<'_, '_, W>, + expanded_codes: Option<&'a Vec>, + expansion_start_tags: &[(&'static str, Class)], + line: u32, + span: Span, +) -> Option<&'a ExpandedCode> { + if let Some(expanded_code) = get_next_expansion(expanded_codes, line, span) { + // We close the current "original" content. + token_handler.pending_elems.push((Cow::Borrowed(""), Some(Class::Expansion))); + return Some(expanded_code); + } + if expansion_start_tags.is_empty() && token_handler.closing_tags.is_empty() { + // No need tag opened so we can just close expansion. + token_handler.pending_elems.push((Cow::Borrowed(""), Some(Class::Expansion))); + return None; + } + + // If tags were opened inside the expansion, we need to close them and re-open them outside + // of the expansion span. + let mut out = String::new(); + let mut end = String::new(); + + let mut closing_tags = token_handler.closing_tags.iter().peekable(); + let mut start_closing_tags = expansion_start_tags.iter().peekable(); + + while let (Some(tag), Some(start_tag)) = (closing_tags.peek(), start_closing_tags.peek()) + && tag == start_tag + { + closing_tags.next(); + start_closing_tags.next(); + } + for (tag, class) in start_closing_tags.chain(closing_tags) { + out.push_str(tag); + end.push_str(&format!("", class.as_html())); + } + token_handler + .pending_elems + .push((Cow::Owned(format!("{out}{end}")), Some(Class::Expansion))); + None +} + #[derive(Clone, Copy)] pub(super) struct LineInfo { pub(super) start_line: u32, @@ -310,7 +437,7 @@ pub(super) fn write_code( closing_tags: Vec::new(), pending_exit_span: None, current_class: None, - pending_elems: Vec::new(), + pending_elems: Vec::with_capacity(20), href_context, write_line_number: match line_info { Some(line_info) => { @@ -331,12 +458,23 @@ pub(super) fn write_code( (0, u32::MAX) }; + let (expanded_codes, file_span) = match token_handler.href_context.as_ref().and_then(|c| { + let expanded_codes = c.context.shared.expanded_codes.get(&c.file_span.lo())?; + Some((expanded_codes, c.file_span)) + }) { + Some((expanded_codes, file_span)) => (Some(expanded_codes), file_span), + None => (None, DUMMY_SP), + }; + let mut current_expansion = get_expansion(&mut token_handler, expanded_codes, line, file_span); + token_handler.write_pending_elems(None); + let mut expansion_start_tags = Vec::new(); + Classifier::new( &src, token_handler.href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP), decoration_info, ) - .highlight(&mut |highlight| { + .highlight(&mut |span, highlight| { match highlight { Highlight::Token { text, class } => { // If we received a `ExitSpan` event and then have a non-compatible `Class`, we @@ -362,10 +500,42 @@ pub(super) fn write_code( if text == "\n" { line += 1; if line < max_lines { - token_handler.pending_elems.push((text, Some(Class::Backline(line)))); + token_handler + .pending_elems + .push((Cow::Borrowed(text), Some(Class::Backline(line)))); + } + if current_expansion.is_none() { + current_expansion = + get_expansion(&mut token_handler, expanded_codes, line, span); + expansion_start_tags = token_handler.closing_tags.clone(); + } + if let Some(ref current_expansion) = current_expansion + && current_expansion.span.lo() == span.hi() + { + start_expansion(&mut token_handler.pending_elems, current_expansion); } } else { - token_handler.pending_elems.push((text, class)); + token_handler.pending_elems.push((Cow::Borrowed(text), class)); + + let mut need_end = false; + if let Some(ref current_expansion) = current_expansion { + if current_expansion.span.lo() == span.hi() { + start_expansion(&mut token_handler.pending_elems, current_expansion); + } else if current_expansion.end_line == line + && span.hi() >= current_expansion.span.hi() + { + need_end = true; + } + } + if need_end { + current_expansion = end_expansion( + &mut token_handler, + expanded_codes, + &expansion_start_tags, + line, + span, + ); + } } } Highlight::EnterSpan { class } => { @@ -433,6 +603,8 @@ enum Class { QuestionMark, Decoration(&'static str), Backline(u32), + /// Macro expansion. + Expansion, } impl Class { @@ -482,6 +654,7 @@ impl Class { Class::QuestionMark => "question-mark", Class::Decoration(kind) => kind, Class::Backline(_) => "", + Class::Expansion => "", } } @@ -506,7 +679,8 @@ impl Class { | Self::Lifetime | Self::QuestionMark | Self::Decoration(_) - | Self::Backline(_) => None, + | Self::Backline(_) + | Self::Expansion => None, } } } @@ -621,6 +795,13 @@ impl Decorations { } } +/// Convenient wrapper to create a [`Span`] from a position in the file. +fn new_span(lo: u32, text: &str, file_span: Span) -> Span { + let hi = lo + text.len() as u32; + let file_lo = file_span.lo(); + file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi)) +} + /// Processes program tokens, classifying strings of text by highlighting /// category (`Class`). struct Classifier<'src> { @@ -653,13 +834,6 @@ impl<'src> Classifier<'src> { } } - /// Convenient wrapper to create a [`Span`] from a position in the file. - fn new_span(&self, lo: u32, text: &str) -> Span { - let hi = lo + text.len() as u32; - let file_lo = self.file_span.lo(); - self.file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi)) - } - /// Concatenate colons and idents as one when possible. fn get_full_ident_path(&mut self) -> Vec<(TokenKind, usize, usize)> { let start = self.byte_pos as usize; @@ -728,18 +902,18 @@ impl<'src> Classifier<'src> { /// The general structure for this method is to iterate over each token, /// possibly giving it an HTML span with a class specifying what flavor of /// token is used. - fn highlight(mut self, sink: &mut dyn FnMut(Highlight<'src>)) { + fn highlight(mut self, sink: &mut dyn FnMut(Span, Highlight<'src>)) { loop { if let Some(decs) = self.decorations.as_mut() { let byte_pos = self.byte_pos; let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count(); for (_, kind) in decs.starts.drain(0..n_starts) { - sink(Highlight::EnterSpan { class: Class::Decoration(kind) }); + sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Decoration(kind) }); } let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count(); for _ in decs.ends.drain(0..n_ends) { - sink(Highlight::ExitSpan); + sink(DUMMY_SP, Highlight::ExitSpan); } } @@ -777,14 +951,22 @@ impl<'src> Classifier<'src> { &mut self, token: TokenKind, text: &'src str, - sink: &mut dyn FnMut(Highlight<'src>), + sink: &mut dyn FnMut(Span, Highlight<'src>), before: u32, ) { let lookahead = self.peek(); - let no_highlight = |sink: &mut dyn FnMut(_)| sink(Highlight::Token { text, class: None }); - let whitespace = |sink: &mut dyn FnMut(_)| { + let file_span = self.file_span; + let no_highlight = |sink: &mut dyn FnMut(_, _)| { + sink(new_span(before, text, file_span), Highlight::Token { text, class: None }) + }; + let whitespace = |sink: &mut dyn FnMut(_, _)| { + let mut start = 0u32; for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) { - sink(Highlight::Token { text: part, class: None }); + sink( + new_span(before + start, part, file_span), + Highlight::Token { text: part, class: None }, + ); + start += part.len() as u32; } }; let class = match token { @@ -800,8 +982,8 @@ impl<'src> Classifier<'src> { // leading identifier. TokenKind::Bang if self.in_macro => { self.in_macro = false; - sink(Highlight::Token { text, class: None }); - sink(Highlight::ExitSpan); + sink(new_span(before, text, file_span), Highlight::Token { text, class: None }); + sink(DUMMY_SP, Highlight::ExitSpan); return; } @@ -812,12 +994,18 @@ impl<'src> Classifier<'src> { Some((TokenKind::Whitespace, _)) => return whitespace(sink), Some((TokenKind::Ident, "mut")) => { self.next(); - sink(Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) }); + sink( + DUMMY_SP, + Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) }, + ); return; } Some((TokenKind::Ident, "const")) => { self.next(); - sink(Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) }); + sink( + DUMMY_SP, + Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) }, + ); return; } _ => Class::RefKeyWord, @@ -825,18 +1013,21 @@ impl<'src> Classifier<'src> { TokenKind::And => match self.tokens.peek() { Some((TokenKind::And, _)) => { self.next(); - sink(Highlight::Token { text: "&&", class: None }); + sink(DUMMY_SP, Highlight::Token { text: "&&", class: None }); return; } Some((TokenKind::Eq, _)) => { self.next(); - sink(Highlight::Token { text: "&=", class: None }); + sink(DUMMY_SP, Highlight::Token { text: "&=", class: None }); return; } Some((TokenKind::Whitespace, _)) => return whitespace(sink), Some((TokenKind::Ident, "mut")) => { self.next(); - sink(Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) }); + sink( + DUMMY_SP, + Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) }, + ); return; } _ => Class::RefKeyWord, @@ -846,19 +1037,19 @@ impl<'src> Classifier<'src> { TokenKind::Eq => match lookahead { Some(TokenKind::Eq) => { self.next(); - sink(Highlight::Token { text: "==", class: None }); + sink(DUMMY_SP, Highlight::Token { text: "==", class: None }); return; } Some(TokenKind::Gt) => { self.next(); - sink(Highlight::Token { text: "=>", class: None }); + sink(DUMMY_SP, Highlight::Token { text: "=>", class: None }); return; } _ => return no_highlight(sink), }, TokenKind::Minus if lookahead == Some(TokenKind::Gt) => { self.next(); - sink(Highlight::Token { text: "->", class: None }); + sink(DUMMY_SP, Highlight::Token { text: "->", class: None }); return; } @@ -909,16 +1100,22 @@ impl<'src> Classifier<'src> { self.next(); if let Some(TokenKind::OpenBracket) = self.peek() { self.in_attribute = true; - sink(Highlight::EnterSpan { class: Class::Attribute }); + sink( + new_span(before, text, file_span), + Highlight::EnterSpan { class: Class::Attribute }, + ); } - sink(Highlight::Token { text: "#", class: None }); - sink(Highlight::Token { text: "!", class: None }); + sink(DUMMY_SP, Highlight::Token { text: "#", class: None }); + sink(DUMMY_SP, Highlight::Token { text: "!", class: None }); return; } // Case 2: #[outer_attribute] Some(TokenKind::OpenBracket) => { self.in_attribute = true; - sink(Highlight::EnterSpan { class: Class::Attribute }); + sink( + new_span(before, text, file_span), + Highlight::EnterSpan { class: Class::Attribute }, + ); } _ => (), } @@ -927,8 +1124,11 @@ impl<'src> Classifier<'src> { TokenKind::CloseBracket => { if self.in_attribute { self.in_attribute = false; - sink(Highlight::Token { text: "]", class: None }); - sink(Highlight::ExitSpan); + sink( + new_span(before, text, file_span), + Highlight::Token { text: "]", class: None }, + ); + sink(DUMMY_SP, Highlight::ExitSpan); return; } return no_highlight(sink); @@ -949,15 +1149,16 @@ impl<'src> Classifier<'src> { TokenKind::GuardedStrPrefix => return no_highlight(sink), TokenKind::Ident | TokenKind::RawIdent if lookahead == Some(TokenKind::Bang) => { self.in_macro = true; - sink(Highlight::EnterSpan { class: Class::Macro(self.new_span(before, text)) }); - sink(Highlight::Token { text, class: None }); + let span = new_span(before, text, file_span); + sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) }); + sink(span, Highlight::Token { text, class: None }); return; } TokenKind::Ident => match get_real_ident_class(text, false) { None => match text { - "Option" | "Result" => Class::PreludeTy(self.new_span(before, text)), + "Option" | "Result" => Class::PreludeTy(new_span(before, text, file_span)), "Some" | "None" | "Ok" | "Err" => { - Class::PreludeVal(self.new_span(before, text)) + Class::PreludeVal(new_span(before, text, file_span)) } // "union" is a weak keyword and is only considered as a keyword when declaring // a union type. @@ -966,13 +1167,13 @@ impl<'src> Classifier<'src> { self.in_macro_nonterminal = false; Class::MacroNonTerminal } - "self" | "Self" => Class::Self_(self.new_span(before, text)), - _ => Class::Ident(self.new_span(before, text)), + "self" | "Self" => Class::Self_(new_span(before, text, file_span)), + _ => Class::Ident(new_span(before, text, file_span)), }, Some(c) => c, }, TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => { - Class::Ident(self.new_span(before, text)) + Class::Ident(new_span(before, text, file_span)) } TokenKind::Lifetime { .. } | TokenKind::RawLifetime @@ -981,8 +1182,13 @@ impl<'src> Classifier<'src> { }; // Anything that didn't return above is the simple case where we the // class just spans a single token, so we can use the `string` method. + let mut start = 0u32; for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) { - sink(Highlight::Token { text: part, class: Some(class) }); + sink( + new_span(before + start, part, file_span), + Highlight::Token { text: part, class: Some(class) }, + ); + start += part.len() as u32; } } @@ -1035,9 +1241,9 @@ fn exit_span(out: &mut impl Write, closing_tag: &str) { /// Note that if `context` is not `None` and that the given `klass` contains a `Span`, the function /// will then try to find this `span` in the `span_correspondence_map`. If found, it'll then /// generate a link for this element (which corresponds to where its definition is located). -fn string( +fn string( out: &mut W, - text: T, + text: EscapeBodyText<'_>, klass: Option, href_context: &Option>, open_tag: bool, @@ -1045,6 +1251,9 @@ fn string( ) { if let Some(Class::Backline(line)) = klass { write_line_number_callback(out, line, "\n"); + } else if let Some(Class::Expansion) = klass { + // Nothing to escape here so we get the text to write it directly. + out.write_str(text.0).unwrap(); } else if let Some(closing_tag) = string_without_closing_tag(out, text, klass, href_context, open_tag) { diff --git a/src/librustdoc/html/macro_expansion.rs b/src/librustdoc/html/macro_expansion.rs new file mode 100644 index 0000000000000..7b6758868b77a --- /dev/null +++ b/src/librustdoc/html/macro_expansion.rs @@ -0,0 +1,140 @@ +use rustc_ast::visit::{Visitor, walk_crate, walk_expr, walk_item}; +use rustc_ast::{Crate, Expr, Item}; +use rustc_data_structures::fx::FxHashMap; +use rustc_span::source_map::SourceMap; +use rustc_span::{BytePos, Span}; + +use crate::config::{OutputFormat, RenderOptions}; + +/// It returns the expanded macros correspondence map. +pub(crate) fn source_macro_expansion( + krate: &Crate, + render_options: &RenderOptions, + output_format: OutputFormat, + source_map: &SourceMap, +) -> FxHashMap> { + if output_format == OutputFormat::Html + && !render_options.html_no_source + && render_options.generate_macro_expansion + { + let mut expanded_visitor = ExpandedCodeVisitor { expanded_codes: Vec::new(), source_map }; + walk_crate(&mut expanded_visitor, krate); + expanded_visitor.compute_expanded() + } else { + Default::default() + } +} + +/// Contains information about macro expansion in the source code pages. +#[derive(Debug)] +pub(crate) struct ExpandedCode { + /// The line where the macro expansion starts. + pub(crate) start_line: u32, + /// The line where the macro expansion ends. + pub(crate) end_line: u32, + /// The source code of the expanded macro. + pub(crate) code: String, + /// The span of macro callsite. + pub(crate) span: Span, +} + +/// Contains temporary information of macro expanded code. +/// +/// As we go through the HIR visitor, if any span overlaps with another, they will +/// both be merged. +struct ExpandedCodeInfo { + /// Callsite of the macro. + span: Span, + /// Expanded macro source code. + code: String, + /// Expanded span + expanded_span: Span, +} + +/// HIR visitor which retrieves expanded macro. +/// +/// Once done, the `expanded_codes` will be transformed into a vec of [`ExpandedCode`] +/// which contains more information needed when running the source code highlighter. +pub(crate) struct ExpandedCodeVisitor<'ast> { + expanded_codes: Vec, + source_map: &'ast SourceMap, +} + +impl<'ast> ExpandedCodeVisitor<'ast> { + fn handle_new_span String>(&mut self, new_span: Span, f: F) { + if new_span.is_dummy() || !new_span.from_expansion() { + return; + } + let callsite_span = new_span.source_callsite(); + if let Some(index) = + self.expanded_codes.iter().position(|info| info.span.overlaps(callsite_span)) + { + let info = &mut self.expanded_codes[index]; + if new_span.contains(info.expanded_span) { + // We replace the item. + info.span = callsite_span; + info.expanded_span = new_span; + info.code = f(); + } else { + // We push the new item after the existing one. + let expanded_code = &mut self.expanded_codes[index]; + expanded_code.code.push('\n'); + expanded_code.code.push_str(&f()); + let lo = BytePos(expanded_code.expanded_span.lo().0.min(new_span.lo().0)); + let hi = BytePos(expanded_code.expanded_span.hi().0.min(new_span.hi().0)); + expanded_code.expanded_span = expanded_code.expanded_span.with_lo(lo).with_hi(hi); + } + } else { + // We add a new item. + self.expanded_codes.push(ExpandedCodeInfo { + span: callsite_span, + code: f(), + expanded_span: new_span, + }); + } + } + + fn compute_expanded(mut self) -> FxHashMap> { + self.expanded_codes.sort_unstable_by(|item1, item2| item1.span.cmp(&item2.span)); + let mut expanded: FxHashMap> = FxHashMap::default(); + for ExpandedCodeInfo { span, code, .. } in self.expanded_codes { + if let Ok(lines) = self.source_map.span_to_lines(span) + && !lines.lines.is_empty() + { + let mut out = String::new(); + super::highlight::write_code(&mut out, &code, None, None, None); + let first = lines.lines.first().unwrap(); + let end = lines.lines.last().unwrap(); + expanded.entry(lines.file.start_pos).or_default().push(ExpandedCode { + start_line: first.line_index as u32 + 1, + end_line: end.line_index as u32 + 1, + code: out, + span, + }); + } + } + expanded + } +} + +// We need to use the AST pretty printing because: +// +// 1. HIR pretty printing doesn't display accurately the code (like `impl Trait`). +// 2. `SourceMap::snippet_opt` might fail if the source is not available. +impl<'ast> Visitor<'ast> for ExpandedCodeVisitor<'ast> { + fn visit_expr(&mut self, expr: &'ast Expr) { + if expr.span.from_expansion() { + self.handle_new_span(expr.span, || rustc_ast_pretty::pprust::expr_to_string(expr)); + } else { + walk_expr(self, expr); + } + } + + fn visit_item(&mut self, item: &'ast Item) { + if item.span.from_expansion() { + self.handle_new_span(item.span, || rustc_ast_pretty::pprust::item_to_string(item)); + } else { + walk_item(self, item); + } + } +} diff --git a/src/librustdoc/html/mod.rs b/src/librustdoc/html/mod.rs index 481ed16c05f7e..170449fc033fe 100644 --- a/src/librustdoc/html/mod.rs +++ b/src/librustdoc/html/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod highlight; pub(crate) mod layout; mod length_limit; // used by the error-index generator, so it needs to be public +pub(crate) mod macro_expansion; pub mod markdown; pub(crate) mod render; pub(crate) mod sources; diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 1f7201b8ca8b2..b4a1f2d5fee8f 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -11,7 +11,7 @@ use rustc_hir::def_id::{DefIdMap, LOCAL_CRATE}; use rustc_middle::ty::TyCtxt; use rustc_session::Session; use rustc_span::edition::Edition; -use rustc_span::{FileName, Symbol, sym}; +use rustc_span::{BytePos, FileName, Symbol, sym}; use tracing::info; use super::print_item::{full_path, item_path, print_item}; @@ -28,6 +28,7 @@ use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; use crate::html::escape::Escape; use crate::html::format::join_with_double_colon; +use crate::html::macro_expansion::ExpandedCode; use crate::html::markdown::{self, ErrorCodes, IdMap, plain_text_summary}; use crate::html::render::write_shared::write_shared; use crate::html::url_parts_builder::UrlPartsBuilder; @@ -139,6 +140,7 @@ pub(crate) struct SharedContext<'tcx> { /// Correspondence map used to link types used in the source code pages to allow to click on /// links to jump to the type's definition. pub(crate) span_correspondence_map: FxHashMap, + pub(crate) expanded_codes: FxHashMap>, /// The [`Cache`] used during rendering. pub(crate) cache: Cache, pub(crate) call_locations: AllCallLocations, @@ -451,20 +453,13 @@ impl<'tcx> Context<'tcx> { } } -/// Generates the documentation for `crate` into the directory `dst` -impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { - fn descr() -> &'static str { - "html" - } - - const RUN_ON_MODULE: bool = true; - type ModuleData = ContextInfo; - - fn init( +impl<'tcx> Context<'tcx> { + pub(crate) fn init( krate: clean::Crate, options: RenderOptions, cache: Cache, tcx: TyCtxt<'tcx>, + expanded_codes: FxHashMap>, ) -> Result<(Self, clean::Crate), Error> { // need to save a copy of the options for rendering the index page let md_opts = options.clone(); @@ -572,6 +567,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { cache, call_locations, should_merge: options.should_merge, + expanded_codes, }; let dst = output; @@ -597,6 +593,16 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { Ok((cx, krate)) } +} + +/// Generates the documentation for `crate` into the directory `dst` +impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { + fn descr() -> &'static str { + "html" + } + + const RUN_ON_MODULE: bool = true; + type ModuleData = ContextInfo; fn save_module_data(&mut self) -> Self::ModuleData { self.deref_id_map.borrow_mut().clear(); diff --git a/src/librustdoc/html/render/span_map.rs b/src/librustdoc/html/render/span_map.rs index 846d3ad310ce4..8bc2e0bd95748 100644 --- a/src/librustdoc/html/render/span_map.rs +++ b/src/librustdoc/html/render/span_map.rs @@ -35,7 +35,7 @@ pub(crate) enum LinkFromSrc { /// 1. Generate a `span` correspondence map which links an item `span` to its definition `span`. /// 2. Collect the source code files. /// -/// It returns the `krate`, the source code files and the `span` correspondence map. +/// It returns the source code files and the `span` correspondence map. /// /// Note about the `span` correspondence map: the keys are actually `(lo, hi)` of `span`s. We don't /// need the `span` context later on, only their position, so instead of keeping a whole `Span`, we diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 7be83b65fbfaf..5e41e05d36aed 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -8,6 +8,8 @@ 3. Copy the filenames with updated suffixes from the directory. */ +/* ignore-tidy-filelength */ + :root { --nav-sub-mobile-padding: 8px; --search-typename-width: 6.75rem; @@ -915,32 +917,74 @@ ul.block, .block li, .block ul { overflow: auto; } -.example-wrap.digits-1:not(.hide-lines) [data-nosnippet] { - width: calc(1ch + var(--line-number-padding) * 2); +.example-wrap code { + position: relative; } -.example-wrap.digits-2:not(.hide-lines) [data-nosnippet] { - width: calc(2ch + var(--line-number-padding) * 2); +.example-wrap pre code span { + display: inline; } -.example-wrap.digits-3:not(.hide-lines) [data-nosnippet] { - width: calc(3ch + var(--line-number-padding) * 2); +.example-wrap .expansion { + position: relative; + display: inline; } -.example-wrap.digits-4:not(.hide-lines) [data-nosnippet] { - width: calc(4ch + var(--line-number-padding) * 2); +.example-wrap .expansion > input { + display: block; + position: absolute; + appearance: none; + content: '↕'; + left: -20px; + top: 0; + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: pointer; + color: var(--main-color); + padding: 0 2px; + line-height: 20px; } -.example-wrap.digits-5:not(.hide-lines) [data-nosnippet] { - width: calc(5ch + var(--line-number-padding) * 2); +.example-wrap .expansion > input::after { + content: "↕"; } -.example-wrap.digits-6:not(.hide-lines) [data-nosnippet] { - width: calc(6ch + var(--line-number-padding) * 2); +.example-wrap .expansion .expanded { + display: none; + color: var(--main-color); } -.example-wrap.digits-7:not(.hide-lines) [data-nosnippet] { - width: calc(7ch + var(--line-number-padding) * 2); +.example-wrap .expansion > input:checked ~ .expanded, +.example-wrap .expansion > input:checked ~ * .expanded { + display: inherit; } -.example-wrap.digits-8:not(.hide-lines) [data-nosnippet] { - width: calc(8ch + var(--line-number-padding) * 2); +.example-wrap .expansion > input:checked ~ .original, +.example-wrap .expansion > input:checked ~ * .original { + display: none; } -.example-wrap.digits-9:not(.hide-lines) [data-nosnippet] { - width: calc(9ch + var(--line-number-padding) * 2); + +.example-wrap.digits-1 { --example-wrap-digits-count: 1ch; } +.example-wrap.digits-2 { --example-wrap-digits-count: 2ch; } +.example-wrap.digits-3 { --example-wrap-digits-count: 3ch; } +.example-wrap.digits-4 { --example-wrap-digits-count: 4ch; } +.example-wrap.digits-5 { --example-wrap-digits-count: 5ch; } +.example-wrap.digits-6 { --example-wrap-digits-count: 6ch; } +.example-wrap.digits-7 { --example-wrap-digits-count: 7ch; } +.example-wrap.digits-8 { --example-wrap-digits-count: 8ch; } +.example-wrap.digits-9 { --example-wrap-digits-count: 9ch; } + +.example-wrap [data-nosnippet] { + width: calc(var(--example-wrap-digits-count) + var(--line-number-padding) * 2); +} +.example-wrap pre > code { + padding-left: calc( + var(--example-wrap-digits-count) + var(--line-number-padding) * 2 + + var(--line-number-right-margin)); +} +.src .example-wrap .expansion [data-nosnippet] { + /* FIXME: Once is solved, uncomment + next line and remove the two other rules. */ + /*left: calc(( + var(--example-wrap-digits-count) + var(--line-number-padding) * 2 + + var(--line-number-right-margin)) * -1);*/ + position: initial; + margin-left: calc(( + var(--example-wrap-digits-count) + var(--line-number-padding) * 2 + + var(--line-number-right-margin)) * -1); } .example-wrap [data-nosnippet] { @@ -953,63 +997,25 @@ ul.block, .block li, .block ul { -ms-user-select: none; user-select: none; padding: 0 var(--line-number-padding); -} -.example-wrap [data-nosnippet]:target { - border-right: none; -} -.example-wrap .line-highlighted[data-nosnippet] { - background-color: var(--src-line-number-highlighted-background-color); -} -:root.word-wrap-source-code .example-wrap [data-nosnippet] { position: absolute; left: 0; } -.word-wrap-source-code .example-wrap pre > code { +.example-wrap pre > code { position: relative; - word-break: break-all; + display: block; } :root.word-wrap-source-code .example-wrap pre > code { - display: block; + word-break: break-all; white-space: pre-wrap; } :root.word-wrap-source-code .example-wrap pre > code * { word-break: break-all; } -:root.word-wrap-source-code .example-wrap.digits-1 pre > code { - padding-left: calc( - 1ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); -} -:root.word-wrap-source-code .example-wrap.digits-2 pre > code { - padding-left: calc( - 2ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); -} -:root.word-wrap-source-code .example-wrap.digits-3 pre > code { - padding-left: calc( - 3ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); -} -:root.word-wrap-source-code .example-wrap.digits-4 pre > code { - padding-left: calc( - 4ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); -} -:root.word-wrap-source-code .example-wrap.digits-5 pre > code { - padding-left: calc( - 5ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); -} -:root.word-wrap-source-code .example-wrap.digits-6 pre > code { - padding-left: calc( - 6ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); -} -:root.word-wrap-source-code .example-wrap.digits-7 pre > code { - padding-left: calc( - 7ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); -} -:root.word-wrap-source-code .example-wrap.digits-8 pre > code { - padding-left: calc( - 8ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); +.example-wrap [data-nosnippet]:target { + border-right: none; } -:root.word-wrap-source-code .example-wrap.digits-9 pre > code { - padding-left: calc( - 9ch + var(--line-number-padding) * 2 + var(--line-number-right-margin)); +.example-wrap .line-highlighted[data-nosnippet] { + background-color: var(--src-line-number-highlighted-background-color); } .example-wrap.hide-lines [data-nosnippet] { display: none; diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs index 131a12ce228f7..79ef7ca80ed28 100644 --- a/src/librustdoc/json/mod.rs +++ b/src/librustdoc/json/mod.rs @@ -176,15 +176,8 @@ fn target(sess: &rustc_session::Session) -> types::Target { } } -impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> { - fn descr() -> &'static str { - "json" - } - - const RUN_ON_MODULE: bool = false; - type ModuleData = (); - - fn init( +impl<'tcx> JsonRenderer<'tcx> { + pub(crate) fn init( krate: clean::Crate, options: RenderOptions, cache: Cache, @@ -206,6 +199,15 @@ impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> { krate, )) } +} + +impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> { + fn descr() -> &'static str { + "json" + } + + const RUN_ON_MODULE: bool = false; + type ModuleData = (); fn save_module_data(&mut self) -> Self::ModuleData { unreachable!("RUN_ON_MODULE = false, should never call save_module_data") diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 025c135aff2a6..8a1cf9efe37e1 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -87,6 +87,8 @@ use rustc_session::{EarlyDiagCtxt, getopts}; use tracing::info; use crate::clean::utils::DOC_RUST_LANG_ORG_VERSION; +use crate::error::Error; +use crate::formats::cache::Cache; /// A macro to create a FxHashMap. /// @@ -711,6 +713,14 @@ fn opts() -> Vec { "removed, see issue #44136 for more information", "[rust]", ), + opt( + Unstable, + Flag, + "", + "generate-macro-expansion", + "Add possibility to expand macros in the HTML source code pages", + "", + ), ] } @@ -733,13 +743,23 @@ pub(crate) fn wrap_return(dcx: DiagCtxtHandle<'_>, res: Result<(), String>) { } } -fn run_renderer<'tcx, T: formats::FormatRenderer<'tcx>>( +fn run_renderer< + 'tcx, + T: formats::FormatRenderer<'tcx>, + F: FnOnce( + clean::Crate, + config::RenderOptions, + Cache, + TyCtxt<'tcx>, + ) -> Result<(T, clean::Crate), Error>, +>( krate: clean::Crate, renderopts: config::RenderOptions, cache: formats::cache::Cache, tcx: TyCtxt<'tcx>, + init: F, ) { - match formats::run_format::(krate, renderopts, cache, tcx) { + match formats::run_format::(krate, renderopts, cache, tcx, init) { Ok(_) => tcx.dcx().abort_if_errors(), Err(e) => { let mut msg = @@ -869,6 +889,7 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) { let scrape_examples_options = options.scrape_examples_options.clone(); let bin_crate = options.bin_crate; + let output_format = options.output_format; let config = core::create_config(input, options, &render_options); let registered_lints = config.register_lints.is_some(); @@ -887,9 +908,10 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) { sess.dcx().fatal("Compilation failed, aborting rustdoc"); } - let (krate, render_opts, mut cache) = sess.time("run_global_ctxt", || { - core::run_global_ctxt(tcx, show_coverage, render_options, output_format) - }); + let (krate, render_opts, mut cache, expanded_macros) = sess + .time("run_global_ctxt", || { + core::run_global_ctxt(tcx, show_coverage, render_options, output_format) + }); info!("finished with rustc"); if let Some(options) = scrape_examples_options { @@ -920,10 +942,32 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) { info!("going to format"); match output_format { config::OutputFormat::Html => sess.time("render_html", || { - run_renderer::>(krate, render_opts, cache, tcx) + run_renderer( + krate, + render_opts, + cache, + tcx, + |krate, render_opts, cache, tcx| { + html::render::Context::init( + krate, + render_opts, + cache, + tcx, + expanded_macros, + ) + }, + ) }), config::OutputFormat::Json => sess.time("render_json", || { - run_renderer::>(krate, render_opts, cache, tcx) + run_renderer( + krate, + render_opts, + cache, + tcx, + |krate, render_opts, cache, tcx| { + json::JsonRenderer::init(krate, render_opts, cache, tcx) + }, + ) }), // Already handled above with doctest runners. config::OutputFormat::Doctest => unreachable!(), diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index fceacb69fb55e..f206d7494f1cd 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -18,7 +18,6 @@ use rustc_span::edition::Edition; use rustc_span::{BytePos, FileName, SourceFile}; use tracing::{debug, trace, warn}; -use crate::formats::renderer::FormatRenderer; use crate::html::render::Context; use crate::{clean, config, formats}; @@ -276,7 +275,8 @@ pub(crate) fn run( let inner = move || -> Result<(), String> { // Generates source files for examples renderopts.no_emit_shared = true; - let (cx, _) = Context::init(krate, renderopts, cache, tcx).map_err(|e| e.to_string())?; + let (cx, _) = Context::init(krate, renderopts, cache, tcx, Default::default()) + .map_err(|e| e.to_string())?; // Collect CrateIds corresponding to provided target crates // If two different versions of the crate in the dependency tree, then examples will be diff --git a/tests/run-make/rustdoc-default-output/output-default.stdout b/tests/run-make/rustdoc-default-output/output-default.stdout index 506f135ff8e72..765c2060c721e 100644 --- a/tests/run-make/rustdoc-default-output/output-default.stdout +++ b/tests/run-make/rustdoc-default-output/output-default.stdout @@ -212,6 +212,9 @@ Options: removed, see issue #44136 for more information + --generate-macro-expansion + Add possibility to expand macros in the HTML source + code pages @path Read newline separated options from `path` diff --git a/tests/rustdoc-gui/docblock-code-block-line-number.goml b/tests/rustdoc-gui/docblock-code-block-line-number.goml index 97273ceb195fa..0df9cc2a65983 100644 --- a/tests/rustdoc-gui/docblock-code-block-line-number.goml +++ b/tests/rustdoc-gui/docblock-code-block-line-number.goml @@ -129,13 +129,13 @@ define-function: ("check-line-numbers-existence", [], block { wait-for-local-storage-false: {"rustdoc-line-numbers": "true" } assert-false: ".example-line-numbers" // Line numbers should still be there. - assert-css: ("[data-nosnippet]", { "display": "inline-block"}) + assert-css: ("[data-nosnippet]", { "display": "block"}) // Now disabling the setting. click: "input#line-numbers" wait-for-local-storage: {"rustdoc-line-numbers": "true" } assert-false: ".example-line-numbers" // Line numbers should still be there. - assert-css: ("[data-nosnippet]", { "display": "inline-block"}) + assert-css: ("[data-nosnippet]", { "display": "block"}) // Closing settings menu. click: "#settings-menu" wait-for-css: ("#settings", {"display": "none"}) diff --git a/tests/rustdoc-gui/macro-expansion.goml b/tests/rustdoc-gui/macro-expansion.goml new file mode 100644 index 0000000000000..5e9a30494052d --- /dev/null +++ b/tests/rustdoc-gui/macro-expansion.goml @@ -0,0 +1,113 @@ +// This test ensures that the macro expansion is generated and working as expected. +go-to: "file://" + |DOC_PATH| + "/src/macro_expansion/lib.rs.html" + +define-function: ( + "check-expansion", + [line, original_content], + block { + assert-text: ("a[id='" + |line| + "'] + .expansion .original", |original_content|) + // The "original" content should be expanded. + assert-css: ("a[id='" + |line| + "'] + .expansion .original", {"display": "inline"}) + // The expanded macro should be hidden. + assert-css: ("a[id='" + |line| + "'] + .expansion .expanded", {"display": "none"}) + + // We "expand" the macro. + click: "a[id='" + |line| + "'] + .expansion input[type=checkbox]" + // The "original" content is hidden. + assert-css: ("a[id='" + |line| + "'] + .expansion .original", {"display": "none"}) + // The expanded macro is visible. + assert-css: ("a[id='" + |line| + "'] + .expansion .expanded", {"display": "inline"}) + + // We collapse the macro. + click: "a[id='" + |line| + "'] + .expansion input[type=checkbox]" + // The "original" content is expanded. + assert-css: ("a[id='" + |line| + "'] + .expansion .original", {"display": "inline"}) + // The expanded macro is hidden. + assert-css: ("a[id='" + |line| + "'] + .expansion .expanded", {"display": "none"}) + } +) + +// First we check the derive macro expansion at line 33. +call-function: ("check-expansion", {"line": 33, "original_content": "Debug"}) +// Then we check the `bar` macro expansion at line 41. +call-function: ("check-expansion", {"line": 41, "original_content": "bar!(y)"}) +// Then we check the `println` macro expansion at line 42-44. +call-function: ("check-expansion", {"line": 42, "original_content": 'println!(" +43 {y} +44 ")'}) + +// Then finally we check when there are two macro calls on a same line. +assert-count: ("#expand-50 ~ .original", 2) +assert-count: ("#expand-50 ~ .expanded", 2) + +store-value: (repeat_o, '/following-sibling::*[@class="original"]') +store-value: (repeat_e, '/following-sibling::*[@class="expanded"]') +assert-text: ('//*[@id="expand-50"]' + |repeat_o|, "stringify!(foo)") +assert-text: ('//*[@id="expand-50"]' + |repeat_o| + |repeat_o|, "stringify!(bar)") +assert-text: ('//*[@id="expand-50"]' + |repeat_e|, '"foo"') +assert-text: ('//*[@id="expand-50"]' + |repeat_e| + |repeat_e|, '"bar"') + +// The "original" content should be expanded. +assert-css: ('//*[@id="expand-50"]' + |repeat_o|, {"display": "inline"}) +assert-css: ('//*[@id="expand-50"]' + |repeat_o| + |repeat_o|, {"display": "inline"}) +// The expanded macro should be hidden. +assert-css: ('//*[@id="expand-50"]' + |repeat_e|, {"display": "none"}) +assert-css: ('//*[@id="expand-50"]' + |repeat_e| + |repeat_e|, {"display": "none"}) + +// We "expand" the macro (because the line starts with a string, the label is not at the "top +// level" of the ``, so we need to use a different selector). +click: "#expand-50" +// The "original" content is hidden. +assert-css: ('//*[@id="expand-50"]' + |repeat_o|, {"display": "none"}) +assert-css: ('//*[@id="expand-50"]' + |repeat_o| + |repeat_o|, {"display": "none"}) +// The expanded macro is visible. +assert-css: ('//*[@id="expand-50"]' + |repeat_e|, {"display": "inline"}) +assert-css: ('//*[@id="expand-50"]' + |repeat_e| + |repeat_e|, {"display": "inline"}) + +// We collapse the macro. +click: "#expand-50" +// The "original" content is expanded. +assert-css: ('//*[@id="expand-50"]' + |repeat_o|, {"display": "inline"}) +assert-css: ('//*[@id="expand-50"]' + |repeat_o| + |repeat_o|, {"display": "inline"}) +// The expanded macro is hidden. +assert-css: ('//*[@id="expand-50"]' + |repeat_e|, {"display": "none"}) +assert-css: ('//*[@id="expand-50"]' + |repeat_e| + |repeat_e|, {"display": "none"}) + +// Checking the line 46 `println` which needs to be handled differently because the line number is +// inside a "comment" span. +assert-text: ("#expand-46 ~ .original", 'println!(" +47 {y} +48 ")') +// The "original" content should be expanded. +assert-css: ("#expand-46 ~ .original", {"display": "inline"}) +// The expanded macro should be hidden. +assert-css: ("#expand-46 ~ .expanded", {"display": "none"}) + +// We "expand" the macro. +click: "#expand-46" +// The "original" content is hidden. +assert-css: ("#expand-46 ~ .original", {"display": "none"}) +// The expanded macro is visible. +assert-css: ("#expand-46 ~ .expanded", {"display": "inline"}) + +// We collapse the macro. +click: "#expand-46" +// The "original" content is expanded. +assert-css: ("#expand-46 ~ .original", {"display": "inline"}) +// The expanded macro is hidden. +assert-css: ("#expand-46 ~ .expanded", {"display": "none"}) + +// Ensure that the toggles are focusable and can be interacted with keyboard. +focus: "//a[@id='27']" +press-key: "Tab" +assert: "#expand-27:focus" +assert-css: ("#expand-27 ~ .expanded", {"display": "none"}) +assert-css: ("#expand-27 ~ .original", {"display": "inline"}) +// We now expand the macro. +press-key: "Space" +assert-css: ("#expand-27 ~ .expanded", {"display": "inline"}) +assert-css: ("#expand-27 ~ .original", {"display": "none"}) +// We collapse the macro. +press-key: "Space" +assert-css: ("#expand-27 ~ .expanded", {"display": "none"}) +assert-css: ("#expand-27 ~ .original", {"display": "inline"}) diff --git a/tests/rustdoc-gui/scrape-examples-button-focus.goml b/tests/rustdoc-gui/scrape-examples-button-focus.goml index 12246a3766151..f6e836e2360d8 100644 --- a/tests/rustdoc-gui/scrape-examples-button-focus.goml +++ b/tests/rustdoc-gui/scrape-examples-button-focus.goml @@ -5,7 +5,7 @@ go-to: "file://" + |DOC_PATH| + "/scrape_examples/fn.test.html" // The next/prev buttons vertically scroll the code viewport between examples move-cursor-to: ".scraped-example-list > .scraped-example" wait-for: ".scraped-example-list > .scraped-example .next" -store-value: (initialScrollTop, 250) +store-value: (initialScrollTop, 236) assert-property: (".scraped-example-list > .scraped-example .rust", { "scrollTop": |initialScrollTop|, }, NEAR) diff --git a/tests/rustdoc-gui/sidebar-source-code.goml b/tests/rustdoc-gui/sidebar-source-code.goml index 6afccf6a95fe2..a28e9b22a7191 100644 --- a/tests/rustdoc-gui/sidebar-source-code.goml +++ b/tests/rustdoc-gui/sidebar-source-code.goml @@ -71,7 +71,7 @@ assert: "//*[@class='dir-entry' and @open]/*[normalize-space()='sub_mod']" // Only "another_folder" should be "open" in "lib2". assert: "//*[@class='dir-entry' and not(@open)]/*[normalize-space()='another_mod']" // All other trees should be collapsed. -assert-count: ("//*[@id='src-sidebar']/details[not(normalize-space()='lib2') and not(@open)]", 11) +assert-count: ("//*[@id='src-sidebar']/details[not(normalize-space()='lib2') and not(@open)]", 12) // We now switch to mobile mode. set-window-size: (600, 600) diff --git a/tests/rustdoc-gui/source-code-wrapping.goml b/tests/rustdoc-gui/source-code-wrapping.goml index cb2fd3052cdac..0dab9c72ea9fd 100644 --- a/tests/rustdoc-gui/source-code-wrapping.goml +++ b/tests/rustdoc-gui/source-code-wrapping.goml @@ -31,17 +31,32 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/trait_bounds/index.html" click: "#settings-menu" wait-for: "#settings" -store-size: (".example-wrap .rust code", {"width": rust_width, "height": rust_height}) -store-size: (".example-wrap .language-text code", {"width": txt_width, "height": txt_height}) +store-property: (".example-wrap .rust code", {"scrollWidth": rust_width, "scrollHeight": rust_height}) +store-property: (".example-wrap .language-text code", {"scrollWidth": txt_width, "scrollHeight": txt_height}) call-function: ("click-code-wrapping", {"expected": "true"}) -wait-for-size-false: (".example-wrap .rust code", {"width": |rust_width|, "height": |rust_height|}) +wait-for-property-false: ( + ".example-wrap .rust code", + {"scrollWidth": |rust_width|, "scrollHeight": |rust_height|}, +) -store-size: (".example-wrap .rust code", {"width": new_rust_width, "height": new_rust_height}) -store-size: (".example-wrap .language-text code", {"width": new_txt_width, "height": new_txt_height}) +store-property: ( + ".example-wrap .rust code", + {"scrollWidth": new_rust_width, "scrollHeight": new_rust_height}, +) +store-property: ( + ".example-wrap .language-text code", + {"scrollWidth": new_txt_width, "scrollHeight": new_txt_height}, +) assert: |rust_width| > |new_rust_width| && |rust_height| < |new_rust_height| assert: |txt_width| > |new_txt_width| && |txt_height| < |new_txt_height| call-function: ("click-code-wrapping", {"expected": "false"}) -wait-for-size: (".example-wrap .rust code", {"width": |rust_width|, "height": |rust_height|}) -assert-size: (".example-wrap .language-text code", {"width": |txt_width|, "height": |txt_height|}) +wait-for-property: ( + ".example-wrap .rust code", + {"scrollWidth": |rust_width|, "scrollHeight": |rust_height|}, +) +assert-property: ( + ".example-wrap .language-text code", + {"scrollWidth": |txt_width|, "scrollHeight": |txt_height|}, +) diff --git a/tests/rustdoc-gui/src/macro_expansion/Cargo.lock b/tests/rustdoc-gui/src/macro_expansion/Cargo.lock new file mode 100644 index 0000000000000..9c5cee8fb9dca --- /dev/null +++ b/tests/rustdoc-gui/src/macro_expansion/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "macro_expansion" +version = "0.1.0" diff --git a/tests/rustdoc-gui/src/macro_expansion/Cargo.toml b/tests/rustdoc-gui/src/macro_expansion/Cargo.toml new file mode 100644 index 0000000000000..6d362850fc52f --- /dev/null +++ b/tests/rustdoc-gui/src/macro_expansion/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "macro_expansion" +version = "0.1.0" +edition = "2021" + +[lib] +path = "lib.rs" diff --git a/tests/rustdoc-gui/src/macro_expansion/lib.rs b/tests/rustdoc-gui/src/macro_expansion/lib.rs new file mode 100644 index 0000000000000..7926e0143d8f0 --- /dev/null +++ b/tests/rustdoc-gui/src/macro_expansion/lib.rs @@ -0,0 +1,51 @@ +// Test crate used to check the `--generate-macro-expansion` option. +//@ compile-flags: -Zunstable-options --generate-macro-expansion --generate-link-to-definition + +#[macro_export] +macro_rules! bar { + ($x:ident) => {{ + $x += 2; + $x *= 2; + }} +} + +macro_rules! bar2 { + () => { + fn foo2() -> impl std::fmt::Display { + String::new() + } + } +} + +macro_rules! bar3 { + () => { + fn foo3() {} + fn foo4() -> String { String::new() } + } +} + +bar2!(); +bar3!(); + +#[derive(Debug, PartialEq)] +pub struct Bar; + +#[derive(Debug +)] +pub struct Bar2; + +fn y_f(_: &str, _: &str, _: &str) {} + +fn foo() { + let mut y = 0; + bar!(y); + println!(" + {y} + "); + // comment + println!(" + {y} + "); + let s = y_f("\ +bla", stringify!(foo), stringify!(bar)); +}