Skip to content
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.
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 @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Eloquent relation and column string completion.** Typing inside string arguments to `with()`, `load()`, `whereHas()`, and other Eloquent methods that accept relation names now offers relationship method names as completions, with dot-notation traversal for nested relations. Similarly, `where()`, `orderBy()`, `select()`, `pluck()`, and other column-accepting methods offer model column names (from `$casts`, `$fillable`, `@property` tags, timestamps, etc.).
- **`model-property<T>` pseudo-type recognition.** The Larastan `model-property<Model>` type no longer triggers "unknown class" diagnostics. It is treated as a string subtype.
- **`compact()` strings are linked to local variables.** A string argument to `compact('user')` is now treated as a reference to the matching local variable. Renaming the variable updates the string (and renaming from the string updates the variable and its other uses), find-references includes the string, and go-to-definition on the string jumps to the variable's assignment. Contributed by @calebdw in https://github.com/PHPantom-dev/phpantom_lsp/pull/159.
- **Imported and same-namespace symbols rank first in completion.** Classes, functions, and constants that are already imported via a `use` statement or live in the same namespace now always appear above non-imported symbols in the completion list, regardless of dependency provenance. Previously a non-imported project class could outrank an already-imported vendor class, forcing users to scroll past irrelevant results. Contributed by @calebdw.
- **Laravel route controller method navigation and completion.** Method-name strings inside `Route::controller(X::class)->group(fn(){…})` closures now resolve as references to the controller's methods. Go-to-definition, find-references, rename, hover, and diagnostics all work on the action string (e.g. `Route::patch('cancel', 'cancel')` resolves `'cancel'` to `WorkItemController::cancel()`). Autocompletion inside the action string offers the controller's methods. Handles `->controller()` anywhere in the fluent chain, chained route calls (`->name()`, etc.), and nested groups where an inner `->controller()` shadows the outer one. Contributed by @calebdw.

### Changed
Expand Down
9 changes: 7 additions & 2 deletions src/completion/context/class_completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ pub(in crate::completion) fn match_quality(short_name: &str, prefix: &str) -> ch

/// Assemble a sort_text string for a class name completion item.
///
/// The format is `{match_quality}{origin_tier}{source_tier}{affinity}{demote}{gap}_{short_name_lower}`
/// The format is `{match_quality}{source_tier}{origin_tier}{affinity}{demote}{gap}_{short_name_lower}`
/// where:
/// - `match_quality`: `'a'` exact, `'b'` starts-with, `'c'` contains
/// - `source_tier`: `'0'` use-imported, `'1'` same-namespace, `'2'` everything else
Expand All @@ -1003,6 +1003,11 @@ pub(in crate::completion) fn match_quality(short_name: &str, prefix: &str) -> ch
/// first. This smooths the visual transition as a prefix match
/// narrows toward an exact match (e.g. typing "Pro" ranks `Product`
/// above `ProductFilterTerm` when both share the same affinity).
///
/// **`source_tier` sorts before `origin_tier`** so that an already-imported
/// class always ranks above a non-imported one, regardless of where it
/// comes from. A vendor class that's already in the file's `use` map
/// is more relevant than a project class the user hasn't imported yet.
pub(in crate::completion) fn class_sort_text(
short_name: &str,
fqn: &str,
Expand All @@ -1023,8 +1028,8 @@ pub(in crate::completion) fn class_sort_text(
format!(
"{}{}{}{}{}{}_{}",
quality,
origin_tier,
source_tier,
origin_tier,
affinity,
demote,
gap,
Expand Down
5 changes: 3 additions & 2 deletions src/completion/context/class_completion_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,9 @@ fn test_class_sort_text_format() {
false,
&table,
);
// quality='a' (exact), origin='0', tier='2', affinity=9999-4=9995 → "9995", demote='0', gap=5-5=0 → "000"
assert_eq!(result, "a0299950000_order");
// quality='a' (exact), source='2', origin='0', affinity=9999-4=9995 → "9995", demote='0', gap=5-5=0 → "000"
// source_tier sorts before origin_tier so imported items always rank first.
assert_eq!(result, "a2099950000_order");
}

#[test]
Expand Down
4 changes: 3 additions & 1 deletion src/completion/context/symbol_ranking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ pub(crate) fn flat_symbol_sort_text(
source_tier: char,
) -> String {
let quality = super::class_completion::match_quality(short_name, prefix);
// source_tier before origin_tier: imported symbols always rank above
// non-imported ones, regardless of provenance.
format!(
"{}{}{}_{}",
quality,
origin_tier,
source_tier,
origin_tier,
short_name.to_lowercase()
)
}
24 changes: 16 additions & 8 deletions tests/integration/completion_class_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,14 @@ async fn test_class_name_completion_prioritizes_project_core_explicit_then_trans
*backend.workspace_root().write() = Some(dir.path().to_path_buf());
backend.initialized(InitializedParams {}).await;

// Use a file in the App namespace so that App\RuntimeException is
// same-namespace (source '1') and all other RuntimeException variants
// are non-imported (source '2'). This isolates origin_tier ordering
// for non-imported classes. (Without a namespace the global core
// RuntimeException would be same-namespace and sort first.)
let uri = Url::parse("file:///app.php").unwrap();
let text = "<?php\nnew RuntimeE\n";
let items = complete_at(&backend, &uri, text, 1, 12).await;
let text = "<?php\nnamespace App;\nnew RuntimeE\n";
let items = complete_at(&backend, &uri, text, 2, 12).await;
let classes = class_items(&items);

let project_pos = classes
Expand Down Expand Up @@ -323,11 +328,13 @@ async fn test_function_completion_prioritizes_project_core_explicit_then_transit
let items = complete_at(&backend, &uri, text, 1, 9).await;
let funcs = function_items(&items);
let sorts: Vec<String> = funcs.iter().filter_map(|i| i.sort_text.clone()).collect();
// sort_text format: {quality}{source}{origin}_name
// source_tier sorts before origin_tier.
assert!(
sorts.iter().any(|s| s.starts_with("b00_"))
&& sorts.iter().any(|s| s.starts_with("b10_"))
&& sorts.iter().any(|s| s.starts_with("b21_"))
&& sorts.iter().any(|s| s.starts_with("b31_")),
&& sorts.iter().any(|s| s.starts_with("b01_"))
&& sorts.iter().any(|s| s.starts_with("b12_"))
&& sorts.iter().any(|s| s.starts_with("b13_")),
"expected project/core/explicit/transitive sort tiers in function completions, got: {sorts:?}"
);
}
Expand Down Expand Up @@ -858,12 +865,13 @@ async fn test_class_name_completion_same_namespace() {
class_fqns
);

// Same-namespace should have source tier '1' at position 2.
// Same-namespace should have source tier '1' at position 1
// (source_tier sorts before origin_tier).
let service_item = find_by_fqn(&classes, "App\\UserService").unwrap();
let sort = service_item.sort_text.as_deref().unwrap_or("");
assert!(
sort.len() > 2 && &sort[2..3] == "1",
"Same-namespace classes should have source tier '1' at position 2, got: {:?}",
sort.len() > 1 && &sort[1..2] == "1",
"Same-namespace classes should have source tier '1' at position 1, got: {:?}",
sort
);
}
Expand Down
Loading