From 5bf3e949e8470a138a61c806769e1a329761cab6 Mon Sep 17 00:00:00 2001 From: Pascal Hertleif Date: Thu, 23 May 2019 19:42:42 +0200 Subject: 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? --- .../src/snapshots/tests__highlighting.snap | 192 +++++++++++++++++++++ .../src/snapshots/tests__sematic_highlighting.snap | 87 ++++++++++ crates/ra_ide_api/src/syntax_highlighting.rs | 101 +++++++---- crates/ra_lsp_server/src/main_loop/handlers.rs | 6 +- crates/ra_lsp_server/src/req.rs | 1 + crates/ra_syntax/src/syntax_node.rs | 4 + editors/code/package-lock.json | 10 ++ editors/code/package.json | 2 + editors/code/src/highlighting.ts | 45 ++++- 9 files changed, 409 insertions(+), 39 deletions(-) create mode 100644 crates/ra_ide_api/src/snapshots/tests__highlighting.snap create mode 100644 crates/ra_ide_api/src/snapshots/tests__sematic_highlighting.snap diff --git a/crates/ra_ide_api/src/snapshots/tests__highlighting.snap b/crates/ra_ide_api/src/snapshots/tests__highlighting.snap new file mode 100644 index 000000000..208681f10 --- /dev/null +++ b/crates/ra_ide_api/src/snapshots/tests__highlighting.snap @@ -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, + }, + ], +) diff --git a/crates/ra_ide_api/src/snapshots/tests__sematic_highlighting.snap b/crates/ra_ide_api/src/snapshots/tests__sematic_highlighting.snap new file mode 100644 index 000000000..3b3fe32e9 --- /dev/null +++ b/crates/ra_ide_api/src/snapshots/tests__sematic_highlighting.snap @@ -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, + }, + ], +) diff --git a/crates/ra_ide_api/src/syntax_highlighting.rs b/crates/ra_ide_api/src/syntax_highlighting.rs index 87e053364..da000c0c3 100644 --- a/crates/ra_ide_api/src/syntax_highlighting.rs +++ b/crates/ra_ide_api/src/syntax_highlighting.rs @@ -10,6 +10,7 @@ use crate::{FileId, db::RootDatabase}; pub struct HighlightedRange { pub range: TextRange, pub tag: &'static str, + pub id: Option, } fn is_control_keyword(kind: SyntaxKind) -> bool { @@ -32,6 +33,14 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec(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 = FxHashSet::default(); let mut res = Vec::new(); @@ -39,52 +48,59 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec "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::() { 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::() { + ("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 Vec Result> { .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) } diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs index 6090eb7b9..cea0e6ce7 100644 --- a/crates/ra_lsp_server/src/req.rs +++ b/crates/ra_lsp_server/src/req.rs @@ -129,6 +129,7 @@ pub struct PublishDecorationsParams { pub struct Decoration { pub range: Range, pub tag: &'static str, + pub id: Option, } pub enum ParentModule {} diff --git a/crates/ra_syntax/src/syntax_node.rs b/crates/ra_syntax/src/syntax_node.rs index 80054f529..89f92e0b7 100644 --- a/crates/ra_syntax/src/syntax_node.rs +++ b/crates/ra_syntax/src/syntax_node.rs @@ -523,6 +523,10 @@ 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, diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 29cd260a4..6b3a12f91 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -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", diff --git a/editors/code/package.json b/editors/code/package.json index cde5fbcb8..d8ba914f5 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -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", diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts index 8389d94b8..4597db08f 100644 --- a/editors/code/src/highlighting.ts +++ b/editors/code/src/highlighting.ts @@ -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 = new Map(); + const colorfulIdents: Map = 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); + } } } -- cgit v1.2.3