[clangd] Add a (currently hidden) --strong-workspace-mode flag (#172160)

Its current effect is to use the `rootUri` provided by the client as the
working directory for fallback commands.

Future effects will include other behaviors appropriate for cases
where clangd is being run in the context of editing a single
workspace, such as using the `compile_commands.json` file in the
workspace root for all opened files.

The flag is hidden until other behaviors are implemented and they
constitute a cohesive "mode" for users.

Original PR in #155905, which was reverted due to a UBSan failure
that is fixed in this version.
This commit is contained in:
Dominicentek 2025-12-14 09:24:18 +01:00 committed by GitHub
parent 7db97696a2
commit 4a1b6966ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 92 additions and 16 deletions

View file

@ -554,6 +554,8 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
if (const auto &Dir = Params.initializationOptions.compilationDatabasePath)
CDBOpts.CompileCommandsDir = Dir;
CDBOpts.ContextProvider = Opts.ContextProvider;
if (Opts.StrongWorkspaceMode)
CDBOpts.applyFallbackWorkingDirectory(Opts.WorkspaceRoot);
BaseCDB =
std::make_unique<DirectoryBasedGlobalCompilationDatabase>(CDBOpts);
}

View file

@ -152,6 +152,11 @@ public:
/// FIXME: If not set, should use the current working directory.
std::optional<std::string> WorkspaceRoot;
/// Sets an alternate mode of operation. Current effects are:
/// - Using the current working directory as the working directory for
/// fallback commands
bool StrongWorkspaceMode = false;
/// The resource directory is used to find internal headers, overriding
/// defaults and -resource-dir compiler flag).
/// If std::nullopt, ClangdServer calls

View file

@ -64,7 +64,9 @@ GlobalCompilationDatabase::getFallbackCommand(PathRef File) const {
if (FileExtension.empty() || FileExtension == ".h")
Argv.push_back("-xobjective-c++-header");
Argv.push_back(std::string(File));
tooling::CompileCommand Cmd(llvm::sys::path::parent_path(File),
tooling::CompileCommand Cmd(FallbackWorkingDirectory
? *FallbackWorkingDirectory
: llvm::sys::path::parent_path(File),
llvm::sys::path::filename(File), std::move(Argv),
/*Output=*/"");
Cmd.Heuristic = "clangd fallback";
@ -349,7 +351,8 @@ bool DirectoryBasedGlobalCompilationDatabase::DirectoryCache::load(
DirectoryBasedGlobalCompilationDatabase::
DirectoryBasedGlobalCompilationDatabase(const Options &Opts)
: Opts(Opts), Broadcaster(std::make_unique<BroadcastThread>(*this)) {
: GlobalCompilationDatabase(Opts.FallbackWorkingDirectory), Opts(Opts),
Broadcaster(std::make_unique<BroadcastThread>(*this)) {
if (!this->Opts.ContextProvider)
this->Opts.ContextProvider = [](llvm::StringRef) {
return Context::current().clone();
@ -460,6 +463,21 @@ DirectoryBasedGlobalCompilationDatabase::lookupCDB(
return Result;
}
void DirectoryBasedGlobalCompilationDatabase::Options::
applyFallbackWorkingDirectory(
std::optional<std::string> FallbackWorkingDirectory) {
if (FallbackWorkingDirectory)
this->FallbackWorkingDirectory = *FallbackWorkingDirectory;
else {
// Clangd is running in strong workspace mode but the client didn't
// specify a workspace path in the `initialize` request.
// Fallback to current working directory.
SmallString<256> CWD;
llvm::sys::fs::current_path(CWD);
this->FallbackWorkingDirectory = std::string(CWD);
}
}
// The broadcast thread announces files with new compile commands to the world.
// Primarily this is used to enqueue them for background indexing.
//
@ -759,9 +777,10 @@ DirectoryBasedGlobalCompilationDatabase::getProjectModules(PathRef File) const {
OverlayCDB::OverlayCDB(const GlobalCompilationDatabase *Base,
std::vector<std::string> FallbackFlags,
CommandMangler Mangler)
: DelegatingCDB(Base), Mangler(std::move(Mangler)),
FallbackFlags(std::move(FallbackFlags)) {}
CommandMangler Mangler,
std::optional<std::string> FallbackWorkingDirectory)
: DelegatingCDB(Base, FallbackWorkingDirectory),
Mangler(std::move(Mangler)), FallbackFlags(std::move(FallbackFlags)) {}
std::optional<tooling::CompileCommand>
OverlayCDB::getCompileCommand(PathRef File) const {
@ -844,16 +863,20 @@ OverlayCDB::getProjectModules(PathRef File) const {
return MDB;
}
DelegatingCDB::DelegatingCDB(const GlobalCompilationDatabase *Base)
: Base(Base) {
DelegatingCDB::DelegatingCDB(
const GlobalCompilationDatabase *Base,
std::optional<std::string> FallbackWorkingDirectory)
: GlobalCompilationDatabase(FallbackWorkingDirectory), Base(Base) {
if (Base)
BaseChanged = Base->watch([this](const std::vector<std::string> Changes) {
OnCommandChanged.broadcast(Changes);
});
}
DelegatingCDB::DelegatingCDB(std::unique_ptr<GlobalCompilationDatabase> Base)
: DelegatingCDB(Base.get()) {
DelegatingCDB::DelegatingCDB(
std::unique_ptr<GlobalCompilationDatabase> Base,
std::optional<std::string> FallbackWorkingDirectory)
: DelegatingCDB(Base.get(), FallbackWorkingDirectory) {
BaseOwner = std::move(Base);
}

View file

@ -35,6 +35,9 @@ struct ProjectInfo {
/// Provides compilation arguments used for parsing C and C++ files.
class GlobalCompilationDatabase {
public:
GlobalCompilationDatabase(
std::optional<std::string> FallbackWorkingDirectory = std::nullopt)
: FallbackWorkingDirectory(FallbackWorkingDirectory) {}
virtual ~GlobalCompilationDatabase() = default;
/// If there are any known-good commands for building this file, returns one.
@ -69,14 +72,19 @@ public:
}
protected:
std::optional<std::string> FallbackWorkingDirectory;
mutable CommandChanged OnCommandChanged;
};
// Helper class for implementing GlobalCompilationDatabases that wrap others.
class DelegatingCDB : public GlobalCompilationDatabase {
public:
DelegatingCDB(const GlobalCompilationDatabase *Base);
DelegatingCDB(std::unique_ptr<GlobalCompilationDatabase> Base);
DelegatingCDB(
const GlobalCompilationDatabase *Base,
std::optional<std::string> FallbackWorkingDirectory = std::nullopt);
DelegatingCDB(
std::unique_ptr<GlobalCompilationDatabase> Base,
std::optional<std::string> FallbackWorkingDirectory = std::nullopt);
std::optional<tooling::CompileCommand>
getCompileCommand(PathRef File) const override;
@ -117,6 +125,12 @@ public:
// Only look for a compilation database in this one fixed directory.
// FIXME: fold this into config/context mechanism.
std::optional<Path> CompileCommandsDir;
// Working directory for fallback commands
// If unset, parent directory of file should be used
std::optional<std::string> FallbackWorkingDirectory;
void applyFallbackWorkingDirectory(
std::optional<std::string> FallbackWorkingDirectory);
};
DirectoryBasedGlobalCompilationDatabase(const Options &Opts);
@ -194,9 +208,11 @@ public:
// Base may be null, in which case no entries are inherited.
// FallbackFlags are added to the fallback compile command.
// Adjuster is applied to all commands, fallback or not.
OverlayCDB(const GlobalCompilationDatabase *Base,
std::vector<std::string> FallbackFlags = {},
CommandMangler Mangler = nullptr);
OverlayCDB(
const GlobalCompilationDatabase *Base,
std::vector<std::string> FallbackFlags = {},
CommandMangler Mangler = nullptr,
std::optional<std::string> FallbackWorkingDirectory = std::nullopt);
std::optional<tooling::CompileCommand>
getCompileCommand(PathRef File) const override;

View file

@ -169,6 +169,8 @@ public:
bool buildCommand(const ThreadsafeFS &TFS) {
log("Loading compilation database...");
DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS);
if (Opts.StrongWorkspaceMode)
CDBOpts.applyFallbackWorkingDirectory(Opts.WorkspaceRoot);
CDBOpts.CompileCommandsDir =
Config::current().CompileFlags.CDBSearch.FixedCDBPath;
BaseCDB =
@ -178,8 +180,10 @@ public:
getSystemIncludeExtractor(llvm::ArrayRef(Opts.QueryDriverGlobs));
if (Opts.ResourceDir)
Mangler.ResourceDir = *Opts.ResourceDir;
CDB = std::make_unique<OverlayCDB>(
BaseCDB.get(), std::vector<std::string>{}, std::move(Mangler));
BaseCDB.get(), std::vector<std::string>{}, std::move(Mangler),
CDBOpts.FallbackWorkingDirectory);
if (auto TrueCmd = CDB->getCompileCommand(File)) {
Cmd = std::move(*TrueCmd);
@ -502,7 +506,7 @@ bool check(llvm::StringRef File, const ThreadsafeFS &TFS,
config::DiagnosticCallback Diag) const override {
config::Fragment F;
// If we're timing clang-tidy checks, implicitly disabling the slow ones
// is counterproductive!
// is counterproductive!
if (CheckTidyTime.getNumOccurrences())
F.Diagnostics.ClangTidy.FastCheckFilter.emplace("None");
return {std::move(F).compile(Diag)};

View file

@ -500,6 +500,17 @@ opt<bool> EnableConfig{
init(true),
};
opt<bool> StrongWorkspaceMode{
"strong-workspace-mode",
cat(Features),
desc("An alternate mode of operation for clangd, where the clangd instance "
"is used to edit a single workspace.\n"
"When enabled, fallback commands use the workspace directory as their "
"working directory instead of the parent folder."),
init(false),
Hidden,
};
opt<bool> UseDirtyHeaders{"use-dirty-headers", cat(Misc),
desc("Use files open in the editor when parsing "
"headers instead of reading from the disk"),
@ -907,6 +918,7 @@ clangd accepts flags on the commandline, and in the CLANGD_FLAGS environment var
}
if (!ResourceDir.empty())
Opts.ResourceDir = ResourceDir;
Opts.StrongWorkspaceMode = StrongWorkspaceMode;
Opts.BuildDynamicSymbolIndex = true;
#if CLANGD_ENABLE_REMOTE
if (RemoteIndexAddress.empty() != ProjectRoot.empty()) {

View file

@ -55,6 +55,20 @@ TEST(GlobalCompilationDatabaseTest, FallbackCommand) {
testPath("foo/bar")));
}
TEST(GlobalCompilationDatabaseTest, FallbackWorkingDirectory) {
MockFS TFS;
DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS);
CDBOpts.applyFallbackWorkingDirectory(testPath("foo"));
EXPECT_EQ(CDBOpts.FallbackWorkingDirectory, testPath("foo"));
DirectoryBasedGlobalCompilationDatabase DB(CDBOpts);
auto Cmd = DB.getFallbackCommand(testPath("foo/src/bar.cc"));
EXPECT_EQ(Cmd.Directory, testPath("foo"));
EXPECT_THAT(Cmd.CommandLine,
ElementsAre("clang", testPath("foo/src/bar.cc")));
EXPECT_EQ(Cmd.Output, "");
}
static tooling::CompileCommand cmd(llvm::StringRef File, llvm::StringRef Arg) {
return tooling::CompileCommand(
testRoot(), File, {"clang", std::string(Arg), std::string(File)}, "");