From 1b0c7701cc97cd7bef8bb9729011d4cf291a60c5 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 13 Aug 2020 17:42:52 +0200 Subject: Rename ra_ide -> ide --- crates/ide/src/syntax_highlighting/html.rs | 97 ++++++ crates/ide/src/syntax_highlighting/injection.rs | 187 ++++++++++ crates/ide/src/syntax_highlighting/tags.rs | 203 +++++++++++ crates/ide/src/syntax_highlighting/tests.rs | 445 ++++++++++++++++++++++++ 4 files changed, 932 insertions(+) create mode 100644 crates/ide/src/syntax_highlighting/html.rs create mode 100644 crates/ide/src/syntax_highlighting/injection.rs create mode 100644 crates/ide/src/syntax_highlighting/tags.rs create mode 100644 crates/ide/src/syntax_highlighting/tests.rs (limited to 'crates/ide/src/syntax_highlighting') diff --git a/crates/ide/src/syntax_highlighting/html.rs b/crates/ide/src/syntax_highlighting/html.rs new file mode 100644 index 000000000..249368ff8 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/html.rs @@ -0,0 +1,97 @@ +//! Renders a bit of code as HTML. + +use base_db::SourceDatabase; +use oorandom::Rand32; +use syntax::{AstNode, TextRange, TextSize}; + +use crate::{syntax_highlighting::highlight, FileId, RootDatabase}; + +pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { + let parse = db.parse(file_id); + + fn rainbowify(seed: u64) -> String { + let mut rng = Rand32::new(seed); + format!( + "hsl({h},{s}%,{l}%)", + h = rng.rand_range(0..361), + s = rng.rand_range(42..99), + l = rng.rand_range(40..91), + ) + } + + let ranges = highlight(db, file_id, None, false); + let text = parse.tree().syntax().to_string(); + let mut prev_pos = TextSize::from(0); + let mut buf = String::new(); + buf.push_str(&STYLE); + buf.push_str("
");
+    for range in &ranges {
+        if range.range.start() > prev_pos {
+            let curr = &text[TextRange::new(prev_pos, range.range.start())];
+            let text = html_escape(curr);
+            buf.push_str(&text);
+        }
+        let curr = &text[TextRange::new(range.range.start(), range.range.end())];
+
+        let class = range.highlight.to_string().replace('.', " ");
+        let color = match (rainbow, range.binding_hash) {
+            (true, Some(hash)) => {
+                format!(" data-binding-hash=\"{}\" style=\"color: {};\"", hash, rainbowify(hash))
+            }
+            _ => "".into(),
+        };
+        buf.push_str(&format!("{}", class, color, html_escape(curr)));
+
+        prev_pos = range.range.end();
+    }
+    // Add the remaining (non-highlighted) text
+    let curr = &text[TextRange::new(prev_pos, TextSize::of(&text))];
+    let text = html_escape(curr);
+    buf.push_str(&text);
+    buf.push_str("
"); + buf +} + +//FIXME: like, real html escaping +fn html_escape(text: &str) -> String { + text.replace("<", "<").replace(">", ">") +} + +const STYLE: &str = " + +"; diff --git a/crates/ide/src/syntax_highlighting/injection.rs b/crates/ide/src/syntax_highlighting/injection.rs new file mode 100644 index 000000000..43f4e6fea --- /dev/null +++ b/crates/ide/src/syntax_highlighting/injection.rs @@ -0,0 +1,187 @@ +//! Syntax highlighting injections such as highlighting of documentation tests. + +use std::{collections::BTreeMap, convert::TryFrom}; + +use ast::{HasQuotes, HasStringValue}; +use hir::Semantics; +use itertools::Itertools; +use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize}; + +use crate::{ + call_info::ActiveParameter, Analysis, Highlight, HighlightModifier, HighlightTag, + HighlightedRange, RootDatabase, +}; + +use super::HighlightedRangeStack; + +pub(super) fn highlight_injection( + acc: &mut HighlightedRangeStack, + sema: &Semantics, + literal: ast::RawString, + expanded: SyntaxToken, +) -> Option<()> { + let active_parameter = ActiveParameter::at_token(&sema, expanded)?; + if !active_parameter.name.starts_with("ra_fixture") { + return None; + } + let value = literal.value()?; + let (analysis, tmp_file_id) = Analysis::from_single_file(value.into_owned()); + + if let Some(range) = literal.open_quote_text_range() { + acc.add(HighlightedRange { + range, + highlight: HighlightTag::StringLiteral.into(), + binding_hash: None, + }) + } + + for mut h in analysis.highlight(tmp_file_id).unwrap() { + if let Some(r) = literal.map_range_up(h.range) { + h.range = r; + acc.add(h) + } + } + + if let Some(range) = literal.close_quote_text_range() { + acc.add(HighlightedRange { + range, + highlight: HighlightTag::StringLiteral.into(), + binding_hash: None, + }) + } + + Some(()) +} + +/// Mapping from extracted documentation code to original code +type RangesMap = BTreeMap; + +const RUSTDOC_FENCE: &'static str = "```"; +const RUSTDOC_FENCE_TOKENS: &[&'static str] = + &["", "rust", "should_panic", "ignore", "no_run", "compile_fail", "edition2015", "edition2018"]; + +/// Extracts Rust code from documentation comments as well as a mapping from +/// the extracted source code back to the original source ranges. +/// Lastly, a vector of new comment highlight ranges (spanning only the +/// comment prefix) is returned which is used in the syntax highlighting +/// injection to replace the previous (line-spanning) comment ranges. +pub(super) fn extract_doc_comments( + node: &SyntaxNode, +) -> Option<(String, RangesMap, Vec)> { + // wrap the doctest into function body to get correct syntax highlighting + let prefix = "fn doctest() {\n"; + let suffix = "}\n"; + // Mapping from extracted documentation code to original code + let mut range_mapping: RangesMap = BTreeMap::new(); + let mut line_start = TextSize::try_from(prefix.len()).unwrap(); + let mut is_codeblock = false; + let mut is_doctest = false; + // Replace the original, line-spanning comment ranges by new, only comment-prefix + // spanning comment ranges. + let mut new_comments = Vec::new(); + let doctest = node + .children_with_tokens() + .filter_map(|el| el.into_token().and_then(ast::Comment::cast)) + .filter(|comment| comment.kind().doc.is_some()) + .filter(|comment| { + if let Some(idx) = comment.text().find(RUSTDOC_FENCE) { + is_codeblock = !is_codeblock; + // Check whether code is rust by inspecting fence guards + let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..]; + let is_rust = + guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim())); + is_doctest = is_codeblock && is_rust; + false + } else { + is_doctest + } + }) + .map(|comment| { + let prefix_len = comment.prefix().len(); + let line: &str = comment.text().as_str(); + let range = comment.syntax().text_range(); + + // whitespace after comment is ignored + let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) { + prefix_len + ws.len_utf8() + } else { + prefix_len + }; + + // lines marked with `#` should be ignored in output, we skip the `#` char + let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') { + pos + ws.len_utf8() + } else { + pos + }; + + range_mapping.insert(line_start, range.start() + TextSize::try_from(pos).unwrap()); + new_comments.push(HighlightedRange { + range: TextRange::new( + range.start(), + range.start() + TextSize::try_from(pos).unwrap(), + ), + highlight: HighlightTag::Comment | HighlightModifier::Documentation, + binding_hash: None, + }); + line_start += range.len() - TextSize::try_from(pos).unwrap(); + line_start += TextSize::try_from('\n'.len_utf8()).unwrap(); + + line[pos..].to_owned() + }) + .join("\n"); + + if doctest.is_empty() { + return None; + } + + let doctest = format!("{}{}{}", prefix, doctest, suffix); + Some((doctest, range_mapping, new_comments)) +} + +/// Injection of syntax highlighting of doctests. +pub(super) fn highlight_doc_comment( + text: String, + range_mapping: RangesMap, + new_comments: Vec, + stack: &mut HighlightedRangeStack, +) { + let (analysis, tmp_file_id) = Analysis::from_single_file(text); + + stack.push(); + for mut h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() { + // Determine start offset and end offset in case of multi-line ranges + let mut start_offset = None; + let mut end_offset = None; + for (line_start, orig_line_start) in range_mapping.range(..h.range.end()).rev() { + // It's possible for orig_line_start - line_start to be negative. Add h.range.start() + // here and remove it from the end range after the loop below so that the values are + // always non-negative. + let offset = h.range.start() + orig_line_start - line_start; + if line_start <= &h.range.start() { + start_offset.get_or_insert(offset); + break; + } else { + end_offset.get_or_insert(offset); + } + } + if let Some(start_offset) = start_offset { + h.range = TextRange::new( + start_offset, + h.range.end() + end_offset.unwrap_or(start_offset) - h.range.start(), + ); + + h.highlight |= HighlightModifier::Injected; + stack.add(h); + } + } + + // Inject the comment prefix highlight ranges + stack.push(); + for comment in new_comments { + stack.add(comment); + } + stack.pop_and_inject(None); + stack + .pop_and_inject(Some(Highlight::from(HighlightTag::Generic) | HighlightModifier::Injected)); +} diff --git a/crates/ide/src/syntax_highlighting/tags.rs b/crates/ide/src/syntax_highlighting/tags.rs new file mode 100644 index 000000000..49ec94bdc --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tags.rs @@ -0,0 +1,203 @@ +//! Defines token tags we use for syntax highlighting. +//! A tag is not unlike a CSS class. + +use std::{fmt, ops}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Highlight { + pub tag: HighlightTag, + pub modifiers: HighlightModifiers, +} + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct HighlightModifiers(u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum HighlightTag { + Attribute, + BoolLiteral, + BuiltinType, + ByteLiteral, + CharLiteral, + Comment, + Constant, + Enum, + EnumVariant, + EscapeSequence, + Field, + Function, + Generic, + Keyword, + Lifetime, + Macro, + Module, + NumericLiteral, + Punctuation, + SelfKeyword, + SelfType, + Static, + StringLiteral, + Struct, + Trait, + TypeAlias, + TypeParam, + Union, + ValueParam, + Local, + UnresolvedReference, + FormatSpecifier, + Operator, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(u8)] +pub enum HighlightModifier { + /// Used to differentiate individual elements within attributes. + Attribute = 0, + /// Used with keywords like `if` and `break`. + ControlFlow, + /// `foo` in `fn foo(x: i32)` is a definition, `foo` in `foo(90 + 2)` is + /// not. + Definition, + Documentation, + Injected, + Mutable, + Unsafe, +} + +impl HighlightTag { + fn as_str(self) -> &'static str { + match self { + HighlightTag::Attribute => "attribute", + HighlightTag::BoolLiteral => "bool_literal", + HighlightTag::BuiltinType => "builtin_type", + HighlightTag::ByteLiteral => "byte_literal", + HighlightTag::CharLiteral => "char_literal", + HighlightTag::Comment => "comment", + HighlightTag::Constant => "constant", + HighlightTag::Enum => "enum", + HighlightTag::EnumVariant => "enum_variant", + HighlightTag::EscapeSequence => "escape_sequence", + HighlightTag::Field => "field", + HighlightTag::FormatSpecifier => "format_specifier", + HighlightTag::Function => "function", + HighlightTag::Generic => "generic", + HighlightTag::Keyword => "keyword", + HighlightTag::Lifetime => "lifetime", + HighlightTag::Punctuation => "punctuation", + HighlightTag::Macro => "macro", + HighlightTag::Module => "module", + HighlightTag::NumericLiteral => "numeric_literal", + HighlightTag::Operator => "operator", + HighlightTag::SelfKeyword => "self_keyword", + HighlightTag::SelfType => "self_type", + HighlightTag::Static => "static", + HighlightTag::StringLiteral => "string_literal", + HighlightTag::Struct => "struct", + HighlightTag::Trait => "trait", + HighlightTag::TypeAlias => "type_alias", + HighlightTag::TypeParam => "type_param", + HighlightTag::Union => "union", + HighlightTag::ValueParam => "value_param", + HighlightTag::Local => "variable", + HighlightTag::UnresolvedReference => "unresolved_reference", + } + } +} + +impl fmt::Display for HighlightTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} + +impl HighlightModifier { + const ALL: &'static [HighlightModifier] = &[ + HighlightModifier::Attribute, + HighlightModifier::ControlFlow, + HighlightModifier::Definition, + HighlightModifier::Documentation, + HighlightModifier::Injected, + HighlightModifier::Mutable, + HighlightModifier::Unsafe, + ]; + + fn as_str(self) -> &'static str { + match self { + HighlightModifier::Attribute => "attribute", + HighlightModifier::ControlFlow => "control", + HighlightModifier::Definition => "declaration", + HighlightModifier::Documentation => "documentation", + HighlightModifier::Injected => "injected", + HighlightModifier::Mutable => "mutable", + HighlightModifier::Unsafe => "unsafe", + } + } + + fn mask(self) -> u32 { + 1 << (self as u32) + } +} + +impl fmt::Display for HighlightModifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} + +impl fmt::Display for Highlight { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.tag)?; + for modifier in self.modifiers.iter() { + write!(f, ".{}", modifier)? + } + Ok(()) + } +} + +impl From for Highlight { + fn from(tag: HighlightTag) -> Highlight { + Highlight::new(tag) + } +} + +impl Highlight { + pub(crate) fn new(tag: HighlightTag) -> Highlight { + Highlight { tag, modifiers: HighlightModifiers::default() } + } +} + +impl ops::BitOr for HighlightTag { + type Output = Highlight; + + fn bitor(self, rhs: HighlightModifier) -> Highlight { + Highlight::new(self) | rhs + } +} + +impl ops::BitOrAssign for HighlightModifiers { + fn bitor_assign(&mut self, rhs: HighlightModifier) { + self.0 |= rhs.mask(); + } +} + +impl ops::BitOrAssign for Highlight { + fn bitor_assign(&mut self, rhs: HighlightModifier) { + self.modifiers |= rhs; + } +} + +impl ops::BitOr for Highlight { + type Output = Highlight; + + fn bitor(mut self, rhs: HighlightModifier) -> Highlight { + self |= rhs; + self + } +} + +impl HighlightModifiers { + pub fn iter(self) -> impl Iterator { + HighlightModifier::ALL.iter().copied().filter(move |it| self.0 & it.mask() == it.mask()) + } +} diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs new file mode 100644 index 000000000..94f37d773 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tests.rs @@ -0,0 +1,445 @@ +use std::fs; + +use expect::{expect_file, ExpectFile}; +use test_utils::project_dir; + +use crate::{mock_analysis::single_file, FileRange, TextRange}; + +#[test] +fn test_highlighting() { + check_highlighting( + r#" +use inner::{self as inner_mod}; +mod inner {} + +#[derive(Clone, Debug)] +struct Foo { + pub x: i32, + pub y: i32, +} + +trait Bar { + fn bar(&self) -> i32; +} + +impl Bar for Foo { + fn bar(&self) -> i32 { + self.x + } +} + +impl Foo { + fn baz(mut self) -> i32 { + self.x + } + + fn qux(&mut self) { + self.x = 0; + } +} + +static mut STATIC_MUT: i32 = 0; + +fn foo<'a, T>() -> T { + foo::<'a, i32>() +} + +macro_rules! def_fn { + ($($tt:tt)*) => {$($tt)*} +} + +def_fn! { + fn bar() -> u32 { + 100 + } +} + +macro_rules! noop { + ($expr:expr) => { + $expr + } +} + +// comment +fn main() { + println!("Hello, {}!", 92); + + let mut vec = Vec::new(); + if true { + let x = 92; + vec.push(Foo { x, y: 1 }); + } + unsafe { + vec.set_len(0); + STATIC_MUT = 1; + } + + for e in vec { + // Do nothing + } + + noop!(noop!(1)); + + let mut x = 42; + let y = &mut x; + let z = &y; + + let Foo { x: z, y } = Foo { x: z, y }; + + y; +} + +enum Option { + Some(T), + None, +} +use Option::*; + +impl Option { + fn and(self, other: Option) -> Option<(T, U)> { + match other { + None => unimplemented!(), + Nope => Nope, + } + } +} +"# + .trim(), + expect_file!["crates/ide/test_data/highlighting.html"], + false, + ); +} + +#[test] +fn test_rainbow_highlighting() { + check_highlighting( + r#" +fn main() { + let hello = "hello"; + let x = hello.to_string(); + let y = hello.to_string(); + + let x = "other color please!"; + let y = x.to_string(); +} + +fn bar() { + let mut hello = "hello"; +} +"# + .trim(), + expect_file!["crates/ide/test_data/rainbow_highlighting.html"], + true, + ); +} + +#[test] +fn accidentally_quadratic() { + let file = project_dir().join("crates/syntax/test_data/accidentally_quadratic"); + let src = fs::read_to_string(file).unwrap(); + + let (analysis, file_id) = single_file(&src); + + // let t = std::time::Instant::now(); + let _ = analysis.highlight(file_id).unwrap(); + // eprintln!("elapsed: {:?}", t.elapsed()); +} + +#[test] +fn test_ranges() { + let (analysis, file_id) = single_file( + r#" +#[derive(Clone, Debug)] +struct Foo { + pub x: i32, + pub y: i32, +} +"#, + ); + + // The "x" + let highlights = &analysis + .highlight_range(FileRange { file_id, range: TextRange::at(45.into(), 1.into()) }) + .unwrap(); + + assert_eq!(&highlights[0].highlight.to_string(), "field.declaration"); +} + +#[test] +fn test_flattening() { + check_highlighting( + r##" +fn fixture(ra_fixture: &str) {} + +fn main() { + fixture(r#" + trait Foo { + fn foo() { + println!("2 + 2 = {}", 4); + } + }"# + ); +}"## + .trim(), + expect_file!["crates/ide/test_data/highlight_injection.html"], + false, + ); +} + +#[test] +fn ranges_sorted() { + let (analysis, file_id) = single_file( + r#" +#[foo(bar = "bar")] +macro_rules! test {} +}"# + .trim(), + ); + let _ = analysis.highlight(file_id).unwrap(); +} + +#[test] +fn test_string_highlighting() { + // The format string detection is based on macro-expansion, + // thus, we have to copy the macro definition from `std` + check_highlighting( + r#" +macro_rules! println { + ($($arg:tt)*) => ({ + $crate::io::_print($crate::format_args_nl!($($arg)*)); + }) +} +#[rustc_builtin_macro] +macro_rules! format_args_nl { + ($fmt:expr) => {{ /* compiler built-in */ }}; + ($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }}; +} + +fn main() { + // from https://doc.rust-lang.org/std/fmt/index.html + println!("Hello"); // => "Hello" + println!("Hello, {}!", "world"); // => "Hello, world!" + println!("The number is {}", 1); // => "The number is 1" + println!("{:?}", (3, 4)); // => "(3, 4)" + println!("{value}", value=4); // => "4" + println!("{} {}", 1, 2); // => "1 2" + println!("{:04}", 42); // => "0042" with leading zerosV + println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2" + println!("{argument}", argument = "test"); // => "test" + println!("{name} {}", 1, name = 2); // => "2 1" + println!("{a} {c} {b}", a="a", b='b', c=3); // => "a 3 b" + println!("{{{}}}", 2); // => "{2}" + println!("Hello {:5}!", "x"); + println!("Hello {:1$}!", "x", 5); + println!("Hello {1:0$}!", 5, "x"); + println!("Hello {:width$}!", "x", width = 5); + println!("Hello {:<5}!", "x"); + println!("Hello {:-<5}!", "x"); + println!("Hello {:^5}!", "x"); + println!("Hello {:>5}!", "x"); + println!("Hello {:+}!", 5); + println!("{:#x}!", 27); + println!("Hello {:05}!", 5); + println!("Hello {:05}!", -5); + println!("{:#010x}!", 27); + println!("Hello {0} is {1:.5}", "x", 0.01); + println!("Hello {1} is {2:.0$}", 5, "x", 0.01); + println!("Hello {0} is {2:.1$}", "x", 5, 0.01); + println!("Hello {} is {:.*}", "x", 5, 0.01); + println!("Hello {} is {2:.*}", "x", 5, 0.01); + println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01); + println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56); + println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56"); + println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56"); + println!("Hello {{}}"); + println!("{{ Hello"); + + println!(r"Hello, {}!", "world"); + + // escape sequences + println!("Hello\nWorld"); + println!("\u{48}\x65\x6C\x6C\x6F World"); + + println!("{\x41}", A = 92); + println!("{ничоси}", ничоси = 92); +}"# + .trim(), + expect_file!["crates/ide/test_data/highlight_strings.html"], + false, + ); +} + +#[test] +fn test_unsafe_highlighting() { + check_highlighting( + r#" +unsafe fn unsafe_fn() {} + +union Union { + a: u32, + b: f32, +} + +struct HasUnsafeFn; + +impl HasUnsafeFn { + unsafe fn unsafe_method(&self) {} +} + +struct TypeForStaticMut { + a: u8 +} + +static mut global_mut: TypeForStaticMut = TypeForStaticMut { a: 0 }; + +#[repr(packed)] +struct Packed { + a: u16, +} + +trait DoTheAutoref { + fn calls_autoref(&self); +} + +impl DoTheAutoref for u16 { + fn calls_autoref(&self) {} +} + +fn main() { + let x = &5 as *const _ as *const usize; + let u = Union { b: 0 }; + unsafe { + // unsafe fn and method calls + unsafe_fn(); + let b = u.b; + match u { + Union { b: 0 } => (), + Union { a } => (), + } + HasUnsafeFn.unsafe_method(); + + // unsafe deref + let y = *x; + + // unsafe access to a static mut + let a = global_mut.a; + + // unsafe ref of packed fields + let packed = Packed { a: 0 }; + let a = &packed.a; + let ref a = packed.a; + let Packed { ref a } = packed; + let Packed { a: ref _a } = packed; + + // unsafe auto ref of packed field + packed.a.calls_autoref(); + } +} +"# + .trim(), + expect_file!["crates/ide/test_data/highlight_unsafe.html"], + false, + ); +} + +#[test] +fn test_highlight_doctest() { + check_highlighting( + r#" +/// ``` +/// let _ = "early doctests should not go boom"; +/// ``` +struct Foo { + bar: bool, +} + +impl Foo { + pub const bar: bool = true; + + /// Constructs a new `Foo`. + /// + /// # Examples + /// + /// ``` + /// # #![allow(unused_mut)] + /// let mut foo: Foo = Foo::new(); + /// ``` + pub const fn new() -> Foo { + Foo { bar: true } + } + + /// `bar` method on `Foo`. + /// + /// # Examples + /// + /// ``` + /// use x::y; + /// + /// let foo = Foo::new(); + /// + /// // calls bar on foo + /// assert!(foo.bar()); + /// + /// let bar = foo.bar || Foo::bar; + /// + /// /* multi-line + /// comment */ + /// + /// let multi_line_string = "Foo + /// bar + /// "; + /// + /// ``` + /// + /// ```rust,no_run + /// let foobar = Foo::new().bar(); + /// ``` + /// + /// ```sh + /// echo 1 + /// ``` + pub fn foo(&self) -> bool { + true + } +} + +/// ``` +/// noop!(1); +/// ``` +macro_rules! noop { + ($expr:expr) => { + $expr + } +} +"# + .trim(), + expect_file!["crates/ide/test_data/highlight_doctest.html"], + false, + ); +} + +#[test] +fn test_extern_crate() { + check_highlighting( + r#" + //- /main.rs + extern crate std; + extern crate alloc as abc; + //- /std/lib.rs + pub struct S; + //- /alloc/lib.rs + pub struct A + "#, + expect_file!["crates/ide/test_data/highlight_extern_crate.html"], + false, + ); +} + +/// Highlights the code given by the `ra_fixture` argument, renders the +/// result as HTML, and compares it with the HTML file given as `snapshot`. +/// Note that the `snapshot` file is overwritten by the rendered HTML. +fn check_highlighting(ra_fixture: &str, expect: ExpectFile, rainbow: bool) { + let (analysis, file_id) = single_file(ra_fixture); + let actual_html = &analysis.highlight_as_html(file_id, rainbow).unwrap(); + expect.assert_eq(actual_html) +} -- cgit v1.2.3