1319: Rainbow highlighting spike 🌈 r=killercup a=killercup

Very simple approach: For each identifier, set the hash of the range
where it's defined as its 'id' and use it in the VSCode extension to
generate unique colors.

Thus, the generated colors are per-file. They are also quite fragile,
and I'm not entirely sure why. Looks like we need to make sure the
same ranges aren't overwritten by a later request?

Co-authored-by: Pascal Hertleif <pascal@technocreatives.com>
This commit is contained in:
bors[bot] 2019-05-27 09:56:06 +00:00
commit 0d1c607607
15 changed files with 242 additions and 46 deletions

1
Cargo.lock generated
View file

@ -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)",

View file

@ -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)) => {

View file

@ -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 }

View file

@ -463,8 +463,8 @@ impl Analysis {
}
/// Computes syntax highlighting for the given file.
pub fn highlight_as_html(&self, file_id: FileId) -> Cancelable<String> {
self.with_db(|db| syntax_highlighting::highlight_as_html(db, file_id))
pub fn highlight_as_html(&self, file_id: FileId, rainbow: bool) -> Cancelable<String> {
self.with_db(|db| syntax_highlighting::highlight_as_html(db, file_id, rainbow))
}
/// Computes completions at the given position.
@ -472,7 +472,7 @@ impl Analysis {
self.with_db(|db| completion::completions(db, position).map(Into::into))
}
/// Computes assists (aks code actons aka intentions) for the given
/// Computes assists (aka code actions aka intentions) for the given
/// position.
pub fn assists(&self, frange: FileRange) -> Cancelable<Vec<Assist>> {
self.with_db(|db| assists::assists(db, frange))

View file

@ -1,10 +1,7 @@
<style>
pre {
color: #DCDCCC;
background-color: #3F3F3F;
font-size: 22px;
}
body { margin: 0; }
pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; }
.comment { color: #7F9F7F; }
.string { color: #CC9393; }
@ -19,10 +16,8 @@ pre {
.keyword { color: #F0DFAF; }
.keyword\.unsafe { color: #F0DFAF; font-weight: bold; }
.keyword\.control { color: #DC8CC3; }
</style>
<pre><code>
<span class="attribute">#</span><span class="attribute">[</span><span class="attribute">derive</span><span class="attribute">(</span><span class="attribute">Clone</span><span class="attribute">,</span><span class="attribute"> </span><span class="attribute">Debug</span><span class="attribute">)</span><span class="attribute">]</span>
<pre><code><span class="attribute">#</span><span class="attribute">[</span><span class="attribute">derive</span><span class="attribute">(</span><span class="attribute">Clone</span><span class="attribute">,</span><span class="attribute"> </span><span class="attribute">Debug</span><span class="attribute">)</span><span class="attribute">]</span>
<span class="keyword">struct</span> <span class="function">Foo</span> {
<span class="keyword">pub</span> <span class="function">x</span>: <span class="text">i32</span>,
<span class="keyword">pub</span> <span class="function">y</span>: <span class="text">i32</span>,
@ -36,10 +31,9 @@ pre {
<span class="keyword">fn</span> <span class="function">main</span>() {
<span class="macro">println</span><span class="macro">!</span>(<span class="string">"Hello, {}!"</span>, <span class="literal">92</span>);
<span class="keyword">let</span> <span class="keyword">mut</span> <span class="function">vec</span> = <span class="text">Vec</span>::<span class="text">new</span>();
<span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable" data-binding-hash="9636295041291189729" style="color: hsl(51,57%,74%);">vec</span> = <span class="text">Vec</span>::<span class="text">new</span>();
<span class="keyword.control">if</span> <span class="keyword">true</span> {
<span class="text">vec</span>.<span class="text">push</span>(<span class="type">Foo</span> { <span class="field">x</span>: <span class="literal">0</span>, <span class="field">y</span>: <span class="literal">1</span> });
<span class="variable" data-binding-hash="8496027264380925433" style="color: hsl(18,48%,55%);">vec</span>.<span class="text">push</span>(<span class="type">Foo</span> { <span class="field">x</span>: <span class="literal">0</span>, <span class="field">y</span>: <span class="literal">1</span> });
}
<span class="keyword.unsafe">unsafe</span> { <span class="text">vec</span>.<span class="text">set_len</span>(<span class="literal">0</span>); }
}
</code></pre>
<span class="keyword.unsafe">unsafe</span> { <span class="variable" data-binding-hash="8496027264380925433" style="color: hsl(18,48%,55%);">vec</span>.<span class="text">set_len</span>(<span class="literal">0</span>); }
}</code></pre>

View file

@ -0,0 +1,27 @@
<style>
body { margin: 0; }
pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; }
.comment { color: #7F9F7F; }
.string { color: #CC9393; }
.function { color: #93E0E3; }
.parameter { color: #94BFF3; }
.builtin { color: #DD6718; }
.text { color: #DCDCCC; }
.attribute { color: #BFEBBF; }
.literal { color: #DFAF8F; }
.macro { color: #DFAF8F; }
.keyword { color: #F0DFAF; }
.keyword\.unsafe { color: #F0DFAF; font-weight: bold; }
.keyword\.control { color: #DC8CC3; }
</style>
<pre><code><span class="keyword">fn</span> <span class="function">main</span>() {
<span class="keyword">let</span> <span class="variable" data-binding-hash="3888301305669440875" style="color: hsl(242,59%,59%);">hello</span> = <span class="string">"hello"</span>;
<span class="keyword">let</span> <span class="variable" data-binding-hash="5695551762718493399" style="color: hsl(272,48%,45%);">x</span> = <span class="variable" data-binding-hash="3888301305669440875" style="color: hsl(242,59%,59%);">hello</span>.<span class="text">to_string</span>();
<span class="keyword">let</span> <span class="variable" data-binding-hash="5435401749617022797" style="color: hsl(353,77%,74%);">y</span> = <span class="variable" data-binding-hash="3888301305669440875" style="color: hsl(242,59%,59%);">hello</span>.<span class="text">to_string</span>();
<span class="keyword">let</span> <span class="variable" data-binding-hash="1903207544374197704" style="color: hsl(58,61%,61%);">x</span> = <span class="string">"other color please!"</span>;
<span class="keyword">let</span> <span class="variable" data-binding-hash="14878783531007968800" style="color: hsl(265,73%,83%);">y</span> = <span class="variable" data-binding-hash="1903207544374197704" style="color: hsl(58,61%,61%);">x</span>.<span class="text">to_string</span>();
}</code></pre>

View file

@ -1,6 +1,6 @@
use rustc_hash::FxHashSet;
use rustc_hash::{FxHashSet, FxHashMap};
use ra_syntax::{ast, AstNode, TextRange, Direction, SyntaxKind, SyntaxKind::*, SyntaxElement, T};
use ra_syntax::{ast, AstNode, TextRange, Direction, SmolStr, SyntaxKind, SyntaxKind::*, SyntaxElement, T};
use ra_db::SourceDatabase;
use ra_prof::profile;
@ -10,6 +10,7 @@ use crate::{FileId, db::RootDatabase};
pub struct HighlightedRange {
pub range: TextRange,
pub tag: &'static str,
pub binding_hash: Option<u64>,
}
fn is_control_keyword(kind: SyntaxKind) -> bool {
@ -29,22 +30,36 @@ fn is_control_keyword(kind: SyntaxKind) -> bool {
pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> {
let _p = profile("highlight");
let source_file = db.parse(file_id);
fn calc_binding_hash(file_id: FileId, text: &SmolStr, shadow_count: u32) -> u64 {
fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 {
use std::{collections::hash_map::DefaultHasher, hash::Hasher};
let mut hasher = DefaultHasher::new();
x.hash(&mut hasher);
hasher.finish()
}
hash((file_id, text, shadow_count))
}
// Visited nodes to handle highlighting priorities
let mut highlighted: FxHashSet<SyntaxElement> = FxHashSet::default();
let mut bindings_shadow_count: FxHashMap<SmolStr, u32> = FxHashMap::default();
let mut res = Vec::new();
for node in source_file.syntax().descendants_with_tokens() {
if highlighted.contains(&node) {
continue;
}
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_node().and_then(|n| ast::NameRef::cast(n)) {
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};
@ -68,7 +83,20 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa
Some(Def(ModuleDef::Trait(_))) => "type",
Some(Def(ModuleDef::TypeAlias(_))) => "type",
Some(SelfType(_)) => "type",
Some(Pat(_)) => "text",
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",
@ -77,7 +105,24 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa
"text"
}
}
NAME => "function",
NAME => {
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 {
"function"
}
} else {
"text"
}
}
TYPE_ALIAS_DEF | TYPE_ARG | TYPE_PARAM => "type",
INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal",
LIFETIME => "parameter",
@ -85,6 +130,7 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa
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) {
if let Some(path) = macro_call.path() {
if let Some(segment) = path.segment() {
@ -101,6 +147,7 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa
res.push(HighlightedRange {
range: TextRange::from_to(range_start, range_end),
tag: "macro",
binding_hash: None,
})
}
}
@ -109,14 +156,25 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa
continue;
}
};
res.push(HighlightedRange { range: node.range(), tag })
res.push(HighlightedRange { range: node.range(), tag, binding_hash })
}
res
}
pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId) -> 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::<u16, _, _>(0, 361),
s = rng.gen_range::<u16, _, _>(42, 99),
l = rng.gen_range::<u16, _, _>(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
@ -138,16 +196,24 @@ 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::<Vec<_>>();
if classes.is_empty() {
if ranges.is_empty() {
buf.push_str(&text);
} else {
let classes = classes.join(" ");
buf.push_str(&format!("<span class=\"{}\">{}</span>", classes, text));
let classes = ranges.iter().map(|x| x.tag).collect::<Vec<_>>().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!("<span class=\"{}\"{}>{}</span>", classes, color, text));
}
}
buf.push_str("</code></pre>");
@ -161,11 +227,8 @@ fn html_escape(text: &str) -> String {
const STYLE: &str = "
<style>
pre {
color: #DCDCCC;
background-color: #3F3F3F;
font-size: 22px;
}
body { margin: 0; }
pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; }
.comment { color: #7F9F7F; }
.string { color: #CC9393; }
@ -180,7 +243,6 @@ pre {
.keyword { color: #F0DFAF; }
.keyword\\.unsafe { color: #F0DFAF; font-weight: bold; }
.keyword\\.control { color: #DC8CC3; }
</style>
";
@ -213,12 +275,36 @@ 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 actual_html = &analysis.highlight_as_html(file_id, true).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);
}
#[test]
fn test_rainbow_highlighting() {
let (analysis, file_id) = single_file(
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();
}
"#
.trim(),
);
let dst_file =
project_dir().join("crates/ra_ide_api/src/snapshots/rainbow_highlighting.html");
let actual_html = &analysis.highlight_as_html(file_id, true).unwrap();
let expected_html = &read_text(&dst_file);
std::fs::write(dst_file, &actual_html).unwrap();
assert_eq_text!(expected_html, actual_html);
}
}

View file

@ -872,7 +872,11 @@ fn highlight(world: &ServerWorld, file_id: FileId) -> Result<Vec<Decoration>> {
.analysis()
.highlight(file_id)?
.into_iter()
.map(|h| Decoration { range: h.range.conv_with(&line_index), tag: h.tag })
.map(|h| Decoration {
range: h.range.conv_with(&line_index),
tag: h.tag,
binding_hash: h.binding_hash.map(|x| x.to_string()),
})
.collect();
Ok(res)
}

View file

@ -129,6 +129,7 @@ pub struct PublishDecorationsParams {
pub struct Decoration {
pub range: Range,
pub tag: &'static str,
pub binding_hash: Option<String>,
}
pub enum ParentModule {}

View file

@ -1,6 +1,6 @@
use std::{fmt, ops::{self, Bound}};
use crate::{SyntaxNode, TextRange, TextUnit, SyntaxElement};
use crate::{SmolStr, SyntaxNode, TextRange, TextUnit, SyntaxElement};
#[derive(Clone)]
pub struct SyntaxText<'a> {
@ -34,6 +34,12 @@ impl<'a> SyntaxText<'a> {
self.chunks().collect()
}
pub fn to_smol_string(&self) -> SmolStr {
// FIXME: use `self.chunks().collect()` here too once
// https://github.com/matklad/smol_str/pull/12 is merged and published
self.to_string().into()
}
pub fn contains(&self, c: char) -> bool {
self.chunks().any(|it| it.contains(c))
}

View file

@ -470,3 +470,12 @@ There also snippet completions:
- `tfn` -> `#[test] fn f(){}`
### Code highlighting
Experimental feature to let rust-analyzer highlight Rust code instead of using the
default highlighter.
#### Rainbow highlighting
Experimental feature that, given code highlighting using rust-analyzer is
active, will pick unique colors for identifiers.

View file

@ -36,6 +36,11 @@
"integrity": "sha512-Ja7d4s0qyGFxjGeDq5S7Si25OFibSAHUi6i17UWnwNnpitADN7hah9q0Tl25gxuV5R1u2Bx+np6w4LHXfHyj/g==",
"dev": true
},
"@types/seedrandom": {
"version": "2.4.28",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.28.tgz",
"integrity": "sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA=="
},
"agent-base": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
@ -984,6 +989,11 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"seedrandom": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.1.tgz",
"integrity": "sha512-1/02Y/rUeU1CJBAGLebiC5Lbo5FnB22gQbIFFYTLkwvp1xdABZJH1sn4ZT1MzXmPpzv+Rf/Lu2NcsLJiK4rcDg=="
},
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",

View file

@ -31,11 +31,13 @@
"singleQuote": true
},
"dependencies": {
"seedrandom": "^3.0.1",
"vscode-languageclient": "^5.3.0-next.4"
},
"devDependencies": {
"@types/mocha": "^5.2.6",
"@types/node": "^10.14.5",
"@types/seedrandom": "^2.4.28",
"prettier": "^1.17.0",
"shx": "^0.3.1",
"tslint": "^5.16.0",
@ -162,6 +164,11 @@
"default": false,
"description": "Highlight Rust code (overrides built-in syntax highlighting)"
},
"rust-analyzer.rainbowHighlightingOn": {
"type": "boolean",
"default": false,
"description": "When highlighting Rust code, use a unique color per identifier"
},
"rust-analyzer.showWorkspaceLoadedNotification": {
"type": "boolean",
"default": true,

View file

@ -15,6 +15,7 @@ export interface CargoWatchOptions {
export class Config {
public highlightingOn = true;
public rainbowHighlightingOn = false;
public enableEnhancedTyping = true;
public raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
public showWorkspaceLoadedNotification = true;
@ -39,6 +40,12 @@ export class Config {
this.highlightingOn = config.get('highlightingOn') as boolean;
}
if (config.has('rainbowHighlightingOn')) {
this.rainbowHighlightingOn = config.get(
'rainbowHighlightingOn'
) as boolean;
}
if (config.has('showWorkspaceLoadedNotification')) {
this.showWorkspaceLoadedNotification = config.get(
'showWorkspaceLoadedNotification'

View file

@ -1,3 +1,4 @@
import seedrandom = require('seedrandom');
import * as vscode from 'vscode';
import * as lc from 'vscode-languageclient';
@ -6,6 +7,20 @@ import { Server } from './server';
export interface Decoration {
range: lc.Range;
tag: string;
bindingHash?: string;
}
// Based on this HSL-based color generator: https://gist.github.com/bendc/76c48ce53299e6078a76
function fancify(seed: string, shade: 'light' | 'dark') {
const random = seedrandom(seed);
const randomInt = (min: number, max: number) => {
return Math.floor(random() * (max - min + 1)) + min;
};
const h = randomInt(0, 360);
const s = randomInt(42, 98);
const l = shade === 'light' ? randomInt(15, 40) : randomInt(40, 90);
return `hsl(${h},${s}%,${l}%)`;
}
export class Highlighter {
@ -76,6 +91,9 @@ export class Highlighter {
}
const byTag: Map<string, vscode.Range[]> = new Map();
const colorfulIdents: Map<string, vscode.Range[]> = new Map();
const rainbowTime = Server.config.rainbowHighlightingOn;
for (const tag of this.decorations.keys()) {
byTag.set(tag, []);
}
@ -84,9 +102,23 @@ export class Highlighter {
if (!byTag.get(d.tag)) {
continue;
}
byTag
.get(d.tag)!
.push(Server.client.protocol2CodeConverter.asRange(d.range));
if (rainbowTime && d.bindingHash) {
if (!colorfulIdents.has(d.bindingHash)) {
colorfulIdents.set(d.bindingHash, []);
}
colorfulIdents
.get(d.bindingHash)!
.push(
Server.client.protocol2CodeConverter.asRange(d.range)
);
} else {
byTag
.get(d.tag)!
.push(
Server.client.protocol2CodeConverter.asRange(d.range)
);
}
}
for (const tag of byTag.keys()) {
@ -96,5 +128,13 @@ export class Highlighter {
const ranges = byTag.get(tag)!;
editor.setDecorations(dec, ranges);
}
for (const [hash, ranges] of colorfulIdents.entries()) {
const dec = vscode.window.createTextEditorDecorationType({
light: { color: fancify(hash, 'light') },
dark: { color: fancify(hash, 'dark') }
});
editor.setDecorations(dec, ranges);
}
}
}