From ca8729ae331bac2ae9acdc20cbc2d0e159382efe Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 3 Jan 2025 21:50:24 +0000 Subject: [PATCH] Improve rust project detection Rust projects don't just have a single Cargo.toml. They can have a deep folder structure with multiple crates within. Cargo.lock is a better indicator of the root of the project. Bit even then we probably want to find the _furthest_ away Cargo.foo not the nearest, as that's more likely to be the real project root. Implement that somewhat generically so that other completers can use it. Java already has a similar codepath but it's more complicated so not touching it. Finally, even with the above 2 changes, we still have a problem because there might jjust be gaps. Taking wasmtime project for example there are: ./Cargo.toml ./Cargo.lock ./src/foo.rs ./crates/bar/Cargo.toml ./crates/bar/src/bar.rs ./crates/baz/Cargo.toml ./crates/baz/src/baz.rs So, we allow for the top-level 'project_directory' setting to take precedence _if_ the file opened is in a subdirectory of it. --- .../language_server_completer.py | 48 +++++++++++++++++-- ycmd/completers/rust/rust_completer.py | 43 +++++++++++++++-- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/ycmd/completers/language_server/language_server_completer.py b/ycmd/completers/language_server/language_server_completer.py index d8a7ee7222..b00148377a 100644 --- a/ycmd/completers/language_server/language_server_completer.py +++ b/ycmd/completers/language_server/language_server_completer.py @@ -2350,6 +2350,44 @@ def GetProjectDirectory( self, request_data ): return os.path.dirname( filepath ) + def FindProjectFromRootFiles( self, + filepath, + project_root_files, + nearest=True ): + + project_folder = None + project_root_type = None + + # First, find the nearest dir that has one of the root file types + for folder in utils.PathsToAllParentFolders( filepath ): + f = Path( folder ) + for root_file in project_root_files: + if next( f.glob( root_file ), [] ): + # Found one, store the root file and the current nearest folder + project_root_type = root_file + project_folder = folder + break + if project_folder: + break + + if not project_folder: + return None + + # If asking for the nearest, return the one found + if nearest: + return str( project_folder ) + + # Otherwise keep searching up from the nearest until we don't find any more + for folder in utils.PathsToAllParentFolders( os.path.join( project_folder, + '..' ) ): + f = Path( folder ) + if next( f.glob( project_root_type ), [] ): + project_folder = folder + else: + break + return project_folder + + def GetWorkspaceForFilepath( self, filepath, strict = False ): """Return the workspace of the provided filepath. This could be a subproject or a completely unrelated project to the root directory. @@ -2360,12 +2398,12 @@ def GetWorkspaceForFilepath( self, filepath, strict = False ): reuse this implementation. """ project_root_files = self.GetProjectRootFiles() + workspace = None if project_root_files: - for folder in utils.PathsToAllParentFolders( filepath ): - for root_file in project_root_files: - if next( Path( folder ).glob( root_file ), [] ): - return folder - return None if strict else os.path.dirname( filepath ) + workspace = self.FindProjectFromRootFiles( filepath, + project_root_files, + nearest = True ) + return workspace or ( None if strict else os.path.dirname( filepath ) ) def _SendInitialize( self, request_data ): diff --git a/ycmd/completers/rust/rust_completer.py b/ycmd/completers/rust/rust_completer.py index 75bc2c23f5..0e31989d8a 100644 --- a/ycmd/completers/rust/rust_completer.py +++ b/ycmd/completers/rust/rust_completer.py @@ -18,6 +18,7 @@ import logging import os from subprocess import PIPE +from pathlib import Path from ycmd import responses, utils from ycmd.completers.language_server import language_server_completer @@ -106,12 +107,44 @@ def GetServerEnvironment( self ): return env - def GetProjectRootFiles( self ): - # Without LSP workspaces support, RA relies on the rootUri to detect a + def GetWorkspaceForFilepath( self, filepath, strict = False ): + # For every unique workspace, rust analyzer launches a nuclear + # weapon^h^h^h^h new server and indexes the internet. Try to minimise the + # number of such launches. + + # If filepath is a subdirectory of the manually-specified project root, use + # the project root + if 'project_directory' in self._settings: + project_root = utils.AbsolutePath( self._settings[ 'project_directory' ], + self._extra_conf_dir ) + + prp = Path( project_root ) + for parent in Path( filepath ).absolute().parents: + if parent == prp: + return project_root + + # Otherwise, we might not have one configured, or it' a totally different # project. - # TODO: add support for LSP workspaces to allow users to change project - # without having to restart RA. - return [ 'Cargo.toml' ] + # + # Our main heuristic is: + # - find the nearest Cargo.lock, and assume that's the root + # - otherwise find the _furthest_ Cargo.toml and assume that's the root + # - otherwise use the project root directory that we previously calculated. + # + # We never use the directory of the file as that could just be anything + # random, and we might as well just use the original project in that case + if candidate := self.FindProjectFromRootFiles( filepath, + [ 'Cargo.lock' ], + nearest = True ): + return candidate + + if candidate := self.FindProjectFromRootFiles( filepath, + [ 'Cargo.toml' ], + nearest = False ): + return candidate + + # Never use the + return None if strict else self._project_directory def ServerIsReady( self ):