diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 19f035a7..83edfb0c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **The editor stays responsive during fast typing in large files.** Editors send a burst of requests on every keystroke (completion, a documentation lookup for each suggestion, diagnostics, code lens, semantic highlighting, and more). The server processed only a few at a time, so during continuous typing the burst backed up until the server stopped answering anything at all, including the completion the user was waiting on, and it only recovered after a restart. Now the burst is processed concurrently and every expensive request runs off the main loop: diagnostics (which re-analyze the whole file on each edit) compute in the background instead of on the request that asked for them, so a diagnostic pull returns immediately and never blocks the threads that deliver completion and hover, and repeated whole-file requests (semantic highlighting, code lens, the document outline, folding, document links) are collapsed so a fast typist's superseded requests no longer pile up and monopolize the CPU. Completion and other requests keep coming back while you type. - **Typing in a large file no longer pegs the CPU and stalls completion.** Semantic highlighting recomputed every token's position by rescanning the file from the beginning, so a large file took many seconds at full CPU to highlight. Editors request highlighting on every keystroke, so this ran continuously while typing and starved completion, hover, and other requests until they appeared to hang. Highlighting a large file is now effectively instant, and the same speedup applies to the document outline and code folding, which used the same per-position rescan. - **The first use of a global helper function no longer stalls.** Functions defined in Composer "files" autoload entries and guarded by `if (! function_exists(...))` (such as Laravel's `app()`, `session()`, and `route()`) were parsed on demand the first time one was used, which meant the first completion, hover, or go-to-definition involving such a helper blocked while the server parsed every autoload file in turn. These files are now parsed up front during indexing, so the first lookup is instant. +- **Framework global helpers loaded outside Composer autoload are now indexed.** Some frameworks ship their global function aliases in a `*_global.php` file that sits beside an autoloaded helper file but is pulled in by the framework's own bootstrap rather than Composer's `files` autoload, so it never appears in the autoload manifest. CakePHP is the canonical case: helpers like `__()`, `h()`, and `env()` live in such a sibling and were reported as unknown functions on every call. These sibling helper files are now indexed too, so the globals resolve. Contributed by @dereuromark in https://github.com/AJenbo/phpantom_lsp/pull/175. - **Classes defined inside conditional blocks are now fully resolved.** A class declared inside an `if`/`else` version guard (the Doctrine `ServiceEntityRepository` pattern, where a base class is defined differently per ORM version) was previously discovered by name only, so its parent and `@extends` generics were dropped. Such classes now carry their full inheritance, so member completion, hover, go-to-definition, and generic type resolution work both on them and inside their own methods. When the same class name appears in more than one branch, the first declaration wins. Contributed by @MrSrsen in https://github.com/PHPantom-dev/phpantom_lsp/pull/154. - **Editing a base class stays responsive in large projects.** Changing a class that many others extend used to invalidate the resolved-class cache by rescanning every cached class on each edit, which briefly stalled large projects with deep class hierarchies. Invalidation now touches only the classes that actually depend on the edited one. - **Completion latency stays flat during sustained fast typing.** Concurrent requests resolving the same classes contended on a single lock guarding the resolved-class cache, so completion latency crept upward for as long as a typing burst continued. The cache now allows parallel reads, so the many lookups in flight at once no longer serialize behind one another, and when a request first loads a vendor class the work to record it in the shared index is prepared before the index is locked, so other requests no longer wait on it. diff --git a/src/composer.rs b/src/composer.rs index 2155fa1c..25e41984 100644 --- a/src/composer.rs +++ b/src/composer.rs @@ -21,7 +21,7 @@ //! Composer JSON parsing is delegated to the [`mago_composer`] crate, //! which provides typed Rust structs for the full `composer.json` schema. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; @@ -342,6 +342,54 @@ pub fn parse_autoload_files(workspace_root: &Path, vendor_dir: &str) -> Vec Vec { + let mut out = Vec::new(); + let mut scanned_dirs = HashSet::new(); + let mut seen_files = HashSet::new(); + + for file in autoload_files { + let Some(dir) = file.parent() else { + continue; + }; + if !scanned_dirs.insert(dir.to_path_buf()) { + continue; + } + let Ok(entries) = fs::read_dir(dir) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + if name.ends_with("_global.php") && path.is_file() && seen_files.insert(path.clone()) { + out.push(path); + } + } + } + + out +} + // ── PSR-4 path abstraction ───────────────────────────────────────── // // `mago-composer` emits two structurally identical but nominally @@ -972,6 +1020,59 @@ mod tests { // ── detect_drupal_web_root ────────────────────────────────────── + #[test] + fn discovers_global_sibling_beside_autoload_file() { + // Mirrors CakePHP: an autoloaded `functions.php` sits next to a + // `functions_global.php` sibling that Composer never lists. + let dir = tempfile::tempdir().unwrap(); + let core = dir.path().join("Core"); + std::fs::create_dir_all(&core).unwrap(); + let autoloaded = core.join("functions.php"); + let sibling = core.join("functions_global.php"); + std::fs::write( + &autoloaded, + " = autoload_files; + file_queue.extend(sibling_globals); let mut visited: HashSet = HashSet::new(); while let Some(file_path) = file_queue.pop() {