diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 21872c81d1..3879da6d03 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -46,6 +46,7 @@ mod references; mod rename; mod runnables; mod ssr; +mod static_index; mod status; mod syntax_highlighting; mod syntax_tree; @@ -86,6 +87,7 @@ pub use crate::{ references::ReferenceSearchResult, rename::RenameError, runnables::{Runnable, RunnableKind, TestId}, + static_index::{StaticIndex, StaticIndexedFile}, syntax_highlighting::{ tags::{Highlight, HlMod, HlMods, HlOperator, HlPunct, HlTag}, HlRange, diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs new file mode 100644 index 0000000000..1b384853be --- /dev/null +++ b/crates/ide/src/static_index.rs @@ -0,0 +1,60 @@ +use hir::{db::HirDatabase, Crate, Module}; +use ide_db::base_db::{FileId, SourceDatabaseExt}; +use ide_db::RootDatabase; +use rustc_hash::FxHashSet; + +use crate::{Analysis, Cancellable, Fold}; + +/// A static representation of fully analyzed source code. +/// +/// The intended use-case is powering read-only code browsers and emitting LSIF +pub struct StaticIndex { + pub files: Vec, +} + +pub struct StaticIndexedFile { + pub file_id: FileId, + pub folds: Vec, +} + +fn all_modules(db: &dyn HirDatabase) -> Vec { + let mut worklist: Vec<_> = + Crate::all(db).into_iter().map(|krate| krate.root_module(db)).collect(); + let mut modules = Vec::new(); + + while let Some(module) = worklist.pop() { + modules.push(module); + worklist.extend(module.children(db)); + } + + modules +} + +impl StaticIndex { + pub fn compute(db: &RootDatabase, analysis: &Analysis) -> Cancellable { + let work = all_modules(db).into_iter().filter(|module| { + let file_id = module.definition_source(db).file_id.original_file(db); + let source_root = db.file_source_root(file_id); + let source_root = db.source_root(source_root); + !source_root.is_library + }); + + let mut visited_files = FxHashSet::default(); + let mut result_files = Vec::::new(); + for module in work { + let file_id = module.definition_source(db).file_id.original_file(db); + if !visited_files.contains(&file_id) { + //let path = vfs.file_path(file_id); + //let path = path.as_path().unwrap(); + //let doc_id = lsif.add(Element::Vertex(Vertex::Document(Document { + // language_id: Language::Rust, + // uri: lsp_types::Url::from_file_path(path).unwrap(), + //}))); + let folds = analysis.folding_ranges(file_id)?; + result_files.push(StaticIndexedFile { file_id, folds }); + visited_files.insert(file_id); + } + } + Ok(StaticIndex { files: result_files }) + } +} diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index 2390bee824..74c041020b 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -87,6 +87,7 @@ fn try_main() -> Result<()> { flags::RustAnalyzerCmd::Diagnostics(cmd) => cmd.run()?, flags::RustAnalyzerCmd::Ssr(cmd) => cmd.run()?, flags::RustAnalyzerCmd::Search(cmd) => cmd.run()?, + flags::RustAnalyzerCmd::Lsif(cmd) => cmd.run()?, } Ok(()) } diff --git a/crates/rust-analyzer/src/cli.rs b/crates/rust-analyzer/src/cli.rs index efd8a2aa9f..6ccdaa86dd 100644 --- a/crates/rust-analyzer/src/cli.rs +++ b/crates/rust-analyzer/src/cli.rs @@ -8,6 +8,7 @@ mod highlight; mod analysis_stats; mod diagnostics; mod ssr; +mod lsif; mod progress_report; diff --git a/crates/rust-analyzer/src/cli/analysis_stats.rs b/crates/rust-analyzer/src/cli/analysis_stats.rs index a2dc842e75..55a542c3c1 100644 --- a/crates/rust-analyzer/src/cli/analysis_stats.rs +++ b/crates/rust-analyzer/src/cli/analysis_stats.rs @@ -367,8 +367,6 @@ fn expr_syntax_range( ) -> Option<(VfsPath, LineCol, LineCol)> { let src = sm.expr_syntax(expr_id); if let Ok(src) = src { - // FIXME: it might be nice to have a function (on Analysis?) that goes from Source -> (LineCol, LineCol) directly - // But also, we should just turn the type mismatches into diagnostics and provide these let root = db.parse_or_expand(src.file_id).unwrap(); let node = src.map(|e| e.to_node(&root).syntax().clone()); let original_range = node.as_ref().original_file_range(db); diff --git a/crates/rust-analyzer/src/cli/flags.rs b/crates/rust-analyzer/src/cli/flags.rs index e2e250143c..b759d912c9 100644 --- a/crates/rust-analyzer/src/cli/flags.rs +++ b/crates/rust-analyzer/src/cli/flags.rs @@ -102,6 +102,10 @@ xflags::xflags! { } cmd proc-macro {} + + cmd lsif + required path: PathBuf + {} } } @@ -129,6 +133,7 @@ pub enum RustAnalyzerCmd { Ssr(Ssr), Search(Search), ProcMacro(ProcMacro), + Lsif(Lsif), } #[derive(Debug)] @@ -190,6 +195,11 @@ pub struct Search { #[derive(Debug)] pub struct ProcMacro; +#[derive(Debug)] +pub struct Lsif { + pub path: PathBuf, +} + impl RustAnalyzer { pub const HELP: &'static str = Self::HELP_; diff --git a/crates/rust-analyzer/src/cli/lsif.rs b/crates/rust-analyzer/src/cli/lsif.rs new file mode 100644 index 0000000000..fbd5b642a6 --- /dev/null +++ b/crates/rust-analyzer/src/cli/lsif.rs @@ -0,0 +1,116 @@ +//! Lsif generator + +use std::env; + +use ide::{StaticIndex, StaticIndexedFile}; +use ide_db::LineIndexDatabase; + +use ide_db::base_db::salsa::{self, ParallelDatabase}; +use lsp_types::NumberOrString; +use project_model::{CargoConfig, ProjectManifest, ProjectWorkspace}; +use vfs::AbsPathBuf; + +use crate::cli::lsif::lsif_types::{Document, Vertex}; +use crate::cli::{ + flags, + load_cargo::{load_workspace, LoadCargoConfig}, + Result, +}; +use crate::line_index::LineIndex; +use crate::to_proto; + +/// Need to wrap Snapshot to provide `Clone` impl for `map_with` +struct Snap(DB); +impl Clone for Snap> { + fn clone(&self) -> Snap> { + Snap(self.0.snapshot()) + } +} + +mod lsif_types; +use lsif_types::*; + +#[derive(Default)] +struct LsifManager { + count: i32, +} + +#[derive(Clone, Copy)] +struct Id(i32); + +impl From for NumberOrString { + fn from(Id(x): Id) -> Self { + NumberOrString::Number(x) + } +} + +impl LsifManager { + fn add(&mut self, data: Element) -> Id { + let id = Id(self.count); + self.emit(&serde_json::to_string(&Entry { id: id.into(), data }).unwrap()); + self.count += 1; + id + } + + // FIXME: support file in addition to stdout here + fn emit(&self, data: &str) { + println!("{}", data); + } +} + +impl flags::Lsif { + pub fn run(self) -> Result<()> { + let cargo_config = CargoConfig::default(); + let no_progress = &|_| (); + let load_cargo_config = LoadCargoConfig { + load_out_dirs_from_check: true, + with_proc_macro: true, + prefill_caches: false, + }; + let path = AbsPathBuf::assert(env::current_dir()?.join(&self.path)); + let manifest = ProjectManifest::discover_single(&path)?; + + let workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress)?; + + let (host, vfs, _proc_macro) = load_workspace(workspace, &load_cargo_config)?; + let db = host.raw_database(); + let analysis = host.analysis(); + + let si = StaticIndex::compute(db, &analysis)?; + + let mut lsif = LsifManager::default(); + lsif.add(Element::Vertex(Vertex::MetaData { + version: String::from("0.5.0"), + project_root: lsp_types::Url::from_file_path(path).unwrap(), + position_encoding: Encoding::Utf16, + tool_info: None, + })); + for StaticIndexedFile { file_id, folds } in si.files { + let path = vfs.file_path(file_id); + let path = path.as_path().unwrap(); + let doc_id = lsif.add(Element::Vertex(Vertex::Document(Document { + language_id: Language::Rust, + uri: lsp_types::Url::from_file_path(path).unwrap(), + }))); + let text = analysis.file_text(file_id)?; + let line_index = db.line_index(file_id); + let result = folds + .into_iter() + .map(|it| { + to_proto::folding_range( + &*text, + &LineIndex::with_default_options(line_index.clone()), + false, + it, + ) + }) + .collect(); + let folding_id = lsif.add(Element::Vertex(Vertex::FoldingRangeResult { result })); + lsif.add(Element::Edge(Edge::FoldingRange(EdgeData { + in_v: folding_id.into(), + out_v: doc_id.into(), + }))); + } + Ok(()) + } +} diff --git a/crates/rust-analyzer/src/cli/lsif/lsif_types.rs b/crates/rust-analyzer/src/cli/lsif/lsif_types.rs new file mode 100644 index 0000000000..bd29fd3ad8 --- /dev/null +++ b/crates/rust-analyzer/src/cli/lsif/lsif_types.rs @@ -0,0 +1,354 @@ +use lsp_types::FoldingRange; +use serde::{Deserialize, Serialize}; + +pub(crate) type RangeId = lsp_types::NumberOrString; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub(crate) enum LocationOrRangeId { + Location(lsp_types::Location), + RangeId(RangeId), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Entry { + pub(crate) id: lsp_types::NumberOrString, + #[serde(flatten)] + pub(crate) data: Element, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] +pub(crate) enum Element { + Vertex(Vertex), + Edge(Edge), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub(crate) struct ToolInfo { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + args: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + version: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub(crate) enum Encoding { + /// Currently only 'utf-16' is supported due to the limitations in LSP. + #[serde(rename = "utf-16")] + Utf16, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "label")] +pub(crate) enum Vertex { + #[serde(rename_all = "camelCase")] + MetaData { + /// The version of the LSIF format using semver notation. See https://semver.org/. Please note + /// the version numbers starting with 0 don't adhere to semver and adopters have to assume + /// that each new version is breaking. + version: String, + + /// The project root (in form of an URI) used to compute this dump. + project_root: lsp_types::Url, + + /// The string encoding used to compute line and character values in + /// positions and ranges. + position_encoding: Encoding, + + /// Information about the tool that created the dump + #[serde(skip_serializing_if = "Option::is_none")] + tool_info: Option, + }, + /// https://github.com/Microsoft/language-server-protocol/blob/master/indexFormat/specification.md#the-project-vertex + Project(Project), + Document(Document), + /// https://github.com/Microsoft/language-server-protocol/blob/master/indexFormat/specification.md#ranges + Range(lsp_types::Range), + /// https://github.com/Microsoft/language-server-protocol/blob/master/indexFormat/specification.md#result-set + ResultSet(ResultSet), + + // FIXME: support all kind of results + DefinitionResult { + result: DefinitionResultType, + }, + FoldingRangeResult { + result: Vec, + }, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "label")] +pub(crate) enum Edge { + Contains(EdgeData), + RefersTo(EdgeData), + Item(Item), + + // Methods + #[serde(rename = "textDocument/definition")] + Definition(EdgeData), + #[serde(rename = "textDocument/declaration")] + Declaration(EdgeData), + #[serde(rename = "textDocument/hover")] + Hover(EdgeData), + #[serde(rename = "textDocument/references")] + References(EdgeData), + #[serde(rename = "textDocument/implementation")] + Implementation(EdgeData), + #[serde(rename = "textDocument/typeDefinition")] + TypeDefinition(EdgeData), + #[serde(rename = "textDocument/foldingRange")] + FoldingRange(EdgeData), + #[serde(rename = "textDocument/documentLink")] + DocumentLink(EdgeData), + #[serde(rename = "textDocument/documentSymbol")] + DocumentSymbol(EdgeData), + #[serde(rename = "textDocument/diagnostic")] + Diagnostic(EdgeData), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct EdgeData { + pub(crate) in_v: lsp_types::NumberOrString, + pub(crate) out_v: lsp_types::NumberOrString, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub(crate) enum DefinitionResultType { + Scalar(LocationOrRangeId), + Array(LocationOrRangeId), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "property")] +pub(crate) enum Item { + Definition(EdgeData), + Reference(EdgeData), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Document { + pub(crate) uri: lsp_types::Url, + pub(crate) language_id: Language, +} + +/// https://github.com/Microsoft/language-server-protocol/blob/master/indexFormat/specification.md#result-set +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ResultSet { + #[serde(skip_serializing_if = "Option::is_none")] + key: Option, +} + +/// https://github.com/Microsoft/language-server-protocol/blob/master/indexFormat/specification.md#the-project-vertex +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Project { + project_file: lsp_types::Url, + language_id: Language, +} + +/// https://github.com/Microsoft/language-server-protocol/issues/213 +/// For examples, see: https://code.visualstudio.com/docs/languages/identifiers. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub(crate) enum Language { + Rust, + TypeScript, + #[serde(other)] + Other, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn metadata() { + let data = Entry { + id: lsp_types::NumberOrString::Number(1), + data: Element::Vertex(Vertex::MetaData { + version: String::from("0.4.3"), + project_root: lsp_types::Url::from_file_path("/hello/world").unwrap(), + position_encoding: Encoding::Utf16, + tool_info: Some(ToolInfo { + name: String::from("lsif-tsc"), + args: Some(vec![String::from("-p"), String::from(".")]), + version: Some(String::from("0.7.2")), + }), + }), + }; + let text = r#"{"id":1,"type":"vertex","label":"metaData","version":"0.4.3","projectRoot":"file:///hello/world","positionEncoding":"utf-16","toolInfo":{"name":"lsif-tsc","args":["-p","."],"version":"0.7.2"}}"# + .replace(' ', ""); + assert_eq!(serde_json::to_string(&data).unwrap(), text); + assert_eq!(serde_json::from_str::(&text).unwrap(), data); + } + + #[test] + fn document() { + let data = Entry { + id: lsp_types::NumberOrString::Number(1), + data: Element::Vertex(Vertex::Document(Document { + uri: lsp_types::Url::from_file_path("/Users/dirkb/sample.ts").unwrap(), + language_id: Language::TypeScript, + })), + }; + + let text = r#"{ "id": 1, "type": "vertex", "label": "document", "uri": "file:///Users/dirkb/sample.ts", "languageId": "typescript" }"# + .replace(' ', ""); + + assert_eq!(serde_json::to_string(&data).unwrap(), text); + assert_eq!(serde_json::from_str::(&text).unwrap(), data); + } + + #[test] + fn range() { + let data = Entry { + id: lsp_types::NumberOrString::Number(4), + data: Element::Vertex(Vertex::Range(lsp_types::Range::new( + lsp_types::Position::new(0, 9), + lsp_types::Position::new(0, 12), + ))), + }; + + let text = r#"{ "id": 4, "type": "vertex", "label": "range", "start": { "line": 0, "character": 9}, "end": { "line": 0, "character": 12 } }"# + .replace(' ', ""); + + assert_eq!(serde_json::to_string(&data).unwrap(), text); + assert_eq!(serde_json::from_str::(&text).unwrap(), data); + } + + #[test] + fn contains() { + let data = Entry { + id: lsp_types::NumberOrString::Number(5), + data: Element::Edge(Edge::Contains(EdgeData { + in_v: lsp_types::NumberOrString::Number(4), + out_v: lsp_types::NumberOrString::Number(1), + })), + }; + + let text = r#"{ "id": 5, "type": "edge", "label": "contains", "outV": 1, "inV": 4}"# + .replace(' ', ""); + + assert_eq!( + serde_json::from_str::(&text).unwrap(), + serde_json::to_value(&data).unwrap() + ); + } + + #[test] + fn refers_to() { + let data = Entry { + id: lsp_types::NumberOrString::Number(5), + data: Element::Edge(Edge::RefersTo(EdgeData { + in_v: lsp_types::NumberOrString::Number(2), + out_v: lsp_types::NumberOrString::Number(3), + })), + }; + + let text = r#"{ "id": 5, "type": "edge", "label": "refersTo", "outV": 3, "inV": 2}"# + .replace(' ', ""); + + assert_eq!( + serde_json::from_str::(&text).unwrap(), + serde_json::to_value(&data).unwrap() + ); + } + + #[test] + fn result_set() { + let data = Entry { + id: lsp_types::NumberOrString::Number(2), + data: Element::Vertex(Vertex::ResultSet(ResultSet { key: None })), + }; + + let text = r#"{ "id": 2, "type": "vertex", "label": "resultSet" }"#.replace(' ', ""); + + assert_eq!(serde_json::to_string(&data).unwrap(), text); + assert_eq!(serde_json::from_str::(&text).unwrap(), data); + + let data = Entry { + id: lsp_types::NumberOrString::Number(4), + data: Element::Vertex(Vertex::ResultSet(ResultSet { + key: Some(String::from("hello")), + })), + }; + + let text = r#"{ "id": 4, "type": "vertex", "label": "resultSet", "key": "hello" }"# + .replace(' ', ""); + + assert_eq!(serde_json::to_string(&data).unwrap(), text); + assert_eq!(serde_json::from_str::(&text).unwrap(), data); + } + + #[test] + fn definition() { + let data = Entry { + id: lsp_types::NumberOrString::Number(21), + data: Element::Edge(Edge::Item(Item::Definition(EdgeData { + in_v: lsp_types::NumberOrString::Number(18), + out_v: lsp_types::NumberOrString::Number(16), + }))), + }; + + let text = r#"{ "id": 21, "type": "edge", "label": "item", "property": "definition", "outV": 16, "inV": 18}"# + .replace(' ', ""); + + assert_eq!( + serde_json::from_str::(&text).unwrap(), + serde_json::to_value(&data).unwrap() + ); + } + + mod methods { + use super::*; + + #[test] + fn references() { + let data = Entry { + id: lsp_types::NumberOrString::Number(17), + data: Element::Edge(Edge::References(EdgeData { + in_v: lsp_types::NumberOrString::Number(16), + out_v: lsp_types::NumberOrString::Number(15), + })), + }; + + let text = r#"{ "id": 17, "type": "edge", "label": "textDocument/references", "outV": 15, "inV": 16 }"#; + + assert_eq!( + serde_json::from_str::(&text).unwrap(), + serde_json::to_value(&data).unwrap() + ); + } + + #[test] + fn definition() { + let data = Entry { + id: lsp_types::NumberOrString::Number(13), + data: Element::Vertex(Vertex::DefinitionResult { + result: DefinitionResultType::Scalar(LocationOrRangeId::RangeId( + lsp_types::NumberOrString::Number(7), + )), + }), + }; + + let text = + r#"{ "id": 13, "type": "vertex", "label": "definitionResult", "result": 7 }"#; + + assert_eq!( + serde_json::from_str::(&text).unwrap(), + serde_json::to_value(&data).unwrap() + ); + } + } +} diff --git a/crates/rust-analyzer/src/line_index.rs b/crates/rust-analyzer/src/line_index.rs index c116414da0..6d46171cc3 100644 --- a/crates/rust-analyzer/src/line_index.rs +++ b/crates/rust-analyzer/src/line_index.rs @@ -18,6 +18,12 @@ pub(crate) struct LineIndex { pub(crate) encoding: OffsetEncoding, } +impl LineIndex { + pub(crate) fn with_default_options(index: Arc) -> Self { + Self { index, endings: LineEndings::Unix, encoding: OffsetEncoding::Utf8 } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum LineEndings { Unix,