2929 InsertTextFormat ,
3030 Location ,
3131 MarkupContent ,
32+ MessageType ,
3233 Position ,
3334 PublishDiagnosticsParams ,
3435 Range ,
3738 SemanticTokens ,
3839 SemanticTokensLegend ,
3940 SemanticTokensParams ,
41+ ShowMessageParams ,
4042 TextDocumentSyncKind ,
4143 TextEdit ,
4244 WorkspaceEdit ,
4749from .ty import TyClient
4850from .transpiler import Transpiler
4951from .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}
5259KNOWN_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+
14371482def _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