Skip to content

Commit 8fbf50a

Browse files
feat: Implement LSP definition and reference fallbacks, enhance transpiler mapping with pywire parser integration, and remove old adhoc repro scripts.
1 parent b2d93da commit 8fbf50a

12 files changed

Lines changed: 574 additions & 1476 deletions

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dev = [
2424
"nox",
2525
"ruff>=0.1.0",
2626
"ty",
27+
"pywire",
2728
]
2829

2930
[tool.pytest.ini_options]

src/pywire_language_server/server.py

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
InsertTextFormat,
3030
Location,
3131
MarkupContent,
32+
MessageType,
3233
Position,
3334
PublishDiagnosticsParams,
3435
Range,
@@ -37,6 +38,7 @@
3738
SemanticTokens,
3839
SemanticTokensLegend,
3940
SemanticTokensParams,
41+
ShowMessageParams,
4042
TextDocumentSyncKind,
4143
TextEdit,
4244
WorkspaceEdit,
@@ -47,6 +49,11 @@
4749
from .ty import TyClient
4850
from .transpiler import Transpiler
4951
from .sourcemap import SourceMap
52+
try:
53+
import pywire
54+
HAS_PYWIRE = True
55+
except ImportError:
56+
HAS_PYWIRE = False
5057

5158
# Valid block keywords: used in {$keyword ...} and {/keyword}
5259
KNOWN_BLOCKS = {"if", "elif", "else", "for", "await", "then", "catch", "try", "except", "finally"}
@@ -87,6 +94,8 @@ def get_shadow_uri(self, doc_uri: str) -> Optional[str]:
8794
doc_path = self._uri_to_path(doc_uri)
8895
if not doc_path:
8996
return None
97+
if doc_path.endswith(".wire"):
98+
return f"file://{doc_path[:-5]}_wire.py"
9099
return f"file://{doc_path}.py"
91100

92101
def get_stub_uri(self, doc_uri: str) -> Optional[str]:
@@ -97,19 +106,19 @@ def get_stub_uri(self, doc_uri: str) -> Optional[str]:
97106
if not doc_path:
98107
return None
99108
if doc_path.endswith(".wire"):
100-
return f"file://{doc_path[:-5]}.pyi"
109+
return f"file://{doc_path[:-5]}_wire.pyi"
101110
return f"file://{doc_path}.pyi"
102111

103112
def get_original_uri(self, shadow_uri: str) -> Optional[str]:
104113
"""Map back from virtual .py or .pyi URI to original .wire URI."""
105114
# Handle both file:// and raw paths
106115
uri = shadow_uri if shadow_uri.startswith("file://") else f"file://{shadow_uri}"
107116

108-
# 1. Direct shadow matches (.wire.py)
109-
if uri.endswith(".wire.py"):
110-
return uri[:-3]
111-
if uri.endswith(".wire.pyi"):
112-
return uri[:-4]
117+
# 1. Direct shadow matches (_wire.py)
118+
if uri.endswith("_wire.py"):
119+
return uri[:-8] + ".wire"
120+
if uri.endswith("_wire.pyi"):
121+
return uri[:-9] + ".wire"
113122

114123
# 2. Stub matches (foo.pyi -> foo.wire)
115124
if uri.endswith(".pyi"):
@@ -126,6 +135,7 @@ def get_original_uri(self, shadow_uri: str) -> Optional[str]:
126135
if norm_uri in [k.lower() for k in self.source_maps.keys()]:
127136
# If it's a known shadow, we can try to guess back
128137
if uri.endswith(".py"):
138+
# For our new pattern, if it got here and ends with .py but wasn't _wire.py
129139
return uri[:-3]
130140
if uri.endswith(".pyi"):
131141
return uri[:-4] + ".wire"
@@ -224,6 +234,13 @@ async def initialize(ls: LanguageServer, params: Any):
224234
global virtual_manager, ty_client
225235
logger.info("PyWire Language Server initializing...")
226236

237+
if not HAS_PYWIRE:
238+
ls.window_show_message(ShowMessageParams(
239+
message="PyWire Language Server: 'pywire' package not found in current environment. Please install it for full functionality.",
240+
type=MessageType.Error
241+
))
242+
logger.error("pywire package not found. Tree-sitter parsing will be unavailable.")
243+
227244
root_uri = params.root_uri or (
228245
params.workspace_folders[0].uri if params.workspace_folders else None
229246
)
@@ -1434,6 +1451,34 @@ def _map_diagnostic(diag: Dict[str, Any], source_map: SourceMap) -> Optional[Dia
14341451
code=diag.get("code"),
14351452
)
14361453

1454+
def _fuzzy_to_original(
1455+
source_map: Any, line: int, col: int
1456+
) -> Optional[Tuple[int, int]]:
1457+
mapped = source_map.to_original(line, col)
1458+
if mapped:
1459+
return mapped
1460+
1461+
best: Optional[Tuple[int, int]] = None
1462+
best_distance = 10**9
1463+
for mapping in source_map.mappings:
1464+
if mapping.generated_line != line:
1465+
continue
1466+
if col < mapping.generated_col:
1467+
distance = mapping.generated_col - col
1468+
candidate_col = mapping.original_col
1469+
elif col > mapping.generated_col + mapping.length:
1470+
distance = col - (mapping.generated_col + mapping.length)
1471+
candidate_col = mapping.original_col + mapping.length
1472+
else:
1473+
distance = 0
1474+
candidate_col = mapping.original_col + (col - mapping.generated_col)
1475+
if distance < best_distance:
1476+
best_distance = distance
1477+
best = (mapping.original_line, candidate_col)
1478+
if distance == 0:
1479+
break
1480+
return best
1481+
14371482
def _map_location_to_original(loc: Dict[str, Any]) -> Location:
14381483
"""Map a virtual location back to an original .wire location if applicable."""
14391484
if "targetUri" in loc:
@@ -1459,8 +1504,8 @@ def _map_location_to_original(loc: Dict[str, Any]) -> Location:
14591504
start = loc_range["start"]
14601505
end = loc_range.get("end", start)
14611506

1462-
orig_start = target_map.to_original(start["line"], start["character"])
1463-
orig_end = target_map.to_original(end["line"], end["character"])
1507+
orig_start = _fuzzy_to_original(target_map, start["line"], start["character"])
1508+
orig_end = _fuzzy_to_original(target_map, end["line"], end["character"])
14641509

14651510
# Create a copy of the range to modify
14661511
new_range = {
@@ -1712,7 +1757,6 @@ async def hover(ls: LanguageServer, params: HoverParams) -> Optional[Hover]:
17121757
"textDocument": {"uri": shadow_uri},
17131758
"position": {"line": gen_line, "character": gen_col},
17141759
}
1715-
17161760
result = await ty_client.send_request(
17171761
"textDocument/hover", shadow_params
17181762
)
@@ -1928,6 +1972,10 @@ async def references(
19281972

19291973
if ty_client:
19301974
gen_loc = source_map.to_generated(position.line, position.character)
1975+
if not gen_loc:
1976+
gen_loc = source_map.nearest_generated_on_line(
1977+
position.line, position.character
1978+
)
19311979
if gen_loc:
19321980
gen_line, gen_col = gen_loc
19331981
shadow_uri = virtual_manager.get_shadow_uri(uri)
@@ -2120,6 +2168,10 @@ async def definition(
21202168

21212169
# Map to virtual python
21222170
gen_pos = doc.map_to_generated(position.line, position.character)
2171+
if not gen_pos:
2172+
gen_pos = doc.source_map.nearest_generated_on_line(
2173+
position.line, position.character
2174+
)
21232175
if not gen_pos:
21242176
return None
21252177

0 commit comments

Comments
 (0)