33import logging
44import os
55import pathlib
6+ import re
67import subprocess
78from collections .abc import Hashable
89from typing import Any , cast
910
1011from overrides import override
1112
12- from solidlsp .ls import RawDocumentSymbol , SolidLanguageServer
13+ from solidlsp import ls_types
14+ from solidlsp .ls import DocumentSymbols , LSPFileBuffer , RawDocumentSymbol , SolidLanguageServer , SymbolBodyFactory
1315from solidlsp .ls_config import LanguageServerConfig
1416from solidlsp .lsp_protocol_handler .lsp_types import InitializeParams
1517from solidlsp .lsp_protocol_handler .server import ProcessLaunchInfo
@@ -23,6 +25,12 @@ class Gopls(SolidLanguageServer):
2325 Provides Go specific instantiation of the LanguageServer class using gopls.
2426 """
2527
28+ # matches a line prefix that consists solely of a leading `type`/`var`/`const` declaration
29+ # keyword (with optional indentation and the whitespace before the declared identifier).
30+ # gopls reports the symbol range of such single declarations starting at the identifier,
31+ # i.e. after the keyword, whereas `func` declarations include the `func` keyword.
32+ _LEADING_DECL_KEYWORD_RE = re .compile (r"(?P<indent>\s*)(?:type|var|const)\s+" )
33+
2634 @classmethod
2735 def supports_implementation_request (cls ) -> bool :
2836 return True
@@ -214,6 +222,92 @@ def _document_symbols_cache_fingerprint(self) -> Hashable:
214222 def _normalize_symbol_name (self , symbol : RawDocumentSymbol , relative_file_path : str ) -> str :
215223 return symbol ["name" ].rsplit ("." , 1 )[- 1 ]
216224
225+ @override
226+ def request_document_symbols (self , relative_file_path : str , file_buffer : LSPFileBuffer | None = None ) -> DocumentSymbols :
227+ # Override to extend single `type`/`var`/`const` declaration ranges to include the leading
228+ # keyword. gopls excludes the keyword from such ranges (unlike `func` declarations), which
229+ # causes replace_symbol_body to drop the keyword from the symbol body and replacement range;
230+ # a natural keyword-inclusive round-trip edit would then corrupt the file (e.g. `type Foo`
231+ # becomes `type type Foo`). See _extend_go_symbol_range_to_include_leading_keyword.
232+ document_symbols = super ().request_document_symbols (relative_file_path , file_buffer = file_buffer )
233+ if not document_symbols .root_symbols :
234+ return document_symbols
235+
236+ # obtain the file lines and a body factory to recompute the bodies of extended symbols
237+ with self ._open_file_context (relative_file_path , file_buffer , open_in_ls = False ) as file_data :
238+ file_lines = file_data .split_lines ()
239+ body_factory = SymbolBodyFactory (file_data )
240+
241+ # extend ranges recursively, operating on copies so the cached symbols are not mutated.
242+ # Children keep their `parent` back-pointer aimed at the original (un-extended) node;
243+ # this is intentional and safe, because ancestor traversal only reads name/kind/overload
244+ # index (see Symbol.iter_ancestors), none of which the extension changes. Rebinding the
245+ # back-pointers would force a deep copy of every extended subtree for no observable gain.
246+ def extend_symbol_and_children (symbol : ls_types .UnifiedSymbolInformation ) -> ls_types .UnifiedSymbolInformation :
247+ extended = self ._extend_go_symbol_range_to_include_leading_keyword (symbol , file_lines , body_factory )
248+ children = symbol .get ("children" )
249+ if children :
250+ if extended is symbol :
251+ extended = symbol .copy ()
252+ extended ["children" ] = [extend_symbol_and_children (child ) for child in children ]
253+ return extended
254+
255+ extended_root_symbols = [extend_symbol_and_children (sym ) for sym in document_symbols .root_symbols ]
256+
257+ return DocumentSymbols (extended_root_symbols )
258+
259+ def _extend_go_symbol_range_to_include_leading_keyword (
260+ self ,
261+ symbol : ls_types .UnifiedSymbolInformation ,
262+ file_lines : list [str ],
263+ body_factory : SymbolBodyFactory ,
264+ ) -> ls_types .UnifiedSymbolInformation :
265+ """
266+ Extend a Go symbol's body range to include a leading `type`/`var`/`const` keyword.
267+
268+ gopls reports the range of a single `type`/`var`/`const` declaration starting at the
269+ declared identifier (after the keyword), whereas the range of a `func` declaration includes
270+ the `func` keyword. This asymmetry makes :meth:`replace_symbol_body` omit the keyword from
271+ both the displayed body and the replacement range, so re-supplying the keyword in an edit
272+ corrupts the file (e.g. ``type Foo`` becomes ``type type Foo``).
273+
274+ :param symbol: the symbol whose range may be extended.
275+ :param file_lines: the lines of the file in which the symbol is defined.
276+ :param body_factory: the factory used to recompute the symbol body from the extended range.
277+ :return: a copy of the symbol with an extended range, or the original symbol if no leading
278+ keyword precedes the identifier on the start line (e.g. for funcs or for grouped
279+ declarations such as ``var ( ... )`` whose keyword sits on a separate line).
280+ """
281+ # determine whether only a declaration keyword precedes the identifier on the start line
282+ range_info = symbol ["range" ]
283+ start_line = range_info ["start" ]["line" ]
284+ start_char = range_info ["start" ]["character" ]
285+ if start_line >= len (file_lines ):
286+ return symbol
287+ prefix = file_lines [start_line ][:start_char ]
288+ match = self ._LEADING_DECL_KEYWORD_RE .fullmatch (prefix )
289+ if match is None :
290+ return symbol
291+
292+ # extend the range start back to the keyword (excluding indentation), updating both the
293+ # symbol range and its location range so the replacement range covers the keyword
294+ new_start = ls_types .Position (line = start_line , character = len (match .group ("indent" )))
295+ extended = symbol .copy ()
296+ extended ["range" ] = ls_types .Range (start = new_start , end = range_info ["end" ])
297+ location = extended .get ("location" )
298+ if location :
299+ location = location .copy ()
300+ if "range" in location :
301+ location ["range" ] = ls_types .Range (start = new_start , end = location ["range" ]["end" ])
302+ extended ["location" ] = location
303+
304+ # recompute the body from the now-extended location range so the displayed body stays
305+ # consistent with the replacement range; the stale body must be removed first, since the
306+ # factory returns an existing SymbolBody as-is and otherwise reads the updated location range
307+ extended .pop ("body" , None )
308+ extended ["body" ] = body_factory .create_symbol_body (extended )
309+ return extended
310+
217311 def _start_server (self ) -> None :
218312 """Start gopls server process"""
219313
0 commit comments