Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Implementation CodeLens.** Interfaces, classes, and methods show clickable implementation counts once the workspace index is ready. Each lens opens every exact implementation location, including members inherited from a parent or supplied by a trait. Contributed by @sidux.
- **`analyze` takes more than one path.** `phpantom_lsp analyze app/ lib/Helper.php tests/` scans the union of everything named, mixing directories and single files freely, so a pre-commit hook or a CI step can hand it exactly the paths that changed instead of running the whole project or invoking the binary once per path. Overlapping arguments are reported once, and a path that does not exist still stops the run with exit code 2. Naming no path scans the entire project, as before.

### Changed
Expand Down
89 changes: 89 additions & 0 deletions src/code_lens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use tower_lsp::lsp_types::*;

use crate::Backend;
use crate::atom::Atom;
use crate::definition::MemberImplementationTarget;
use crate::definition::member::MemberKind;
use crate::text_position::offset_to_position;
use crate::types::{ClassInfo, ClassLikeKind, MAX_INHERITANCE_DEPTH};
Expand All @@ -21,6 +22,17 @@ fn line_indent(content: &str, byte_offset: usize) -> u32 {
.count() as u32
}

fn implementation_lens_title(count: usize) -> String {
format!(
"{count} {}",
if count == 1 {
"implementation"
} else {
"implementations"
}
)
}

/// Information about a prototype (ancestor) method that a local method
/// overrides or implements.
struct Prototype {
Expand All @@ -45,9 +57,36 @@ impl Backend {
};

let mut lenses = Vec::new();
let context = self.file_context(uri);
let class_loader = self.class_loader(&context);

for class in &classes {
let class_fqn = class.fqn();
let implementation_descendants = if self
.workspace_indexed
.load(std::sync::atomic::Ordering::Acquire)
{
self.implementation_descendants(class, &class_loader, true)
} else {
Vec::new()
};

if !implementation_descendants.is_empty() {
let locations = self.class_implementation_locations_from_descendants(
uri,
content,
class,
&implementation_descendants,
);
if let Some(lens) = self.build_locations_lens(
uri,
offset_to_position(content, class.keyword_offset as usize),
implementation_lens_title(locations.len()),
locations,
) {
lenses.push(lens);
}
}

if let Some(lens) = self.build_covers_lens(class, uri, content) {
lenses.push(lens);
Expand Down Expand Up @@ -75,6 +114,27 @@ impl Backend {
};

let proto = self.find_prototype(class, &class_fqn, &method.name, uri, content);
if !implementation_descendants.is_empty() {
let locations = self.member_implementation_locations_from_descendants(
uri,
content,
MemberImplementationTarget {
class,
name: &method.name,
kind: MemberKind::Method,
},
&implementation_descendants,
&class_loader,
);
if let Some(lens) = self.build_locations_lens(
uri,
pos,
implementation_lens_title(locations.len()),
locations,
) {
lenses.push(lens);
}
}
if let Some(proto) = proto {
let icon = if proto.is_interface { "◆" } else { "↑" };
let title = format!("{} {}::{}", icon, proto.ancestor_name, method.name);
Expand Down Expand Up @@ -102,6 +162,35 @@ impl Backend {
}
}

fn build_locations_lens(
&self,
origin_uri: &str,
source_position: Position,
title: String,
locations: Vec<Location>,
) -> Option<CodeLens> {
if locations.is_empty() {
return None;
}
let origin_uri = Url::parse(origin_uri).ok()?;
Some(CodeLens {
range: Range::new(
Position::new(source_position.line, 0),
Position::new(source_position.line, 0),
),
command: Some(Command {
title,
command: "editor.action.showReferences".to_string(),
arguments: Some(vec![
serde_json::json!(origin_uri),
serde_json::json!(source_position),
serde_json::json!(locations),
]),
}),
data: None,
})
}

/// Build the "which tests cover this class" lens for a class
/// declaration, from the test classes whose PHPUnit coverage metadata
/// (`@covers` / `@uses` / `#[CoversClass]` and friends) names it.
Expand Down
Loading
Loading