mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(python): add Pyrefly language server (#911)
Pyrefly (`pyrefly lsp`) joins basedpyright, Pyright and ty as a fallback Python language server. It is installable from Tools as a managed virtualenv tool and is listed among the interchangeable type checkers, before Ruff, so Ruff still runs after them. Pyrefly only reports type errors in projects with a Pyrefly configuration (`pyrefly.toml` or `[tool.pyrefly]`); hover, completion and navigation work without one. A real-server test (ignored by default, needs `pyrefly` on PATH) covers startup, go-to-definition and diagnostics. Co-authored-by: pcastro93 <2058578+pcastro93@users.noreply.github.com>
This commit is contained in:
@@ -553,7 +553,7 @@ mod tests {
|
||||
fn alternative_servers_are_fallbacks() {
|
||||
let reg = reg();
|
||||
for (lang, servers) in [
|
||||
("python", vec!["basedpyright", "pyright", "ty"]),
|
||||
("python", vec!["basedpyright", "pyright", "ty", "pyrefly"]),
|
||||
("typescript", vec!["vtsls", "typescript-language-server"]),
|
||||
] {
|
||||
let cfg = reg.get(&LanguageId::new(lang)).unwrap();
|
||||
@@ -610,7 +610,7 @@ mod tests {
|
||||
let cfg = reg.get(&LanguageId::new("python")).unwrap();
|
||||
let names: Vec<&str> = cfg.language_servers.iter().map(|s| s.name.as_str()).collect();
|
||||
// After the fallbacks: hover, rename and symbols stay with the type checker.
|
||||
assert_eq!(names, ["basedpyright", "pyright", "ty", "ruff"]);
|
||||
assert_eq!(names, ["basedpyright", "pyright", "ty", "pyrefly", "ruff"]);
|
||||
let ruff = cfg.language_servers.iter().find(|s| s.name == "ruff").unwrap();
|
||||
assert_eq!((ruff.command.as_str(), ruff.args.as_slice()), ("ruff", ["server".to_string()].as_slice()));
|
||||
assert!(ruff.require_root_marker && !ruff.fallback);
|
||||
|
||||
@@ -250,6 +250,46 @@ async fn ruff_next_to_basedpyright_end_to_end() {
|
||||
tokio::time::timeout(Duration::from_secs(10), mgr.shutdown_all()).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "requires pyrefly on PATH"]
|
||||
async fn pyrefly_python_end_to_end() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dir.path().canonicalize().unwrap();
|
||||
let path = root.join("example.py");
|
||||
let source = "def greet(name: str) -> str:\n return name\n\nresult = greet('Ada')\nbad: int = 'wrong'\n";
|
||||
std::fs::write(&path, source).unwrap();
|
||||
// Pyrefly only reports type errors in projects with a Pyrefly configuration.
|
||||
std::fs::write(root.join("pyrefly.toml"), "").unwrap();
|
||||
let mut python = LanguageRegistry::load(None).unwrap().get(&LanguageId::new("python")).unwrap();
|
||||
python.language_servers.retain(|server| server.name == "pyrefly");
|
||||
assert_eq!(python.language_servers.len(), 1);
|
||||
let (mgr, events) = manager(&root, LanguageRegistry::from_configs(vec![python]));
|
||||
let doc = DocId::next();
|
||||
mgr.did_open(doc, &path, &LanguageId::new("python"), 1, source);
|
||||
retry("pyrefly ready", Duration::from_secs(30), || {
|
||||
let status = last_status(&events, "pyrefly");
|
||||
if let Some((ServerStatus::Crashed | ServerStatus::Missing | ServerStatus::Misconfigured, msg)) = &status {
|
||||
panic!("pyrefly failed: {msg:?}");
|
||||
}
|
||||
async move { matches!(status, Some((ServerStatus::Ready | ServerStatus::Busy, _))).then_some(()) }
|
||||
})
|
||||
.await;
|
||||
let defs = retry("pyrefly definition", Duration::from_secs(30), || {
|
||||
let mgr = mgr.clone();
|
||||
async move { mgr.goto(doc, Pos::new(3, 10), GotoKind::Definition).await.ok().filter(|d| !d.is_empty()) }
|
||||
})
|
||||
.await;
|
||||
assert_eq!(defs[0].path, path);
|
||||
assert_eq!(defs[0].range.start.line, 0);
|
||||
retry("pyrefly diagnostics", Duration::from_secs(30), || {
|
||||
let found = events.lock().iter().any(
|
||||
|event| matches!(event, IdeEvent::Diagnostics { source, items, .. } if source == "pyrefly" && !items.is_empty()),
|
||||
);
|
||||
async move { found.then_some(()) }
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "requires rust-analyzer"]
|
||||
async fn rust_analyzer_end_to_end() {
|
||||
|
||||
@@ -204,7 +204,7 @@ pub fn tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
const PYTHON_SERVERS: [&str; 6] = ["basedpyright", "pyright", "ty", "pylsp", "jedi-language-server", "ruff"];
|
||||
const PYTHON_SERVERS: [&str; 7] = ["basedpyright", "pyright", "ty", "pyrefly", "pylsp", "jedi-language-server", "ruff"];
|
||||
|
||||
fn merge(base: &mut Value, layer: Value) {
|
||||
match (base, layer) {
|
||||
|
||||
@@ -233,6 +233,17 @@ pub static TOOLS: &[ToolSpec] = &[
|
||||
homepage: "https://docs.astral.sh/ty/",
|
||||
needs: &[],
|
||||
},
|
||||
ToolSpec {
|
||||
name: "pyrefly",
|
||||
display: "Pyrefly (Python)",
|
||||
kind: LanguageServer,
|
||||
commands: &["pyrefly"],
|
||||
method: Method::PipVenv { package: "pyrefly" },
|
||||
fallback: None,
|
||||
version: ver("pyrefly"),
|
||||
homepage: "https://pyrefly.org",
|
||||
needs: &[],
|
||||
},
|
||||
ToolSpec {
|
||||
name: "ruff",
|
||||
display: "Ruff",
|
||||
|
||||
@@ -130,6 +130,11 @@ fn everything_missing_on_an_empty_system() {
|
||||
assert_eq!(ty.command, "ty");
|
||||
assert_eq!(ty.install_method.as_deref(), Some("pip (virtualenv)"));
|
||||
assert!(ty.languages.iter().any(|language| language == "Python"));
|
||||
let pyrefly = find(&list, "pyrefly");
|
||||
assert_eq!(pyrefly.status, ToolStatus::Missing);
|
||||
assert_eq!(pyrefly.command, "pyrefly");
|
||||
assert_eq!(pyrefly.install_method.as_deref(), Some("pip (virtualenv)"));
|
||||
assert!(pyrefly.languages.iter().any(|language| language == "Python"));
|
||||
let angular = find(&list, "angular-language-server");
|
||||
assert_eq!(angular.command, "ngserver");
|
||||
assert_eq!(angular.install_method.as_deref(), Some("npm"));
|
||||
@@ -150,7 +155,7 @@ fn everything_missing_on_an_empty_system() {
|
||||
fn interchangeable_servers_list_their_alternatives() {
|
||||
let env = Env::new();
|
||||
let list = env.tools().list(&langs());
|
||||
let python = ["basedpyright", "pyright", "ty"];
|
||||
let python = ["basedpyright", "pyright", "ty", "pyrefly"];
|
||||
for name in python {
|
||||
assert_eq!(find(&list, name).alternatives, python);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,14 @@ args = ["server"]
|
||||
root-markers = ["ty.toml", "pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"]
|
||||
install-hint = "uv tool install ty (or: pip install ty)"
|
||||
|
||||
[[language-servers]]
|
||||
name = "pyrefly"
|
||||
fallback = true
|
||||
command = "pyrefly"
|
||||
args = ["lsp"]
|
||||
root-markers = ["pyrefly.toml", "pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"]
|
||||
install-hint = "uv tool install pyrefly (or: pip install pyrefly)"
|
||||
|
||||
# Lint, format, organize imports and fix all next to the type checker, in
|
||||
# projects configured for Ruff (formatting is Ruff's alone: basedpyright has none).
|
||||
[[language-servers]]
|
||||
|
||||
@@ -89,7 +89,7 @@ describe("Language Servers page settings", () => {
|
||||
const ty = details("ty (Python)");
|
||||
expect(within(ty).getByRole("button", { name: "Language server for Python" })).toHaveTextContent("Automatic");
|
||||
// The language is in the label already, so names drop a "(Python)" suffix.
|
||||
expect(within(ty).getByText(/basedpyright → Pyright → ty\.$/)).toBeInTheDocument();
|
||||
expect(within(ty).getByText(/basedpyright → Pyright → ty → Pyrefly\.$/)).toBeInTheDocument();
|
||||
await user.click(listItem("gopls")!);
|
||||
expect(within(details("gopls")).getByRole("checkbox", { name: /Use gopls/ })).toBeInTheDocument();
|
||||
expect(within(details("gopls")).queryByRole("button", { name: /^Language server for/ })).toBeNull();
|
||||
@@ -108,7 +108,7 @@ describe("Language Servers page settings", () => {
|
||||
expect(picker).toHaveTextContent("ty");
|
||||
await user.click(picker);
|
||||
await user.click(screen.getByRole("option", { name: /Automatic/ }));
|
||||
for (const name of ["basedpyright", "pyright", "ty"]) {
|
||||
for (const name of ["basedpyright", "pyright", "ty", "pyrefly"]) {
|
||||
expect(stagedFor(`languageServers.${name}.enabled`)).toBeNull();
|
||||
expect(useTestForm.getState().values).not.toHaveProperty(`languageServers.${name}.enabled`);
|
||||
}
|
||||
@@ -127,10 +127,11 @@ describe("Language Servers page settings", () => {
|
||||
expect(stagedFor("languageServers.basedpyright.enabled")).toBe(false);
|
||||
expect(stagedFor("languageServers.pyright.enabled")).toBe(true);
|
||||
expect(stagedFor("languageServers.ty.enabled")).toBe(false);
|
||||
expect(stagedFor("languageServers.pyrefly.enabled")).toBe(false);
|
||||
expect(picker).toHaveTextContent("Pyright");
|
||||
await user.click(picker);
|
||||
await user.click(screen.getByRole("option", { name: /Automatic/ }));
|
||||
for (const name of ["basedpyright", "pyright", "ty"]) {
|
||||
for (const name of ["basedpyright", "pyright", "ty", "pyrefly"]) {
|
||||
expect(stagedFor(`languageServers.${name}.enabled`)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,13 +94,20 @@ export function sampleTools(): ToolInfo[] {
|
||||
version: "4.3.3",
|
||||
path: "/opt/homebrew/bin/typescript-language-server",
|
||||
}),
|
||||
...["basedpyright", "pyright", "ty"].map((name) =>
|
||||
...["basedpyright", "pyright", "ty", "pyrefly"].map((name) =>
|
||||
tool({
|
||||
name,
|
||||
displayName: name === "pyright" ? "Pyright" : name === "ty" ? "ty (Python)" : name,
|
||||
displayName:
|
||||
name === "pyright"
|
||||
? "Pyright"
|
||||
: name === "ty"
|
||||
? "ty (Python)"
|
||||
: name === "pyrefly"
|
||||
? "Pyrefly (Python)"
|
||||
: name,
|
||||
languages: ["Python"],
|
||||
installMethod: "uv (virtualenv)",
|
||||
alternatives: ["basedpyright", "pyright", "ty"],
|
||||
alternatives: ["basedpyright", "pyright", "ty", "pyrefly"],
|
||||
}),
|
||||
),
|
||||
tool({ name: "gopls", languages: ["Go"], installMethod: "go install" }),
|
||||
|
||||
@@ -149,6 +149,7 @@ const UNTRANSLATED = new Set<string>([
|
||||
"OpenIDEA",
|
||||
"Spring Boot",
|
||||
"PostgreSQL",
|
||||
"Pyrefly",
|
||||
"Pyright",
|
||||
"pytest",
|
||||
"Rust",
|
||||
|
||||
Reference in New Issue
Block a user