From 43d5a4965308ec4b594725c0bd02cb046bdb730c Mon Sep 17 00:00:00 2001 From: Pascal Hertleif Date: Sat, 25 May 2019 16:23:58 +0200 Subject: [PATCH] More clever highlighting, incl draft for structs --- Cargo.lock | 1 + crates/ra_cli/src/main.rs | 9 +- crates/ra_ide_api/Cargo.toml | 1 + crates/ra_ide_api/src/lib.rs | 2 +- .../src/snapshots/highlighting.html | 24 +-- .../src/snapshots/rainbow_highlighting.html | 27 +++ .../src/snapshots/tests__highlighting.snap | 192 ------------------ .../tests__rainbow_highlighting.snap | 128 ------------ crates/ra_ide_api/src/syntax_highlighting.rs | 185 ++++++++++------- crates/ra_syntax/src/syntax_node.rs | 4 - 10 files changed, 159 insertions(+), 414 deletions(-) create mode 100644 crates/ra_ide_api/src/snapshots/rainbow_highlighting.html delete mode 100644 crates/ra_ide_api/src/snapshots/tests__highlighting.snap delete mode 100644 crates/ra_ide_api/src/snapshots/tests__rainbow_highlighting.snap diff --git a/Cargo.lock b/Cargo.lock index 79e6329076..68c5f78747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1155,6 +1155,7 @@ dependencies = [ "ra_prof 0.1.0", "ra_syntax 0.1.0", "ra_text_edit 0.1.0", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "relative-path 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/crates/ra_cli/src/main.rs b/crates/ra_cli/src/main.rs index 93aba4c704..bdc7a76c6e 100644 --- a/crates/ra_cli/src/main.rs +++ b/crates/ra_cli/src/main.rs @@ -16,7 +16,10 @@ fn main() -> Result<()> { .setting(clap::AppSettings::SubcommandRequiredElseHelp) .subcommand(SubCommand::with_name("parse").arg(Arg::with_name("no-dump").long("--no-dump"))) .subcommand(SubCommand::with_name("symbols")) - .subcommand(SubCommand::with_name("highlight")) + .subcommand( + SubCommand::with_name("highlight") + .arg(Arg::with_name("rainbow").short("r").long("rainbow")) + ) .subcommand( SubCommand::with_name("analysis-stats") .arg(Arg::with_name("verbose").short("v").long("verbose")) @@ -39,9 +42,9 @@ fn main() -> Result<()> { println!("{:?}", s); } } - ("highlight", _) => { + ("highlight", Some(matches)) => { let (analysis, file_id) = Analysis::from_single_file(read_stdin()?); - let html = analysis.highlight_as_html(file_id).unwrap(); + let html = analysis.highlight_as_html(file_id, matches.is_present("rainbow")).unwrap(); println!("{}", html); } ("analysis-stats", Some(matches)) => { diff --git a/crates/ra_ide_api/Cargo.toml b/crates/ra_ide_api/Cargo.toml index d399d5e2e0..8939e9d79d 100644 --- a/crates/ra_ide_api/Cargo.toml +++ b/crates/ra_ide_api/Cargo.toml @@ -15,6 +15,7 @@ rustc-hash = "1.0" parking_lot = "0.7.0" unicase = "2.2.0" superslice = "1.0.0" +rand = "0.6.5" jemallocator = { version = "0.1.9", optional = true } jemalloc-ctl = { version = "0.2.0", optional = true } diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index d3456d5b25..65a3b591ae 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs @@ -464,7 +464,7 @@ impl Analysis { /// Computes syntax highlighting for the given file. pub fn highlight_as_html(&self, file_id: FileId) -> Cancelable { - self.with_db(|db| syntax_highlighting::highlight_as_html(db, file_id)) + self.with_db(|db| syntax_highlighting::highlight_as_html(db, file_id, true)) } /// Computes completions at the given position. diff --git a/crates/ra_ide_api/src/snapshots/highlighting.html b/crates/ra_ide_api/src/snapshots/highlighting.html index bfc0a67b11..4f4ed62a1b 100644 --- a/crates/ra_ide_api/src/snapshots/highlighting.html +++ b/crates/ra_ide_api/src/snapshots/highlighting.html @@ -1,10 +1,7 @@ -

-#[derive(Clone, Debug)]
+
#[derive(Clone, Debug)]
 struct Foo {
-    pub x: i32,
-    pub y: i32,
+    pub x: i32,
+    pub y: i32,
 }
 
 fn foo<T>() -> T {
@@ -36,10 +31,9 @@ pre {
 fn main() {
     println!("Hello, {}!", 92);
 
-    let mut vec = Vec::new();
+    let mut vec = Vec::new();
     if true {
-        vec.push(Foo { x: 0, y: 1 });
+        vec.push(Foo { x: 0, y: 1 });
     }
-    unsafe { vec.set_len(0); }
-}
-
\ No newline at end of file + unsafe { vec.set_len(0); } +}
\ No newline at end of file diff --git a/crates/ra_ide_api/src/snapshots/rainbow_highlighting.html b/crates/ra_ide_api/src/snapshots/rainbow_highlighting.html new file mode 100644 index 0000000000..729d129d07 --- /dev/null +++ b/crates/ra_ide_api/src/snapshots/rainbow_highlighting.html @@ -0,0 +1,27 @@ + + +
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();
+}
\ No newline at end of file diff --git a/crates/ra_ide_api/src/snapshots/tests__highlighting.snap b/crates/ra_ide_api/src/snapshots/tests__highlighting.snap deleted file mode 100644 index e50003b3c0..0000000000 --- a/crates/ra_ide_api/src/snapshots/tests__highlighting.snap +++ /dev/null @@ -1,192 +0,0 @@ ---- -created: "2019-05-25T11:24:53.486036Z" -creator: insta@0.8.1 -source: crates/ra_ide_api/src/syntax_highlighting.rs -expression: result ---- -Ok( - [ - HighlightedRange { - range: [1; 24), - tag: "attribute", - id: None, - }, - HighlightedRange { - range: [25; 31), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [32; 35), - tag: "variable", - id: Some( - 8465336196764640996, - ), - }, - HighlightedRange { - range: [42; 45), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [46; 47), - tag: "variable", - id: Some( - 176272420896316891, - ), - }, - HighlightedRange { - range: [49; 52), - tag: "text", - id: None, - }, - HighlightedRange { - range: [58; 61), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [62; 63), - tag: "variable", - id: Some( - 15061637676198917049, - ), - }, - HighlightedRange { - range: [65; 68), - tag: "text", - id: None, - }, - HighlightedRange { - range: [73; 75), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [76; 79), - tag: "variable", - id: Some( - 14077410872302487760, - ), - }, - HighlightedRange { - range: [80; 81), - tag: "type", - id: None, - }, - HighlightedRange { - range: [80; 81), - tag: "variable", - id: Some( - 8379786015941272633, - ), - }, - HighlightedRange { - range: [88; 89), - tag: "type", - id: None, - }, - HighlightedRange { - range: [96; 110), - tag: "macro", - id: None, - }, - HighlightedRange { - range: [117; 127), - tag: "comment", - id: None, - }, - HighlightedRange { - range: [128; 130), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [131; 135), - tag: "variable", - id: Some( - 5766414492220109266, - ), - }, - HighlightedRange { - range: [145; 153), - tag: "macro", - id: None, - }, - HighlightedRange { - range: [154; 166), - tag: "string", - id: None, - }, - HighlightedRange { - range: [168; 170), - tag: "literal", - id: None, - }, - HighlightedRange { - range: [178; 181), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [182; 185), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [186; 189), - tag: "macro", - id: None, - }, - HighlightedRange { - range: [197; 200), - tag: "macro", - id: None, - }, - HighlightedRange { - range: [192; 195), - tag: "text", - id: None, - }, - HighlightedRange { - range: [208; 211), - tag: "macro", - id: None, - }, - HighlightedRange { - range: [212; 216), - tag: "macro", - id: None, - }, - HighlightedRange { - range: [226; 227), - tag: "literal", - id: None, - }, - HighlightedRange { - range: [232; 233), - tag: "literal", - id: None, - }, - HighlightedRange { - range: [242; 248), - tag: "keyword.unsafe", - id: None, - }, - HighlightedRange { - range: [251; 254), - tag: "text", - id: None, - }, - HighlightedRange { - range: [255; 262), - tag: "text", - id: None, - }, - HighlightedRange { - range: [263; 264), - tag: "literal", - id: None, - }, - ], -) diff --git a/crates/ra_ide_api/src/snapshots/tests__rainbow_highlighting.snap b/crates/ra_ide_api/src/snapshots/tests__rainbow_highlighting.snap deleted file mode 100644 index 84cd521a24..0000000000 --- a/crates/ra_ide_api/src/snapshots/tests__rainbow_highlighting.snap +++ /dev/null @@ -1,128 +0,0 @@ ---- -created: "2019-05-25T11:21:56.117898Z" -creator: insta@0.8.1 -source: crates/ra_ide_api/src/syntax_highlighting.rs -expression: result ---- -Ok( - [ - HighlightedRange { - range: [1; 3), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [4; 8), - tag: "variable", - id: Some( - 5766414492220109266, - ), - }, - HighlightedRange { - range: [17; 20), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [21; 26), - tag: "variable", - id: Some( - 15975256018338854530, - ), - }, - HighlightedRange { - range: [29; 36), - tag: "string", - id: None, - }, - HighlightedRange { - range: [42; 45), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [46; 47), - tag: "variable", - id: Some( - 176272420896316891, - ), - }, - HighlightedRange { - range: [50; 55), - tag: "variable", - id: Some( - 15975256018338854530, - ), - }, - HighlightedRange { - range: [56; 65), - tag: "text", - id: None, - }, - HighlightedRange { - range: [73; 76), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [77; 78), - tag: "variable", - id: Some( - 15061637676198917049, - ), - }, - HighlightedRange { - range: [81; 86), - tag: "variable", - id: Some( - 15975256018338854530, - ), - }, - HighlightedRange { - range: [87; 96), - tag: "text", - id: None, - }, - HighlightedRange { - range: [105; 108), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [109; 110), - tag: "variable", - id: Some( - 1714508680417729339, - ), - }, - HighlightedRange { - range: [113; 134), - tag: "string", - id: None, - }, - HighlightedRange { - range: [140; 143), - tag: "keyword", - id: None, - }, - HighlightedRange { - range: [144; 145), - tag: "variable", - id: Some( - 15953336624848413466, - ), - }, - HighlightedRange { - range: [148; 149), - tag: "variable", - id: Some( - 1714508680417729339, - ), - }, - HighlightedRange { - range: [150; 159), - tag: "text", - id: None, - }, - ], -) diff --git a/crates/ra_ide_api/src/syntax_highlighting.rs b/crates/ra_ide_api/src/syntax_highlighting.rs index 407fcda4a7..8981c85e65 100644 --- a/crates/ra_ide_api/src/syntax_highlighting.rs +++ b/crates/ra_ide_api/src/syntax_highlighting.rs @@ -10,7 +10,7 @@ use crate::{FileId, db::RootDatabase}; pub struct HighlightedRange { pub range: TextRange, pub tag: &'static str, - pub id: Option, + pub binding_hash: Option, } fn is_control_keyword(kind: SyntaxKind) -> bool { @@ -30,15 +30,18 @@ fn is_control_keyword(kind: SyntaxKind) -> bool { pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec { let _p = profile("highlight"); - let source_file = db.parse(file_id); - fn hash(x: T) -> u64 { - use std::{collections::hash_map::DefaultHasher, hash::Hasher}; + fn calc_binding_hash(file_id: FileId, text: &SmolStr, shadow_count: u32) -> u64 { + fn hash(x: T) -> u64 { + use std::{collections::hash_map::DefaultHasher, hash::Hasher}; - let mut hasher = DefaultHasher::new(); - x.hash(&mut hasher); - hasher.finish() + let mut hasher = DefaultHasher::new(); + x.hash(&mut hasher); + hasher.finish() + } + + hash((file_id, text, shadow_count)) } // Visited nodes to handle highlighting priorities @@ -50,66 +53,92 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec ("comment", None), - STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => ("string", None), - ATTR => ("attribute", None), + let mut binding_hash = None; + let tag = match node.kind() { + COMMENT => "comment", + STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", + ATTR => "attribute", NAME_REF => { - if let Some(name_ref) = node.as_ast_node::() { + if let Some(name_ref) = node.as_node().and_then(ast::NameRef::cast) { use crate::name_ref_kind::{classify_name_ref, NameRefKind::*}; use hir::{ModuleDef, ImplItem}; // FIXME: try to reuse the SourceAnalyzers let analyzer = hir::SourceAnalyzer::new(db, file_id, name_ref.syntax(), None); match classify_name_ref(db, &analyzer, name_ref) { - Some(Method(_)) => ("function", None), - Some(Macro(_)) => ("macro", None), - Some(FieldAccess(_)) => ("field", None), - Some(AssocItem(ImplItem::Method(_))) => ("function", None), - Some(AssocItem(ImplItem::Const(_))) => ("constant", None), - Some(AssocItem(ImplItem::TypeAlias(_))) => ("type", None), - Some(Def(ModuleDef::Module(_))) => ("module", None), - Some(Def(ModuleDef::Function(_))) => ("function", None), - Some(Def(ModuleDef::Struct(_))) => ("type", None), - Some(Def(ModuleDef::Union(_))) => ("type", None), - Some(Def(ModuleDef::Enum(_))) => ("type", None), - Some(Def(ModuleDef::EnumVariant(_))) => ("constant", None), - Some(Def(ModuleDef::Const(_))) => ("constant", None), - Some(Def(ModuleDef::Static(_))) => ("constant", None), - Some(Def(ModuleDef::Trait(_))) => ("type", None), - Some(Def(ModuleDef::TypeAlias(_))) => ("type", None), - Some(SelfType(_)) => ("type", None), - Some(Pat(ptr)) => ("variable", Some(hash({ - let text = ptr.syntax_node_ptr().to_node(&source_file.syntax()).text().to_smol_string(); - let shadow_count = bindings_shadow_count.entry(text.clone()).or_default(); - (text, shadow_count) - }))), - Some(SelfParam(_)) => ("type", None), - Some(GenericParam(_)) => ("type", None), - None => ("text", None), + Some(Method(_)) => "function", + Some(Macro(_)) => "macro", + Some(FieldAccess(field)) => { + let (hir_file_id, src) = field.source(db); + if let hir::FieldSource::Named(name) = src { + let text = name.syntax().text().to_smol_string(); + let shadow_count = 0; // potentially even from different file + binding_hash = Some(calc_binding_hash(hir_file_id.original_file(db), &text, shadow_count)); + } + + "field" + }, + Some(AssocItem(ImplItem::Method(_))) => "function", + Some(AssocItem(ImplItem::Const(_))) => "constant", + Some(AssocItem(ImplItem::TypeAlias(_))) => "type", + Some(Def(ModuleDef::Module(_))) => "module", + Some(Def(ModuleDef::Function(_))) => "function", + Some(Def(ModuleDef::Struct(_))) => "type", + Some(Def(ModuleDef::Union(_))) => "type", + Some(Def(ModuleDef::Enum(_))) => "type", + Some(Def(ModuleDef::EnumVariant(_))) => "constant", + Some(Def(ModuleDef::Const(_))) => "constant", + Some(Def(ModuleDef::Static(_))) => "constant", + Some(Def(ModuleDef::Trait(_))) => "type", + Some(Def(ModuleDef::TypeAlias(_))) => "type", + Some(SelfType(_)) => "type", + Some(Pat(ptr)) => { + binding_hash = Some({ + let text = ptr.syntax_node_ptr().to_node(&source_file.syntax()).text().to_smol_string(); + let shadow_count = bindings_shadow_count.entry(text.clone()).or_default(); + calc_binding_hash(file_id, &text, *shadow_count) + }); + + "variable" + }, + Some(SelfParam(_)) => "type", + Some(GenericParam(_)) => "type", + None => "text", } } else { - ("text", None) + "text" } } NAME => { - if let Some(name) = node.as_ast_node::() { - ("variable", Some(hash({ - let text = name.syntax().text().to_smol_string(); - let shadow_count = bindings_shadow_count.entry(text.clone()).or_insert(1); - *shadow_count += 1; - (text, shadow_count) - }))) + if let Some(name) = node.as_node().and_then(ast::Name::cast) { + if name.syntax().ancestors().any(|x| ast::BindPat::cast(x).is_some()) { + binding_hash = Some({ + let text = name.syntax().text().to_smol_string(); + let shadow_count = bindings_shadow_count.entry(text.clone()).or_insert(0); + *shadow_count += 1; + calc_binding_hash(file_id, &text, *shadow_count) + }); + "variable" + } else if name.syntax().ancestors().any(|x| ast::NamedFieldDef::cast(x).is_some()) { + binding_hash = Some({ + let text = name.syntax().text().to_smol_string(); + let shadow_count = 0; + calc_binding_hash(file_id, &text, shadow_count) + }); + "variable" + } else { + "function" + } } else { - ("text", None) + "text" } } - TYPE_ALIAS_DEF | TYPE_ARG | TYPE_PARAM => ("type", None), - INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => ("literal", None), - LIFETIME => ("parameter", None), - T![unsafe] => ("keyword.unsafe", None), - k if is_control_keyword(k) => ("keyword.control", None), - k if k.is_keyword() => ("keyword", None), + TYPE_ALIAS_DEF | TYPE_ARG | TYPE_PARAM => "type", + INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", + LIFETIME => "parameter", + T![unsafe] => "keyword.unsafe", + k if is_control_keyword(k) => "keyword.control", + k if k.is_keyword() => "keyword", _ => { // let analyzer = hir::SourceAnalyzer::new(db, file_id, name_ref.syntax(), None); if let Some(macro_call) = node.as_node().and_then(ast::MacroCall::cast) { @@ -128,7 +157,7 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec Vec String { +pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { let source_file = db.parse(file_id); + fn rainbowify(seed: u64) -> String { + use rand::prelude::*; + let mut rng = SmallRng::seed_from_u64(seed); + format!("hsl({h},{s}%,{l}%)", + h = rng.gen_range::(0, 361), + s = rng.gen_range::(42, 99), + l = rng.gen_range::(40, 91), + ) + } + let mut ranges = highlight(db, file_id); ranges.sort_by_key(|it| it.range.start()); // quick non-optimal heuristic to intersect token ranges and highlighted ranges @@ -166,16 +205,20 @@ pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId) -> String { } } let text = html_escape(&token.text()); - let classes = could_intersect + let ranges = could_intersect .iter() .filter(|it| token.range().is_subrange(&it.range)) - .map(|it| it.tag) .collect::>(); - if classes.is_empty() { + if ranges.is_empty() { buf.push_str(&text); } else { - let classes = classes.join(" "); - buf.push_str(&format!("{}", classes, text)); + let classes = ranges.iter().map(|x| x.tag).collect::>().join(" "); + let binding_hash = ranges.first().and_then(|x| x.binding_hash); + let color = match (rainbow, binding_hash) { + (true, Some(hash)) => format!(" data-binding-hash=\"{}\" style=\"color: {};\"", hash, rainbowify(hash)), + _ => "".into() + }; + buf.push_str(&format!("{}", classes, color, text)); } } buf.push_str(""); @@ -189,11 +232,8 @@ fn html_escape(text: &str) -> String { const STYLE: &str = " "; @@ -241,12 +280,12 @@ fn main() { } unsafe { vec.set_len(0); } } -"#, +"#.trim(), ); let dst_file = project_dir().join("crates/ra_ide_api/src/snapshots/highlighting.html"); let actual_html = &analysis.highlight_as_html(file_id).unwrap(); let expected_html = &read_text(&dst_file); - // std::fs::write(dst_file, &actual_html).unwrap(); + std::fs::write(dst_file, &actual_html).unwrap(); assert_eq_text!(expected_html, actual_html); } @@ -261,9 +300,13 @@ fn main() { let x = "other color please!"; let y = x.to_string(); -}"#, +} +"#.trim(), ); - let result = analysis.highlight(file_id); - assert_debug_snapshot_matches!("rainbow_highlighting", result); + let dst_file = project_dir().join("crates/ra_ide_api/src/snapshots/rainbow_highlighting.html"); + let actual_html = &analysis.highlight_as_html(file_id).unwrap(); + let expected_html = &read_text(&dst_file); + std::fs::write(dst_file, &actual_html).unwrap(); + assert_eq_text!(expected_html, actual_html); } } diff --git a/crates/ra_syntax/src/syntax_node.rs b/crates/ra_syntax/src/syntax_node.rs index 89f92e0b7b..80054f529a 100644 --- a/crates/ra_syntax/src/syntax_node.rs +++ b/crates/ra_syntax/src/syntax_node.rs @@ -523,10 +523,6 @@ impl<'a> SyntaxElement<'a> { } } - pub fn as_ast_node(&self) -> Option<&T> { - self.as_node().and_then(|x| ::cast(x)) - } - pub fn as_token(&self) -> Option> { match self { SyntaxElement::Node(_) => None,