Semantic highlighting spike

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?
This commit is contained in:
Pascal Hertleif 2019-05-23 19:42:42 +02:00
parent 4b48cff022
commit 5bf3e949e8
No known key found for this signature in database
GPG key ID: EDBB1A8D2047A074
9 changed files with 409 additions and 39 deletions

View file

@ -0,0 +1,192 @@
---
created: "2019-05-25T10:53:54.439877Z"
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(
461893210254723387,
),
},
HighlightedRange {
range: [42; 45),
tag: "keyword",
id: None,
},
HighlightedRange {
range: [46; 47),
tag: "variable",
id: Some(
8312289520117458465,
),
},
HighlightedRange {
range: [49; 52),
tag: "text",
id: None,
},
HighlightedRange {
range: [58; 61),
tag: "keyword",
id: None,
},
HighlightedRange {
range: [62; 63),
tag: "variable",
id: Some(
4497542318236667727,
),
},
HighlightedRange {
range: [65; 68),
tag: "text",
id: None,
},
HighlightedRange {
range: [73; 75),
tag: "keyword",
id: None,
},
HighlightedRange {
range: [76; 79),
tag: "variable",
id: Some(
4506850079084802999,
),
},
HighlightedRange {
range: [80; 81),
tag: "type",
id: None,
},
HighlightedRange {
range: [80; 81),
tag: "variable",
id: Some(
16968185728268100018,
),
},
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(
14467718814232352107,
),
},
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,
},
],
)

View file

@ -0,0 +1,87 @@
---
created: "2019-05-25T10:25:13.898113Z"
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(
17119830160611610240,
),
},
HighlightedRange {
range: [17; 20),
tag: "keyword",
id: None,
},
HighlightedRange {
range: [21; 26),
tag: "variable",
id: Some(
2744494144922727377,
),
},
HighlightedRange {
range: [29; 36),
tag: "string",
id: None,
},
HighlightedRange {
range: [42; 45),
tag: "keyword",
id: None,
},
HighlightedRange {
range: [46; 47),
tag: "variable",
id: Some(
10375904121795371996,
),
},
HighlightedRange {
range: [50; 55),
tag: "variable",
id: Some(
2744494144922727377,
),
},
HighlightedRange {
range: [56; 65),
tag: "text",
id: None,
},
HighlightedRange {
range: [73; 76),
tag: "keyword",
id: None,
},
HighlightedRange {
range: [77; 78),
tag: "variable",
id: Some(
8228548264153724449,
),
},
HighlightedRange {
range: [81; 86),
tag: "variable",
id: Some(
2744494144922727377,
),
},
HighlightedRange {
range: [87; 96),
tag: "text",
id: None,
},
],
)

View file

@ -10,6 +10,7 @@ use crate::{FileId, db::RootDatabase};
pub struct HighlightedRange {
pub range: TextRange,
pub tag: &'static str,
pub id: Option<u64>,
}
fn is_control_keyword(kind: SyntaxKind) -> bool {
@ -32,6 +33,14 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa
let source_file = db.parse(file_id);
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()
}
// Visited nodes to handle highlighting priorities
let mut highlighted: FxHashSet<SyntaxElement> = FxHashSet::default();
let mut res = Vec::new();
@ -39,52 +48,59 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa
if highlighted.contains(&node) {
continue;
}
let tag = match node.kind() {
COMMENT => "comment",
STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string",
ATTR => "attribute",
let (tag, id) = match node.kind() {
COMMENT => ("comment", None),
STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => ("string", None),
ATTR => ("attribute", None),
NAME_REF => {
if let Some(name_ref) = node.as_node().and_then(|n| ast::NameRef::cast(n)) {
if let Some(name_ref) = node.as_ast_node::<ast::NameRef>() {
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",
Some(Macro(_)) => "macro",
Some(FieldAccess(_)) => "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(_)) => "text",
Some(SelfParam(_)) => "type",
Some(GenericParam(_)) => "type",
None => "text",
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(ptr.syntax_node_ptr().range()))),
Some(SelfParam(_)) => ("type", None),
Some(GenericParam(_)) => ("type", None),
None => ("text", None),
}
} else {
"text"
("text", None)
}
}
NAME => "function",
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",
NAME => {
if let Some(name) = node.as_ast_node::<ast::Name>() {
("variable", Some(hash(name.syntax().range())))
} else {
("text", None)
}
}
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),
_ => {
// 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 +117,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",
id: None,
})
}
}
@ -109,7 +126,7 @@ 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, id })
}
res
}
@ -221,4 +238,18 @@ fn main() {
// std::fs::write(dst_file, &actual_html).unwrap();
assert_eq_text!(expected_html, actual_html);
}
#[test]
fn test_sematic_highlighting() {
let (analysis, file_id) = single_file(
r#"
fn main() {
let hello = "hello";
let x = hello.to_string();
let y = hello.to_string();
}"#,
);
let result = analysis.highlight(file_id);
assert_debug_snapshot_matches!("sematic_highlighting", result);
}
}

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,
id: h.id.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 id: Option<String>,
}
pub enum ParentModule {}

View file

@ -523,6 +523,10 @@ impl<'a> SyntaxElement<'a> {
}
}
pub fn as_ast_node<T: AstNode>(&self) -> Option<&T> {
self.as_node().and_then(|x| <T as AstNode>::cast(x))
}
pub fn as_token(&self) -> Option<SyntaxToken<'a>> {
match self {
SyntaxElement::Node(_) => None,

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

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;
id?: 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,8 @@ export class Highlighter {
}
const byTag: Map<string, vscode.Range[]> = new Map();
const colorfulIdents: Map<string, vscode.Range[]> = new Map();
for (const tag of this.decorations.keys()) {
byTag.set(tag, []);
}
@ -84,9 +101,23 @@ export class Highlighter {
if (!byTag.get(d.tag)) {
continue;
}
byTag
.get(d.tag)!
.push(Server.client.protocol2CodeConverter.asRange(d.range));
if (d.id) {
if (!colorfulIdents.has(d.id)) {
colorfulIdents.set(d.id, []);
}
colorfulIdents
.get(d.id)!
.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 +127,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);
}
}
}