Skip to content

Commit 5dc18a6

Browse files
committed
initial pyrefly support
1 parent daa1f9d commit 5dc18a6

5 files changed

Lines changed: 1332 additions & 0 deletions

File tree

lsp_types/pyrefly/__init__.py

Whitespace-only changes.

lsp_types/pyrefly/config_schema.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Pyrefly configuration schema
2+
# Based on CLI options documented in PYREFLY_GUIDE.md
3+
# Note: Pyrefly configuration is still evolving, this is a minimal implementation
4+
5+
from __future__ import annotations
6+
7+
from typing import Literal, NotRequired, TypedDict
8+
9+
# Basic configuration options based on Pyrefly CLI
10+
IndexingMode = Literal["none", "lazy-non-blocking-background", "lazy-blocking"]
11+
12+
13+
class Model(TypedDict):
14+
"""
15+
Pyrefly Configuration Schema
16+
17+
Based on available CLI options and environment variables.
18+
Note: Pyrefly's configuration format is still evolving.
19+
"""
20+
21+
# Core options
22+
verbose: NotRequired[bool]
23+
threads: NotRequired[int] # 0 = auto, 1 = sequential, higher = parallel
24+
color: NotRequired[Literal["auto", "always", "never"]]
25+
26+
# LSP server options
27+
indexing_mode: NotRequired[IndexingMode] # Indexing strategy for LSP server
28+
29+
# File inclusion/exclusion (basic patterns)
30+
include: NotRequired[list[str]]
31+
exclude: NotRequired[list[str]]
32+
33+
# Environment variables that can be configured
34+
pyrefly_threads: NotRequired[int]
35+
pyrefly_color: NotRequired[str]
36+
pyrefly_verbose: NotRequired[bool]

lsp_types/pyrefly/session.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
from __future__ import annotations
2+
3+
import typing as t
4+
from pathlib import Path
5+
6+
import lsp_types
7+
from lsp_types.pool import LSPProcessPool
8+
from lsp_types.process import LSPProcess, ProcessLaunchInfo
9+
10+
from .config_schema import Model as PyreflyConfig
11+
12+
13+
class PyreflySession(lsp_types.Session):
14+
"""
15+
Pyrefly LSP session implementation with process pooling support.
16+
17+
Pyrefly is Facebook's fast Python type checker written in Rust with built-in LSP support.
18+
"""
19+
20+
@classmethod
21+
async def create(
22+
cls,
23+
*,
24+
base_path: Path = Path("."),
25+
initial_code: str = "",
26+
options: PyreflyConfig = {},
27+
pool: LSPProcessPool | None = None,
28+
) -> t.Self:
29+
"""Create a new Pyrefly session using a process pool."""
30+
base_path = base_path.resolve()
31+
base_path_str = str(base_path)
32+
33+
# Create pyrefly.toml config file (Pyrefly's configuration format)
34+
config_path = base_path / "pyrefly.toml"
35+
36+
# Convert options to TOML format (basic implementation)
37+
# Note: Pyrefly's config format is still evolving, so we keep it minimal
38+
toml_content = ""
39+
if options.get("verbose"):
40+
toml_content += "verbose = true\n"
41+
if "threads" in options and options["threads"] is not None:
42+
toml_content += f"threads = {options['threads']}\n"
43+
if "color" in options:
44+
toml_content += f"color = \"{options['color']}\"\n"
45+
if "indexing_mode" in options:
46+
toml_content += f"indexing-mode = \"{options['indexing_mode']}\"\n"
47+
48+
config_path.write_text(toml_content)
49+
50+
async def create_lsp_process():
51+
# Build command args for Pyrefly LSP server
52+
cmd_args = ["pyrefly", "lsp"]
53+
54+
# Add CLI options based on configuration
55+
if options.get("verbose"):
56+
cmd_args.append("--verbose")
57+
if "threads" in options and options["threads"] is not None:
58+
cmd_args.extend(["--threads", str(options["threads"])])
59+
if "indexing_mode" in options:
60+
cmd_args.extend(["--indexing-mode", options["indexing_mode"]])
61+
62+
# NOTE: requires pyrefly to be installed and accessible
63+
proc_info = ProcessLaunchInfo(cmd=cmd_args)
64+
lsp_process = LSPProcess(proc_info)
65+
await lsp_process.start()
66+
67+
# Initialize LSP connection with capabilities similar to Pyright
68+
await lsp_process.send.initialize(
69+
{
70+
"processId": None,
71+
"rootUri": f"file://{base_path}",
72+
"rootPath": base_path_str,
73+
"capabilities": {
74+
"textDocument": {
75+
"publishDiagnostics": {
76+
"versionSupport": True,
77+
"tagSupport": {
78+
"valueSet": [
79+
lsp_types.DiagnosticTag.Unnecessary,
80+
lsp_types.DiagnosticTag.Deprecated,
81+
]
82+
},
83+
},
84+
"hover": {
85+
"contentFormat": [
86+
lsp_types.MarkupKind.Markdown,
87+
lsp_types.MarkupKind.PlainText,
88+
],
89+
},
90+
"signatureHelp": {},
91+
"completion": {},
92+
"definition": {},
93+
"references": {},
94+
}
95+
},
96+
}
97+
)
98+
99+
# Pyrefly requires initialized event
100+
await lsp_process.notify.initialized({})
101+
102+
return lsp_process
103+
104+
# Use pool if provided, otherwise create a default non-pooling pool
105+
if pool is None:
106+
pool = LSPProcessPool(max_size=0) # No recycling, immediate shutdown
107+
108+
lsp_process = await pool.acquire(create_lsp_process, base_path_str)
109+
pyrefly_session = cls(lsp_process, pool=pool)
110+
111+
# Update settings via didChangeConfiguration
112+
# Pyrefly might not support all workspace configuration changes yet
113+
await lsp_process.notify.workspace_did_change_configuration(
114+
{"settings": options}
115+
)
116+
117+
# Simulate opening a document
118+
await pyrefly_session._open_document(initial_code)
119+
120+
return pyrefly_session
121+
122+
def __init__(
123+
self,
124+
lsp_process: LSPProcess,
125+
*,
126+
pool: LSPProcessPool,
127+
):
128+
self._process = lsp_process
129+
self._document_uri = "file:///test.py"
130+
self._document_version = 1
131+
self._document_text = ""
132+
133+
self._pool = pool
134+
135+
# region - Session methods
136+
137+
async def shutdown(self) -> None:
138+
"""Shutdown and recycle the session back to the pool"""
139+
if self._pool is None:
140+
return # Already recycled
141+
142+
# Release back to pool (document cleanup handled by pool/process reset)
143+
# For max_size=0 pools, this will immediately shutdown the process
144+
await self._pool.release(self._process)
145+
146+
# Clear references to prevent further use
147+
self._pool = None
148+
149+
async def update_code(self, code: str) -> int:
150+
"""Update the code in the current document"""
151+
self._document_version += 1
152+
self._document_text = code
153+
154+
document_version = self._document_version
155+
await self._process.notify.did_change_text_document(
156+
{
157+
"textDocument": {
158+
"uri": self._document_uri,
159+
"version": self._document_version,
160+
},
161+
"contentChanges": [{"text": code}],
162+
}
163+
)
164+
165+
return document_version
166+
167+
async def get_diagnostics(self):
168+
"""Get diagnostics for the given code"""
169+
# FIXME: riddled with race conditions
170+
# As a bare minimum, cache the diagnostics per document version
171+
# When diagnostics are requested twice, it would hang otherwise
172+
return await self._process.notify.on_publish_diagnostics()
173+
174+
async def get_hover_info(
175+
self, position: lsp_types.Position
176+
) -> lsp_types.Hover | None:
177+
"""Get hover information at the given position"""
178+
return await self._process.send.hover(
179+
{"textDocument": {"uri": self._document_uri}, "position": position}
180+
)
181+
182+
async def get_rename_edits(
183+
self, position: lsp_types.Position, new_name: str
184+
) -> lsp_types.WorkspaceEdit | None:
185+
"""Get rename edits for the given position"""
186+
return await self._process.send.rename(
187+
{
188+
"textDocument": {"uri": self._document_uri},
189+
"position": position,
190+
"newName": new_name,
191+
}
192+
)
193+
194+
async def get_signature_help(
195+
self, position: lsp_types.Position
196+
) -> lsp_types.SignatureHelp | None:
197+
"""Get signature help at the given position"""
198+
return await self._process.send.signature_help(
199+
{"textDocument": {"uri": self._document_uri}, "position": position}
200+
)
201+
202+
async def get_completion(
203+
self, position: lsp_types.Position
204+
) -> lsp_types.CompletionList | list[lsp_types.CompletionItem] | None:
205+
"""Get completion items at the given position"""
206+
return await self._process.send.completion(
207+
{"textDocument": {"uri": self._document_uri}, "position": position}
208+
)
209+
210+
async def resolve_completion(
211+
self, completion_item: lsp_types.CompletionItem
212+
) -> lsp_types.CompletionItem:
213+
"""Resolve the given completion item"""
214+
return await self._process.send.resolve_completion_item(completion_item)
215+
216+
async def get_semantic_tokens(self) -> lsp_types.SemanticTokens | None:
217+
"""Get semantic tokens for the current document"""
218+
return await self._process.send.semantic_tokens_full(
219+
{"textDocument": {"uri": self._document_uri}}
220+
)
221+
222+
# endregion
223+
224+
# Private methods
225+
226+
async def _open_document(self, code: str) -> None:
227+
"""Open a document with the given code"""
228+
self._document_text = code
229+
await self._process.notify.did_open_text_document(
230+
{
231+
"textDocument": {
232+
"languageId": lsp_types.LanguageKind.Python,
233+
"version": self._document_version,
234+
"uri": self._document_uri,
235+
"text": code,
236+
}
237+
}
238+
)
239+
# Track the opened document
240+
self._process.track_document_open(self._document_uri)

0 commit comments

Comments
 (0)