Skip to content

Commit

Permalink
Improve rust project detection
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
puremourning committed Jan 3, 2025
1 parent a51329a commit ca8729a
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 10 deletions.
48 changes: 43 additions & 5 deletions ycmd/completers/language_server/language_server_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 2385 in ycmd/completers/language_server/language_server_completer.py

View check run for this annotation

Codecov / codecov/patch

ycmd/completers/language_server/language_server_completer.py#L2385

Added line #L2385 was not covered by tests
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.
Expand All @@ -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 ):
Expand Down
43 changes: 38 additions & 5 deletions ycmd/completers/rust/rust_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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' ],

Check warning on line 118 in ycmd/completers/rust/rust_completer.py

View check run for this annotation

Codecov / codecov/patch

ycmd/completers/rust/rust_completer.py#L118

Added line #L118 was not covered by tests
self._extra_conf_dir )

prp = Path( project_root )
for parent in Path( filepath ).absolute().parents:
if parent == prp:
return project_root

Check warning on line 124 in ycmd/completers/rust/rust_completer.py

View check run for this annotation

Codecov / codecov/patch

ycmd/completers/rust/rust_completer.py#L121-L124

Added lines #L121 - L124 were not covered by tests

# 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 ):
Expand Down

0 comments on commit ca8729a

Please sign in to comment.