Skip to content

Commit d4eac18

Browse files
committed
Reuse external proxy pattern
1 parent eba0193 commit d4eac18

8 files changed

Lines changed: 1322 additions & 1447 deletions

File tree

src/mago.rs

Lines changed: 5 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use std::time::Duration;
5353
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range};
5454

5555
use crate::config::MagoConfig;
56+
use crate::process::paths_match;
5657

5758
// ── Tool resolution ─────────────────────────────────────────────────
5859

@@ -91,59 +92,13 @@ pub(crate) fn resolve_mago(
9192
Some(cmd) => Some(ResolvedMago {
9293
path: PathBuf::from(cmd),
9394
}),
94-
// Auto-detect.
95-
None => auto_detect(workspace_root, bin_dir),
95+
// Auto-detect: `<bin_dir>/mago` under the workspace root,
96+
// then `$PATH`.
97+
None => crate::process::auto_detect_binary(workspace_root, bin_dir, "mago")
98+
.map(|path| ResolvedMago { path }),
9699
}
97100
}
98101

99-
/// Auto-detect Mago by checking `<bin_dir>/mago` then `$PATH`.
100-
fn auto_detect(workspace_root: Option<&Path>, bin_dir: Option<&str>) -> Option<ResolvedMago> {
101-
// Check the Composer bin directory first (vendor/bin/mago).
102-
if let Some(root) = workspace_root {
103-
let bin = bin_dir.unwrap_or("vendor/bin");
104-
let candidate = root.join(bin).join("mago");
105-
if candidate.is_file() {
106-
return Some(ResolvedMago { path: candidate });
107-
}
108-
}
109-
110-
// Fall back to $PATH.
111-
if let Ok(path) = which("mago") {
112-
return Some(ResolvedMago { path });
113-
}
114-
115-
None
116-
}
117-
118-
/// Simple `which`-like lookup: search `$PATH` for an executable with
119-
/// the given name.
120-
fn which(binary_name: &str) -> Result<PathBuf, String> {
121-
let path_var = std::env::var("PATH").map_err(|_| "PATH not set".to_string())?;
122-
123-
for dir in std::env::split_paths(&path_var) {
124-
let candidate = dir.join(binary_name);
125-
if candidate.is_file() && is_executable(&candidate) {
126-
return Ok(candidate);
127-
}
128-
}
129-
130-
Err(format!("{} not found on PATH", binary_name))
131-
}
132-
133-
/// Check whether a file is executable.
134-
#[cfg(unix)]
135-
fn is_executable(path: &Path) -> bool {
136-
use std::os::unix::fs::PermissionsExt;
137-
std::fs::metadata(path)
138-
.map(|m| m.permissions().mode() & 0o111 != 0)
139-
.unwrap_or(false)
140-
}
141-
142-
#[cfg(not(unix))]
143-
fn is_executable(_path: &Path) -> bool {
144-
true
145-
}
146-
147102
// ── Mago execution ─────────────────────────────────────────────────
148103

149104
/// Run `mago lint` on the given buffer content and return LSP diagnostics.
@@ -657,26 +612,6 @@ pub(crate) fn byte_offset_to_position(content: &str, offset: usize) -> Position
657612
}
658613
}
659614

660-
/// Check whether two file paths refer to the same file.
661-
///
662-
/// Mago reports paths as absolute. We compare by checking suffix
663-
/// matches (one path ends with the other) to handle cases where one
664-
/// path is relative and the other is absolute.
665-
fn paths_match(a: &str, b: &str) -> bool {
666-
if a == b {
667-
return true;
668-
}
669-
// Normalize separators for comparison.
670-
let a_norm = a.replace('\\', "/");
671-
let b_norm = b.replace('\\', "/");
672-
if a_norm == b_norm {
673-
return true;
674-
}
675-
// Check suffix match (one is a suffix of the other), requiring a
676-
// path separator boundary so that e.g. "AFoo.php" does not match "Foo.php".
677-
a_norm.ends_with(&format!("/{}", b_norm)) || b_norm.ends_with(&format!("/{}", a_norm))
678-
}
679-
680615
// ── Tests ───────────────────────────────────────────────────────────
681616

682617
#[cfg(test)]
@@ -724,34 +659,6 @@ mod tests {
724659
);
725660
}
726661

727-
// ── paths_match ─────────────────────────────────────────────────
728-
729-
#[test]
730-
fn paths_match_identical() {
731-
assert!(paths_match(
732-
"/home/user/project/src/Foo.php",
733-
"/home/user/project/src/Foo.php"
734-
));
735-
}
736-
737-
#[test]
738-
fn paths_match_suffix() {
739-
assert!(paths_match("/home/user/project/src/Foo.php", "src/Foo.php"));
740-
}
741-
742-
#[test]
743-
fn paths_match_different_files() {
744-
assert!(!paths_match(
745-
"/home/user/project/src/Foo.php",
746-
"src/Bar.php"
747-
));
748-
}
749-
750-
#[test]
751-
fn paths_match_rejects_partial_filename() {
752-
assert!(!paths_match("/project/src/AFoo.php", "Foo.php"));
753-
}
754-
755662
// ── byte_offset_to_position ─────────────────────────────────────
756663

757664
#[test]

src/phpcs.rs

Lines changed: 5 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use std::time::Duration;
4747
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range};
4848

4949
use crate::config::PhpcsConfig;
50+
use crate::process::paths_match;
5051

5152
/// Default PHPCS timeout in milliseconds (30 seconds).
5253
const DEFAULT_TIMEOUT_MS: u64 = 30_000;
@@ -80,59 +81,13 @@ pub(crate) fn resolve_phpcs(
8081
Some(cmd) => Some(ResolvedPhpcs {
8182
path: PathBuf::from(cmd),
8283
}),
83-
// Auto-detect.
84-
None => auto_detect(workspace_root, bin_dir),
84+
// Auto-detect: `<bin_dir>/phpcs` under the workspace root,
85+
// then `$PATH`.
86+
None => crate::process::auto_detect_binary(workspace_root, bin_dir, "phpcs")
87+
.map(|path| ResolvedPhpcs { path }),
8588
}
8689
}
8790

88-
/// Auto-detect PHPCS by checking `<bin_dir>/phpcs` then `$PATH`.
89-
fn auto_detect(workspace_root: Option<&Path>, bin_dir: Option<&str>) -> Option<ResolvedPhpcs> {
90-
// Check the Composer bin directory first.
91-
if let Some(root) = workspace_root {
92-
let bin = bin_dir.unwrap_or("vendor/bin");
93-
let candidate = root.join(bin).join("phpcs");
94-
if candidate.is_file() {
95-
return Some(ResolvedPhpcs { path: candidate });
96-
}
97-
}
98-
99-
// Fall back to $PATH.
100-
if let Ok(path) = which("phpcs") {
101-
return Some(ResolvedPhpcs { path });
102-
}
103-
104-
None
105-
}
106-
107-
/// Simple `which`-like lookup: search `$PATH` for an executable with
108-
/// the given name.
109-
fn which(binary_name: &str) -> Result<PathBuf, String> {
110-
let path_var = std::env::var("PATH").map_err(|_| "PATH not set".to_string())?;
111-
112-
for dir in std::env::split_paths(&path_var) {
113-
let candidate = dir.join(binary_name);
114-
if candidate.is_file() && is_executable(&candidate) {
115-
return Ok(candidate);
116-
}
117-
}
118-
119-
Err(format!("{} not found on PATH", binary_name))
120-
}
121-
122-
/// Check whether a file is executable.
123-
#[cfg(unix)]
124-
fn is_executable(path: &Path) -> bool {
125-
use std::os::unix::fs::PermissionsExt;
126-
std::fs::metadata(path)
127-
.map(|m| m.permissions().mode() & 0o111 != 0)
128-
.unwrap_or(false)
129-
}
130-
131-
#[cfg(not(unix))]
132-
fn is_executable(_path: &Path) -> bool {
133-
true
134-
}
135-
13691
// ── PHPCS execution ─────────────────────────────────────────────────
13792

13893
/// Run PHPCS on the given buffer content and return LSP diagnostics.
@@ -438,27 +393,6 @@ fn parse_phpcs_message(msg: &serde_json::Value) -> Option<Diagnostic> {
438393
})
439394
}
440395

441-
/// Check whether two file paths refer to the same file.
442-
///
443-
/// PHPCS may use the `--stdin-path` value as the key. We compare by
444-
/// checking suffix matches (one path ends with the other) to handle
445-
/// cases where one path is relative and the other is absolute, or
446-
/// where symlinks produce different prefixes.
447-
fn paths_match(a: &str, b: &str) -> bool {
448-
if a == b {
449-
return true;
450-
}
451-
// Normalize separators for comparison.
452-
let a_norm = a.replace('\\', "/");
453-
let b_norm = b.replace('\\', "/");
454-
if a_norm == b_norm {
455-
return true;
456-
}
457-
// Check suffix match (one is a suffix of the other), requiring a
458-
// path separator boundary so that e.g. "AFoo.php" does not match "Foo.php".
459-
a_norm.ends_with(&format!("/{}", b_norm)) || b_norm.ends_with(&format!("/{}", a_norm))
460-
}
461-
462396
// ── Tests ───────────────────────────────────────────────────────────
463397

464398
#[cfg(test)]
@@ -491,29 +425,6 @@ mod tests {
491425
assert_eq!(map[Path::new("/proj/src/B.php")][0].range.start.line, 7);
492426
}
493427

494-
// ── paths_match ─────────────────────────────────────────────────
495-
496-
#[test]
497-
fn paths_match_identical() {
498-
assert!(paths_match(
499-
"/home/user/project/src/Foo.php",
500-
"/home/user/project/src/Foo.php"
501-
));
502-
}
503-
504-
#[test]
505-
fn paths_match_suffix() {
506-
assert!(paths_match("/home/user/project/src/Foo.php", "src/Foo.php"));
507-
}
508-
509-
#[test]
510-
fn paths_match_different_files() {
511-
assert!(!paths_match(
512-
"/home/user/project/src/Foo.php",
513-
"src/Bar.php"
514-
));
515-
}
516-
517428
// ── parse_phpcs_json ────────────────────────────────────────────
518429

519430
#[test]

0 commit comments

Comments
 (0)