diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index ca62a79f..0fad4728 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -821,8 +821,10 @@ class LanguageServer: def turn_item_into_symbol_with_children(item: GenericDocumentSymbol): item = cast(multilspy_types.UnifiedSymbolInformation, item) + absolute_path = os.path.join(self.repository_root_path, relative_file_path) + + # handle missing entries in location if "location" not in item: - absolute_path = os.path.join(self.repository_root_path, relative_file_path) uri = pathlib.Path(absolute_path).as_uri() assert "range" in item tree_location = multilspy_types.Location( @@ -832,8 +834,19 @@ class LanguageServer: relativePath=relative_file_path, ) item['location'] = tree_location + location = item["location"] + if "absolutePath" not in location: + location["absolutePath"] = absolute_path + if "relativePath" not in location: + location["relativePath"] = relative_file_path if include_body: item['body'] = self.retrieve_symbol_body(item) + # handle missing selectionRange + if "selectionRange" not in item: + if "range" in item: + item["selectionRange"] = item["range"] + else: + item["selectionRange"] = item["location"]["range"] item[LSPConstants.CHILDREN] = item.get(LSPConstants.CHILDREN, []) flat_all_symbol_list: List[multilspy_types.UnifiedSymbolInformation] = [] @@ -1044,15 +1057,20 @@ class LanguageServer: Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. """ _, document_roots = await self.request_document_symbols(relative_file_path) - return [ - ( - root["name"], - root["kind"], - root["selectionRange"]["start"]["line"], # type: ignore - root["selectionRange"]["start"]["character"] # type: ignore - ) - for root in document_roots - ] + result = [] + for root in document_roots: + try: + result.append( + ( root["name"], + root["kind"], + root["selectionRange"]["start"]["line"], + root["selectionRange"]["start"]["character"],) + ) + except KeyError as e: + raise KeyError( + f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}" + ) from e + return result async def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: """ diff --git a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py index 17253c4a..8565825e 100644 --- a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py @@ -64,13 +64,19 @@ class ClangdLanguageServer(LanguageServer): clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static/clangd") clangd_executable_path = os.path.join(clangd_ls_dir, "clangd_19.1.2", "bin", dependency["binaryName"]) - if not os.path.exists(clangd_ls_dir): - os.makedirs(clangd_ls_dir) + if not os.path.exists(clangd_executable_path): + clangd_url = dependency["url"] + logger.log(f"Clangd executable not found at {clangd_executable_path}. Downloading from {clangd_url}", logging.INFO) + os.makedirs(clangd_ls_dir, exist_ok=True) if dependency["archiveType"] == "zip": FileUtils.download_and_extract_archive( - logger, dependency["url"], clangd_ls_dir, dependency["archiveType"] + logger, clangd_url, clangd_ls_dir, dependency["archiveType"] ) - assert os.path.exists(clangd_executable_path) + if not os.path.exists(clangd_executable_path): + raise FileNotFoundError( + f"Clangd executable not found at {clangd_executable_path}.\n" + "Make sure you have installed clangd. See https://clangd.llvm.org/installation" + ) os.chmod(clangd_executable_path, stat.S_IEXEC) return clangd_executable_path diff --git a/src/multilspy/multilspy_utils.py b/src/multilspy/multilspy_utils.py index f7365b5f..7c46d37d 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/multilspy/multilspy_utils.py @@ -117,7 +117,10 @@ class FileUtils: """ Reads the file at the given path and returns the contents as a string. """ - encodings = ["utf-8-sig", "utf-16"] + if not os.path.exists(file_path): + logger.log(f"File read '{file_path}' failed: File does not exist.", logging.ERROR) + raise MultilspyException(f"File read '{file_path}' failed: File does not exist.") + encodings = ["utf-8-sig", "utf-16", "utf-8", "latin-1"] try: for encoding in encodings: try: @@ -125,6 +128,9 @@ class FileUtils: return inp_file.read() except UnicodeError: continue + # Try system default encoding as a last resort + with open(file_path, "r") as inp_file: + return inp_file.read() except Exception as exc: logger.log(f"File read '{file_path}' failed: {exc}", logging.ERROR) raise MultilspyException("File read failed.") from None diff --git a/src/serena/agent.py b/src/serena/agent.py index df7099fd..fe8028af 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -73,7 +73,10 @@ class ProjectConfig(ToStringMixin): def __init__(self, config_dict: dict[str, Any], project_name: str, project_root: Path | None = None): self.project_name: str = project_name - self.language: Language = Language(config_dict["language"]) + try: + self.language: Language = Language(config_dict["language"].lower()) + except ValueError as e: + raise ValueError(f"Invalid language: {config_dict['language']}.\nValid languages are: {[l.value for l in Language]}") from e if project_root is None: project_root = Path(config_dict["project_root"]) self.project_root: str = str(project_root.resolve())