Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method references CodeLens #5928

Merged
merged 6 commits into from Sep 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
95 changes: 95 additions & 0 deletions crates/ide/src/fn_references.rs
@@ -0,0 +1,95 @@
//! This module implements a methods and free functions search in the specified file.
//! We have to skip tests, so cannot reuse file_structure module.

use hir::Semantics;
use ide_db::RootDatabase;
use syntax::{ast, ast::NameOwner, AstNode, SyntaxNode};

use crate::{runnables::has_test_related_attribute, FileId, FileRange};

pub(crate) fn find_all_methods(db: &RootDatabase, file_id: FileId) -> Vec<FileRange> {
let sema = Semantics::new(db);
let source_file = sema.parse(file_id);
source_file.syntax().descendants().filter_map(|it| method_range(it, file_id)).collect()
}

fn method_range(item: SyntaxNode, file_id: FileId) -> Option<FileRange> {
ast::Fn::cast(item).and_then(|fn_def| {
if has_test_related_attribute(&fn_def) {
None
} else {
fn_def.name().map(|name| FileRange { file_id, range: name.syntax().text_range() })
}
})
}

#[cfg(test)]
mod tests {
use crate::mock_analysis::analysis_and_position;
use crate::{FileRange, TextSize};
use std::ops::RangeInclusive;

#[test]
fn test_find_all_methods() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
fn private_fn() {<|>}

pub fn pub_fn() {}

pub fn generic_fn<T>(arg: T) {}
"#,
);

let refs = analysis.find_all_methods(pos.file_id).unwrap();
check_result(&refs, &[3..=13, 27..=33, 47..=57]);
}

#[test]
fn test_find_trait_methods() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
trait Foo {
fn bar() {<|>}
fn baz() {}
}
"#,
);

let refs = analysis.find_all_methods(pos.file_id).unwrap();
check_result(&refs, &[19..=22, 35..=38]);
}

#[test]
fn test_skip_tests() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
#[test]
fn foo() {<|>}

pub fn pub_fn() {}

mod tests {
#[test]
fn bar() {}
}
"#,
);

let refs = analysis.find_all_methods(pos.file_id).unwrap();
check_result(&refs, &[28..=34]);
}

fn check_result(refs: &[FileRange], expected: &[RangeInclusive<u32>]) {
assert_eq!(refs.len(), expected.len());

for (i, item) in refs.iter().enumerate() {
let range = &expected[i];
assert_eq!(TextSize::from(*range.start()), item.range.start());
assert_eq!(TextSize::from(*range.end()), item.range.end());
}
}
}
6 changes: 6 additions & 0 deletions crates/ide/src/lib.rs
Expand Up @@ -38,6 +38,7 @@ mod join_lines;
mod matching_brace;
mod parent_module;
mod references;
mod fn_references;
mod runnables;
mod status;
mod syntax_highlighting;
Expand Down Expand Up @@ -369,6 +370,11 @@ impl Analysis {
})
}

/// Finds all methods and free functions for the file. Does not return tests!
pub fn find_all_methods(&self, file_id: FileId) -> Cancelable<Vec<FileRange>> {
self.with_db(|db| fn_references::find_all_methods(db, file_id))
}

/// Returns a short text describing element at position.
pub fn hover(
&self,
Expand Down
2 changes: 1 addition & 1 deletion crates/ide/src/runnables.rs
Expand Up @@ -203,7 +203,7 @@ impl TestAttr {
///
/// It may produce false positives, for example, `#[wasm_bindgen_test]` requires a different command to run the test,
/// but it's better than not to have the runnables for the tests at all.
fn has_test_related_attribute(fn_def: &ast::Fn) -> bool {
pub(crate) fn has_test_related_attribute(fn_def: &ast::Fn) -> bool {
fn_def
.attrs()
.filter_map(|attr| attr.path())
Expand Down
21 changes: 13 additions & 8 deletions crates/rust-analyzer/src/config.rs
Expand Up @@ -74,19 +74,18 @@ pub struct LensConfig {
pub run: bool,
pub debug: bool,
pub implementations: bool,
pub method_refs: bool,
}

impl Default for LensConfig {
fn default() -> Self {
Self { run: true, debug: true, implementations: true }
Self { run: true, debug: true, implementations: true, method_refs: false }
}
}

impl LensConfig {
pub const NO_LENS: LensConfig = Self { run: false, debug: false, implementations: false };

pub fn any(&self) -> bool {
self.implementations || self.runnable()
self.implementations || self.runnable() || self.references()
}

pub fn none(&self) -> bool {
Expand All @@ -96,6 +95,10 @@ impl LensConfig {
pub fn runnable(&self) -> bool {
self.run || self.debug
}

pub fn references(&self) -> bool {
self.method_refs
}
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -278,6 +281,7 @@ impl Config {
run: data.lens_enable && data.lens_run,
debug: data.lens_enable && data.lens_debug,
implementations: data.lens_enable && data.lens_implementations,
method_refs: data.lens_enable && data.lens_methodReferences,
};

if !data.linkedProjects.is_empty() {
Expand Down Expand Up @@ -459,10 +463,11 @@ config_data! {
inlayHints_parameterHints: bool = true,
inlayHints_typeHints: bool = true,

lens_debug: bool = true,
lens_enable: bool = true,
lens_implementations: bool = true,
lens_run: bool = true,
lens_debug: bool = true,
lens_enable: bool = true,
lens_implementations: bool = true,
lens_run: bool = true,
lens_methodReferences: bool = false,

linkedProjects: Vec<ManifestOrProjectJson> = Vec::new(),
lruCapacity: Option<usize> = None,
Expand Down
54 changes: 54 additions & 0 deletions crates/rust-analyzer/src/handlers.rs
Expand Up @@ -11,6 +11,7 @@ use ide::{
FileId, FilePosition, FileRange, HoverAction, HoverGotoTypeData, NavigationTarget, Query,
RangeInfo, Runnable, RunnableKind, SearchScope, TextEdit,
};
use itertools::Itertools;
use lsp_server::ErrorCode;
use lsp_types::{
CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
Expand Down Expand Up @@ -952,13 +953,30 @@ pub(crate) fn handle_code_lens(
}),
);
}

if snap.config.lens.references() {
lenses.extend(snap.analysis.find_all_methods(file_id)?.into_iter().map(|it| {
let range = to_proto::range(&line_index, it.range);
let position = to_proto::position(&line_index, it.range.start());
let lens_params =
lsp_types::TextDocumentPositionParams::new(params.text_document.clone(), position);

CodeLens {
range,
command: None,
data: Some(to_value(CodeLensResolveData::References(lens_params)).unwrap()),
}
}));
}

Ok(Some(lenses))
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum CodeLensResolveData {
Impls(lsp_types::request::GotoImplementationParams),
References(lsp_types::TextDocumentPositionParams),
}

pub(crate) fn handle_code_lens_resolve(
Expand Down Expand Up @@ -990,6 +1008,34 @@ pub(crate) fn handle_code_lens_resolve(
);
Ok(CodeLens { range: code_lens.range, command: Some(cmd), data: None })
}
Some(CodeLensResolveData::References(doc_position)) => {
let position = from_proto::file_position(&snap, doc_position.clone())?;
let locations = snap
.analysis
.find_all_refs(position, None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, yeah, I am still not happy about factoring here -- way to many logic happens in the handlers.

But it also is non-trivial to properly factor the logic given the two-phase requests.

I think there's a better solution here, but I haven't looked in the details yet. I expect we want to use exactly the same pattern for code lenses as for assists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just reuse existing find_all_refs method and convert the results to LSP types. And that's all.

I think there's a better solution here, but I haven't looked in the details yet.

Perhaps we can (should) utilize the fact that Code Lens Request supports partial results. But it requires a lot of work and if you do not mind I'd prefer to leave it for another PR.

.unwrap_or(None)
.map(|r| {
r.references()
.iter()
.filter_map(|it| to_proto::location(&snap, it.file_range).ok())
.collect_vec()
})
.unwrap_or_default();

let title = reference_title(locations.len());
let cmd = if locations.is_empty() {
Command { title, command: "".into(), arguments: None }
} else {
show_references_command(
title,
&doc_position.text_document.uri,
code_lens.range.start,
locations,
)
};

Ok(CodeLens { range: code_lens.range, command: Some(cmd), data: None })
}
None => Ok(CodeLens {
range: code_lens.range,
command: Some(Command { title: "Error".into(), ..Default::default() }),
Expand Down Expand Up @@ -1248,6 +1294,14 @@ fn implementation_title(count: usize) -> String {
}
}

fn reference_title(count: usize) -> String {
if count == 1 {
"1 reference".into()
} else {
format!("{} references", count)
}
}

fn show_references_command(
title: String,
uri: &lsp_types::Url,
Expand Down
5 changes: 5 additions & 0 deletions editors/code/package.json
Expand Up @@ -554,6 +554,11 @@
"type": "boolean",
"default": true
},
"rust-analyzer.lens.methodReferences": {
"markdownDescription": "Whether to show `Method References` lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"type": "boolean",
"default": false
},
"rust-analyzer.hoverActions.enable": {
"description": "Whether to show HoverActions in Rust files.",
"type": "boolean",
Expand Down
1 change: 1 addition & 0 deletions editors/code/src/config.ts
Expand Up @@ -138,6 +138,7 @@ export class Config {
run: this.get<boolean>("lens.run"),
debug: this.get<boolean>("lens.debug"),
implementations: this.get<boolean>("lens.implementations"),
methodReferences: this.get<boolean>("lens.methodReferences"),
};
}

Expand Down