From 6181259cf10d428745e70af43feb40b4b58a3939 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 26 Jun 2026 13:16:28 -0700 Subject: [PATCH] Language server: prelude support. (#7417) Support multi-file compilation, and in particular imports of files from the prelude, in `carbon language_server`. In order to properly interface with `CompileDriver`, also switch over to building a proper VFS from the documents we're given. Assisted-by: Gemini via Antigravity --- toolchain/driver/BUILD | 57 +++++- toolchain/driver/compile_driver.cpp | 18 +- toolchain/driver/compile_driver.h | 14 +- toolchain/driver/compile_options.cpp | 2 +- toolchain/driver/config_subcommand.cpp | 4 +- toolchain/driver/driver.cpp | 5 +- toolchain/driver/driver_env.h | 18 +- toolchain/driver/driver_subcommand.h | 10 - toolchain/driver/format_subcommand.cpp | 4 +- .../driver/language_server_subcommand.cpp | 2 +- toolchain/language_server/BUILD | 2 + toolchain/language_server/context.cpp | 181 ++++++++++++------ toolchain/language_server/context.h | 36 ++-- .../text_document/import_prelude.carbon | 91 +++++++++ .../text_document/multiple_files.carbon | 96 ++++++++++ 15 files changed, 430 insertions(+), 110 deletions(-) create mode 100644 toolchain/language_server/testdata/text_document/import_prelude.carbon create mode 100644 toolchain/language_server/testdata/text_document/multiple_files.carbon diff --git a/toolchain/driver/BUILD b/toolchain/driver/BUILD index 393f94dfc114..663e71a6fc18 100644 --- a/toolchain/driver/BUILD +++ b/toolchain/driver/BUILD @@ -126,6 +126,57 @@ cc_library( ], ) +cc_library( + name = "compile_driver", + srcs = [ + "compile_driver.cpp", + "compile_options.cpp", + ], + hdrs = [ + "compile_driver.h", + "compile_options.h", + "driver_env.h", + ], + deps = [ + ":codegen_options", + ":runtimes_cache", + "//common:check", + "//common:command_line", + "//common:error", + "//common:ostream", + "//common:pretty_stack_trace_function", + "//common:raw_string_ostream", + "//common:version", + "//common:vlog", + "//toolchain/base:clang_invocation", + "//toolchain/base:install_paths", + "//toolchain/base:shared_value_stores", + "//toolchain/base:timings", + "//toolchain/check", + "//toolchain/codegen", + "//toolchain/diagnostics:emitter", + "//toolchain/diagnostics:format_providers", + "//toolchain/diagnostics:sorting_consumer", + "//toolchain/lex", + "//toolchain/lower", + "//toolchain/lower:options", + "//toolchain/parse", + "//toolchain/parse:tree", + "//toolchain/sem_ir:file", + "//toolchain/sem_ir:typed_insts", + "//toolchain/source:source_buffer", + "@llvm-project//clang:codegen", + "@llvm-project//clang:frontend", + "@llvm-project//clang:lex", + "@llvm-project//llvm:Core", + "@llvm-project//llvm:MC", + "@llvm-project//llvm:Passes", + "@llvm-project//llvm:Support", + "@llvm-project//llvm:Target", + "@llvm-project//llvm:TargetParser", + ], +) + cc_library( name = "driver", srcs = [ @@ -135,16 +186,11 @@ cc_library( "build_subcommand.h", "clang_subcommand.cpp", "clang_subcommand.h", - "compile_driver.cpp", - "compile_driver.h", - "compile_options.cpp", - "compile_options.h", "compile_subcommand.cpp", "compile_subcommand.h", "config_subcommand.cpp", "config_subcommand.h", "driver.cpp", - "driver_env.h", "driver_subcommand.cpp", "format_subcommand.cpp", "format_subcommand.h", @@ -172,6 +218,7 @@ cc_library( deps = [ ":clang_runner", ":codegen_options", + ":compile_driver", ":lld_runner", ":llvm_runner", ":runtimes_cache", diff --git a/toolchain/driver/compile_driver.cpp b/toolchain/driver/compile_driver.cpp index c6d27c44de10..4644fd2d30cc 100644 --- a/toolchain/driver/compile_driver.cpp +++ b/toolchain/driver/compile_driver.cpp @@ -23,6 +23,7 @@ #include "llvm/Passes/OptimizationLevel.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/StandardInstrumentations.h" +#include "llvm/Support/SaveAndRestore.h" #include "toolchain/base/clang_invocation.h" #include "toolchain/base/timings.h" #include "toolchain/check/check.h" @@ -403,7 +404,7 @@ auto CompilationUnit::RunCodeGenHelper() -> bool { return true; } -auto CompilationUnit::GetParseTreeAndSubtrees() +auto CompilationUnit::GetParseTreeAndSubtrees() const -> const Parse::TreeAndSubtrees& { if (!parse_tree_and_subtrees_) { parse_tree_and_subtrees_ = Parse::TreeAndSubtrees(*tokens_, *parse_tree_); @@ -475,7 +476,7 @@ auto CompileDriver::Initialize( ++unit_index; return std::make_unique( SemIR::CheckIRId(unit_index), total_unit_count, &driver_env, options_, - &driver_env.consumer, filename, map_input(filename), target); + driver_env.consumer, filename, map_input(filename), target); }; llvm::append_range(units_, llvm::map_range(prelude, unit_builder)); input_filenames_index_ = units_.size(); @@ -500,7 +501,7 @@ auto CompileDriver::Compile(DriverEnv& driver_env) -> DriverResult { unit->PostCompile(); } - driver_env.consumer.Flush(); + driver_env.consumer->Flush(); }); PrettyStackTraceFunction flush_on_crash([&](llvm::raw_ostream& out) { @@ -512,14 +513,19 @@ auto CompileDriver::Compile(DriverEnv& driver_env) -> DriverResult { out << "Flushing diagnostics\n"; } else { out << "Pending diagnostics:\n"; - driver_env.consumer.set_stream(&out); } + // In non-streaming mode, swap out the consumer for one that writes to the + // given ostream before flushing the diagnostics. + Diagnostics::StreamConsumer stack_trace_consumer(&out); + llvm::SaveAndRestore restore( + driver_env.consumer, + options_->stream_errors ? driver_env.consumer : &stack_trace_consumer); + for (auto& unit : units_) { unit->FlushForStackTrace(); } - driver_env.consumer.Flush(); - driver_env.consumer.set_stream(driver_env.error_stream); + driver_env.consumer->Flush(); }); // Returns a DriverResult object. Called whenever Compile returns. diff --git a/toolchain/driver/compile_driver.h b/toolchain/driver/compile_driver.h index d70a4b3938eb..12e6c61fc4ac 100644 --- a/toolchain/driver/compile_driver.h +++ b/toolchain/driver/compile_driver.h @@ -12,7 +12,6 @@ #include "toolchain/diagnostics/sorting_consumer.h" #include "toolchain/driver/compile_options.h" #include "toolchain/driver/driver_env.h" -#include "toolchain/driver/driver_subcommand.h" namespace Carbon { @@ -76,13 +75,20 @@ class CompilationUnit { return *tree_and_subtrees_getter_; } + auto source() const -> const SourceBuffer& { return *source_; } + auto tokens() const -> const Lex::TokenizedBuffer& { return *tokens_; } + auto parse_tree() const -> const Parse::Tree& { return *parse_tree_; } + auto parse_tree_and_subtrees() const -> const Parse::TreeAndSubtrees& { + return GetParseTreeAndSubtrees(); + } + private: // Do codegen. Returns true on success. auto RunCodeGenHelper() -> bool; // The TreeAndSubtrees is mainly used for debugging and diagnostics, and has // significant overhead. Avoid constructing it when unused. - auto GetParseTreeAndSubtrees() -> const Parse::TreeAndSubtrees&; + auto GetParseTreeAndSubtrees() const -> const Parse::TreeAndSubtrees&; // Wraps a call with log statements to indicate start and end. Typically logs // with the actual function name, but marks timings with the appropriate @@ -128,7 +134,7 @@ class CompilationUnit { // Initialized by `SetMultiUnitCache`. MultiUnitCache* cache_ = nullptr; // Tracks memory usage of the compile. - std::optional mem_usage_; + mutable std::optional mem_usage_; // Tracks timings of the compile. std::optional timings_; @@ -136,7 +142,7 @@ class CompilationUnit { std::optional source_; std::optional tokens_; std::optional parse_tree_; - std::optional parse_tree_and_subtrees_; + mutable std::optional parse_tree_and_subtrees_; std::optionalconst Parse::TreeAndSubtrees&>> tree_and_subtrees_getter_; std::unique_ptr llvm_context_; diff --git a/toolchain/driver/compile_options.cpp b/toolchain/driver/compile_options.cpp index 835369bcdc24..7f143854e662 100644 --- a/toolchain/driver/compile_options.cpp +++ b/toolchain/driver/compile_options.cpp @@ -463,7 +463,7 @@ auto CompileOptions::BuildClangInvocation(DriverEnv& driver_env) }; all_clang_args.append(clang_args); auto clang_invocation = Carbon::BuildClangInvocation( - driver_env.consumer, driver_env.fs, *driver_env.installation, + *driver_env.consumer, driver_env.fs, *driver_env.installation, codegen_options->target, all_clang_args); if (!clang_invocation) { return ErrorBuilder() << "Failed to build a valid clang invocation."; diff --git a/toolchain/driver/config_subcommand.cpp b/toolchain/driver/config_subcommand.cpp index 21465543b314..d4e4bcd50bcb 100644 --- a/toolchain/driver/config_subcommand.cpp +++ b/toolchain/driver/config_subcommand.cpp @@ -82,13 +82,13 @@ static auto ComputeClangConfig(DriverEnv& driver_env, // Build a library invocation of Clang in order to query its header search // paths. std::shared_ptr clang_invocation = - BuildClangInvocation(driver_env.consumer, driver_env.fs, + BuildClangInvocation(*driver_env.consumer, driver_env.fs, *driver_env.installation, target_str, {}); clang_invocation->getFrontendOpts().DisableFree = false; // Setup up a driver-style diagnostic engine for the compiler invocation and // instance below as we won't go past that while computing the include dirs. - Diagnostics::ErrorTrackingConsumer error_tracker(driver_env.consumer); + Diagnostics::ErrorTrackingConsumer error_tracker(*driver_env.consumer); Diagnostics::NoLocEmitter emitter(&error_tracker); ClangDriverDiagnosticConsumer diagnostic_consumer(&emitter); llvm::IntrusiveRefCntPtr diags( diff --git a/toolchain/driver/driver.cpp b/toolchain/driver/driver.cpp index f47df68a711e..84cb3a6740fb 100644 --- a/toolchain/driver/driver.cpp +++ b/toolchain/driver/driver.cpp @@ -235,8 +235,9 @@ auto Driver::RunCommand(llvm::ArrayRef args) -> DriverResult { }); Options options; + Diagnostics::StreamConsumer consumer(error_stream_); DriverEnv env(fs_, installation_, input_stream_, output_stream_, - error_stream_, fuzzing_, enable_leaking_); + error_stream_, fuzzing_, enable_leaking_, &consumer); ErrorOr result = CommandLine::Parse( args, *env.output_stream, Options::Info, @@ -244,7 +245,7 @@ auto Driver::RunCommand(llvm::ArrayRef args) -> DriverResult { // Regardless of whether the parse succeeded, try to use the diagnostic kind // flag. - env.consumer.set_include_diagnostic_kind(options.include_diagnostic_kind); + consumer.set_include_diagnostic_kind(options.include_diagnostic_kind); if (env.installation->error()) { CARBON_DIAGNOSTIC(DriverInstallInvalid, Error, "{0}", std::string); diff --git a/toolchain/driver/driver_env.h b/toolchain/driver/driver_env.h index 0f951db84217..8ffea3977670 100644 --- a/toolchain/driver/driver_env.h +++ b/toolchain/driver/driver_env.h @@ -18,13 +18,23 @@ namespace Carbon { +// The result of a driver run. +struct DriverResult { + // Overall success result. + bool success; + + // Per-file success results. May be empty if files aren't individually + // processed. + llvm::SmallVector> per_file_success = {}; +}; + // Driver environment information, encapsulated for easy passing to subcommands. struct DriverEnv { explicit DriverEnv(llvm::IntrusiveRefCntPtr fs, const InstallPaths* installation, FILE* input_stream, llvm::raw_pwrite_stream* output_stream, llvm::raw_pwrite_stream* error_stream, bool fuzzing, - bool enable_leaking) + bool enable_leaking, Diagnostics::Consumer* consumer) : fs(std::move(fs)), installation(installation), input_stream(input_stream), @@ -32,8 +42,8 @@ struct DriverEnv { error_stream(error_stream), fuzzing(fuzzing), enable_leaking(enable_leaking), - consumer(error_stream), - emitter(&consumer) {} + consumer(consumer), + emitter(consumer) {} // The filesystem for source code. llvm::IntrusiveRefCntPtr fs; @@ -63,7 +73,7 @@ struct DriverEnv { bool build_runtimes_on_demand = false; // A diagnostic consumer, to be able to connect output. - Diagnostics::StreamConsumer consumer; + Diagnostics::Consumer* consumer; // A diagnostic emitter that has no locations. Diagnostics::NoLocEmitter emitter; diff --git a/toolchain/driver/driver_subcommand.h b/toolchain/driver/driver_subcommand.h index c91e556b76cd..299b593b2c50 100644 --- a/toolchain/driver/driver_subcommand.h +++ b/toolchain/driver/driver_subcommand.h @@ -13,16 +13,6 @@ namespace Carbon { -// The result of a driver run. -struct DriverResult { - // Overall success result. - bool success; - - // Per-file success results. May be empty if files aren't individually - // processed. - llvm::SmallVector> per_file_success = {}; -}; - // A subcommand for the driver. class DriverSubcommand { public: diff --git a/toolchain/driver/format_subcommand.cpp b/toolchain/driver/format_subcommand.cpp index 3f7fc1a13117..8225698d587e 100644 --- a/toolchain/driver/format_subcommand.cpp +++ b/toolchain/driver/format_subcommand.cpp @@ -76,14 +76,14 @@ auto FormatSubcommand::Run(DriverEnv& driver_env) -> DriverResult { // TODO: Consider refactoring this for sharing with compile. // TODO: Decide what to do with `-` when there are multiple arguments. auto source = SourceBuffer::MakeFromFileOrStdin(*driver_env.fs, f, - driver_env.consumer); + *driver_env.consumer); if (!source) { mark_per_file_error(); continue; } SharedValueStores value_stores; Lex::LexOptions lex_options; - lex_options.consumer = &driver_env.consumer; + lex_options.consumer = driver_env.consumer; auto tokens = Lex::Lex(value_stores, *source, lex_options); RawStringOstream buffer; diff --git a/toolchain/driver/language_server_subcommand.cpp b/toolchain/driver/language_server_subcommand.cpp index b55e686a5c15..6a52f81882c8 100644 --- a/toolchain/driver/language_server_subcommand.cpp +++ b/toolchain/driver/language_server_subcommand.cpp @@ -30,7 +30,7 @@ auto LanguageServerSubcommand::Run(DriverEnv& driver_env) -> DriverResult { bool success = LanguageServer::Run(*driver_env.installation, driver_env.input_stream, *driver_env.output_stream, *driver_env.error_stream, - driver_env.vlog_stream, driver_env.consumer); + driver_env.vlog_stream, *driver_env.consumer); return {.success = success}; } diff --git a/toolchain/language_server/BUILD b/toolchain/language_server/BUILD index 74f21e92d092..6c675a1a47a6 100644 --- a/toolchain/language_server/BUILD +++ b/toolchain/language_server/BUILD @@ -41,6 +41,7 @@ cc_library( "//toolchain/check", "//toolchain/diagnostics:emitter", "//toolchain/diagnostics:file_diagnostics", + "//toolchain/driver:compile_driver", "//toolchain/lex", "//toolchain/lex:tokenized_buffer", "//toolchain/parse", @@ -48,6 +49,7 @@ cc_library( "//toolchain/sem_ir:file", "//toolchain/source:source_buffer", "@llvm-project//clang-tools-extra/clangd:ClangDaemon", + "@llvm-project//llvm:Support", "@llvm-project//llvm:TargetParser", ], ) diff --git a/toolchain/language_server/context.cpp b/toolchain/language_server/context.cpp index 8aaa4df65cba..e895d823a48b 100644 --- a/toolchain/language_server/context.cpp +++ b/toolchain/language_server/context.cpp @@ -10,6 +10,7 @@ #include "common/check.h" #include "common/raw_string_ostream.h" +#include "llvm/Support/VirtualFileSystem.h" #include "llvm/TargetParser/Host.h" #include "toolchain/base/clang_invocation.h" #include "toolchain/base/shared_value_stores.h" @@ -17,6 +18,8 @@ #include "toolchain/diagnostics/consumer.h" #include "toolchain/diagnostics/diagnostic.h" #include "toolchain/diagnostics/emitter.h" +#include "toolchain/driver/compile_driver.h" +#include "toolchain/driver/compile_options.h" #include "toolchain/lex/lex.h" #include "toolchain/lex/tokenized_buffer.h" #include "toolchain/parse/parse.h" @@ -104,76 +107,138 @@ class DiagnosticConsumer : public Diagnostics::Consumer { Context* context_; clang::clangd::PublishDiagnosticsParams params_; }; + +// A virtual file corresponding to a file that has been opened and potentially +// edited by the language client. +class VirtualFile : public llvm::vfs::File { + public: + explicit VirtualFile(const Context::File* file) : file_(file) {} + + auto status() -> llvm::ErrorOr override { + return llvm::vfs::Status( + file_->filename(), llvm::sys::fs::UniqueID(0, 0), + std::chrono::system_clock::now(), 0, 0, file_->text().size(), + llvm::sys::fs::file_type::regular_file, llvm::sys::fs::all_all); + } + + auto getBuffer(const llvm::Twine& /*name*/, int64_t /*file_size*/, + bool /*requires_null_terminator*/, bool /*is_volatile*/) + -> llvm::ErrorOr> override { + return llvm::MemoryBuffer::getMemBuffer(file_->text(), file_->filename()); + } + + auto close() -> std::error_code override { return std::error_code(); } + + private: + const Context::File* file_; +}; + +// A virtual file system containing the documents whose contents were customized +// by the language client. We can't use InMemoryFileSystem for this, because it +// only supports setting the contents for each file once. +// +// TODO: Investigate whether we can replace this with clangd's `DraftStore`. +class VirtualFileSystem : public llvm::vfs::FileSystem { + public: + explicit VirtualFileSystem(Context* context) : context_(context) {} + + auto status(const llvm::Twine& path) + -> llvm::ErrorOr override { + std::string path_str = path.str(); + if (auto lookup_result = context_->files().Lookup(path_str)) { + return llvm::vfs::Status(path_str, llvm::sys::fs::UniqueID(0, 0), + std::chrono::system_clock::now(), /*User=*/0, + /*Group=*/0, lookup_result.value().text().size(), + llvm::sys::fs::file_type::regular_file, + llvm::sys::fs::all_all); + } + return std::make_error_code(std::errc::no_such_file_or_directory); + } + + auto openFileForRead(const llvm::Twine& path) + -> llvm::ErrorOr> override { + std::string path_str = path.str(); + if (auto lookup_result = context_->files().Lookup(path_str)) { + return std::unique_ptr( + new VirtualFile(&lookup_result.value())); + } + return std::make_error_code(std::errc::no_such_file_or_directory); + } + + auto dir_begin(const llvm::Twine& /*dir*/, std::error_code& ec) + -> llvm::vfs::directory_iterator override { + ec = std::make_error_code(std::errc::no_such_file_or_directory); + return llvm::vfs::directory_iterator(); + } + + auto getCurrentWorkingDirectory() const + -> llvm::ErrorOr override { + return std::string(""); + } + + auto setCurrentWorkingDirectory(const llvm::Twine& /*path*/) + -> std::error_code override { + return std::error_code(); + } + + private: + Context* context_; +}; + } // namespace +Context::Context(const InstallPaths* installation, + llvm::raw_ostream* vlog_stream, + Diagnostics::Consumer* consumer, + clang::clangd::LSPBinder::RawOutgoing* outgoing) + : installation_(installation), + vlog_stream_(vlog_stream), + file_emitter_(consumer), + no_loc_emitter_(consumer), + outgoing_(outgoing) { + auto ls_fs = llvm::makeIntrusiveRefCnt(this); + auto vfs = llvm::makeIntrusiveRefCnt( + llvm::vfs::getRealFileSystem()); + vfs->pushOverlay(ls_fs); + vfs_ = vfs; +} + auto Context::File::SetText(Context& context, std::optional version, llvm::StringRef text) -> void { // Clear state dependent on the source text. - tree_and_subtrees_.reset(); - tree_.reset(); - tokens_.reset(); - value_stores_.reset(); - source_.reset(); + compile_driver_.reset(); + + text_ = text.str(); // A consumer to gather diagnostics for the file. DiagnosticConsumer consumer(&context, uri_, version); // TODO: Make the processing asynchronous, to better handle rapid text // updates. - CARBON_CHECK(!source_ && !value_stores_ && !tokens_ && !tree_, - "We currently cache everything together"); - // TODO: Diagnostics should be passed to the LSP instead of dropped. - std::optional source = - SourceBuffer::MakeFromStringCopy(uri_.file(), text, consumer); - if (!source) { - // Failing here should be rare, but provide stub data for recovery so that - // we can have a simple API. - source = SourceBuffer::MakeFromStringCopy(uri_.file(), "", consumer); - CARBON_CHECK(source, "Making an empty buffer should always succeed"); + + llvm::raw_null_ostream null_stream; + DriverEnv driver_env(context.vfs(), &context.installation(), + /*input_stream=*/nullptr, &null_stream, &null_stream, + /*fuzzing=*/false, + /*enable_leaking=*/false, &consumer); + // TODO: Either use `raw_pwrite_stream` for all vlog streams or stop requiring + // one in DriverEnv. + driver_env.vlog_stream = + static_cast(context.vlog_stream()); + + options_ = CompileOptions(); + options_.codegen_options->target = options_.codegen_options->host; + options_.phase = CompileOptions::Phase::Check; + options_.input_filenames.push_back(filename()); + options_.prelude_import = true; + + compile_driver_ = std::make_unique(&options_); + auto map_input = [](llvm::StringRef) -> std::string { return ""; }; + if (!compile_driver_->Initialize(driver_env, map_input)) { + context.PublishDiagnostics(consumer.params()); + return; } - source_ = std::make_unique(std::move(*source)); - value_stores_ = std::make_unique(); - - Lex::LexOptions lex_options; - lex_options.consumer = &consumer; - tokens_ = std::make_unique( - Lex::Lex(*value_stores_, *source_, lex_options)); - - Parse::ParseOptions parse_options; - parse_options.consumer = &consumer; - parse_options.vlog_stream = context.vlog_stream(); - tree_ = std::make_unique(Parse::Parse(*tokens_, parse_options)); - tree_and_subtrees_ = - std::make_unique(*tokens_, *tree_); - - SemIR::File sem_ir(tree_.get(), SemIR::CheckIRId(0), tree_->packaging_decl(), - *value_stores_, uri_.file().str()); - // TODO: Support cross-file checking when multiple files have edits. - llvm::SmallVector units = {{{.consumer = &consumer, - .value_stores = value_stores_.get(), - .timings = nullptr, - .sem_ir = &sem_ir, - .total_ir_count = 1}}}; - - auto getter = [this]() -> const Parse::TreeAndSubtrees& { - return *tree_and_subtrees_; - }; - // TODO: Include any unsaved files as an overlay on the real file system. - llvm::IntrusiveRefCntPtr fs = - llvm::vfs::getRealFileSystem(); - - // TODO: Include the prelude. Make sure `total_ir_count` includes the files. - Check::CheckParseTreesOptions check_options; - check_options.vlog_stream = context.vlog_stream(); - auto getters = - Parse::GetTreeAndSubtreesStore::MakeWithExplicitSize(1, getter); - - auto clang_invocation = - BuildClangInvocation(consumer, fs, context.installation(), - llvm::sys::getDefaultTargetTriple()); - - Check::CheckParseTrees(units, getters, fs, check_options, - std::move(clang_invocation)); + compile_driver_->Compile(driver_env); // Note we need to publish diagnostics even when empty. // TODO: Consider caching previously published diagnostics and only publishing diff --git a/toolchain/language_server/context.h b/toolchain/language_server/context.h index 1671599a7bf3..46a4c6251f63 100644 --- a/toolchain/language_server/context.h +++ b/toolchain/language_server/context.h @@ -15,10 +15,10 @@ #include "toolchain/diagnostics/consumer.h" #include "toolchain/diagnostics/emitter.h" #include "toolchain/diagnostics/file_diagnostics.h" +#include "toolchain/driver/compile_driver.h" #include "toolchain/lex/tokenized_buffer.h" #include "toolchain/parse/tree_and_subtrees.h" #include "toolchain/sem_ir/file.h" -#include "toolchain/source/source_buffer.h" namespace Carbon::LanguageServer { @@ -28,40 +28,43 @@ class Context { // Cached information for an open file. class File { public: - explicit File(clang::clangd::URIForFile uri) : uri_(std::move(uri)) {} + explicit File(clang::clangd::URIForFile uri) + : uri_(std::move(uri)), filename_(uri_.file().str()) {} // Changes the file's text, updating dependent state. auto SetText(Context& context, std::optional version, llvm::StringRef text) -> void; - auto text() const -> llvm::StringRef { return source_->text(); } + auto filename() const -> llvm::StringRef { return filename_; } + auto text() const -> llvm::StringRef { return text_; } auto tree_and_subtrees() const -> const Parse::TreeAndSubtrees& { - return *tree_and_subtrees_; + CARBON_CHECK(compile_driver_); + return compile_driver_->units()[compile_driver_->first_input_index()] + ->parse_tree_and_subtrees(); } private: // The filename, stable across instances. clang::clangd::URIForFile uri_; + std::string filename_; // Current file content, and derived values. - std::unique_ptr source_; - std::unique_ptr value_stores_; - std::unique_ptr tokens_; - std::unique_ptr tree_; - std::unique_ptr tree_and_subtrees_; + std::string text_; + CompileOptions options_; + std::unique_ptr compile_driver_; }; // `vlog_stream` is optional; other parameters are required. explicit Context(const InstallPaths* installation, llvm::raw_ostream* vlog_stream, Diagnostics::Consumer* consumer, - clang::clangd::LSPBinder::RawOutgoing* outgoing) - : installation_(installation), - vlog_stream_(vlog_stream), - file_emitter_(consumer), - no_loc_emitter_(consumer), - outgoing_(outgoing) {} + clang::clangd::LSPBinder::RawOutgoing* outgoing); + + // Returns the virtual filesystem. + auto vfs() -> llvm::IntrusiveRefCntPtr& { + return vfs_; + } // Returns a reference to the file if it's known, or diagnoses and returns // null. @@ -92,6 +95,9 @@ class Context { Diagnostics::NoLocEmitter no_loc_emitter_; clang::clangd::LSPBinder::RawOutgoing* outgoing_; + // Shared virtual filesystem. + llvm::IntrusiveRefCntPtr vfs_; + // Content of files managed by the language client. Map files_; }; diff --git a/toolchain/language_server/testdata/text_document/import_prelude.carbon b/toolchain/language_server/testdata/text_document/import_prelude.carbon new file mode 100644 index 000000000000..4f6ac8144493 --- /dev/null +++ b/toolchain/language_server/testdata/text_document/import_prelude.carbon @@ -0,0 +1,91 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// AUTOUPDATE +// TIP: To test this file alone, run: +// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/text_document/import_prelude.carbon +// TIP: To dump output, run: +// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/import_prelude.carbon + +// --- STDIN +[[@LSP-CALL:initialize]] +[[@LSP-NOTIFY:textDocument/didOpen: + "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon", + "version": 1, "text": "fn Run() -> i32 { return 0; }"} +]] +[[@LSP-NOTIFY:textDocument/didChange: + "textDocument": {"uri": "file:/test.carbon", "version": 2}, + "contentChanges": [{"text": "fn Run() -> i32 { return \"foo\"; }"}] +]] +[[@LSP-NOTIFY:textDocument/didClose: + "textDocument": {"uri": "file:/test.carbon"} +]] +[[@LSP-CALL:shutdown]] +[[@LSP-NOTIFY:exit]] + +// --- AUTOUPDATE-SPLIT + +// CHECK:STDOUT: Content-Length: 146{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 1, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "capabilities": { +// CHECK:STDOUT: "documentSymbolProvider": true, +// CHECK:STDOUT: "textDocumentSync": 2 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 162{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "method": "textDocument/publishDiagnostics", +// CHECK:STDOUT: "params": { +// CHECK:STDOUT: "diagnostics": [], +// CHECK:STDOUT: "uri": "file:///test.carbon", +// CHECK:STDOUT: "version": 1 +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 512{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "method": "textDocument/publishDiagnostics", +// CHECK:STDOUT: "params": { +// CHECK:STDOUT: "diagnostics": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "message": "cannot implicitly convert expression of type `str` to `i32`", +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 31, +// CHECK:STDOUT: "line": 0 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 18, +// CHECK:STDOUT: "line": 0 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "severity": 1, +// CHECK:STDOUT: "source": "carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ], +// CHECK:STDOUT: "uri": "file:///test.carbon", +// CHECK:STDOUT: "version": 2 +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 144{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "method": "textDocument/publishDiagnostics", +// CHECK:STDOUT: "params": { +// CHECK:STDOUT: "diagnostics": [], +// CHECK:STDOUT: "uri": "file:///test.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 51{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 2, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": null +// CHECK:STDOUT: } diff --git a/toolchain/language_server/testdata/text_document/multiple_files.carbon b/toolchain/language_server/testdata/text_document/multiple_files.carbon new file mode 100644 index 000000000000..c910f8bc95b3 --- /dev/null +++ b/toolchain/language_server/testdata/text_document/multiple_files.carbon @@ -0,0 +1,96 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// AUTOUPDATE +// TIP: To test this file alone, run: +// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/text_document/multiple_files.carbon +// TIP: To dump output, run: +// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/multiple_files.carbon + +// --- STDIN +[[@LSP-CALL:initialize]] +[[@LSP-NOTIFY:textDocument/didOpen: + "textDocument": {"uri": "file:/foo.carbon", "languageId": "carbon", + "version": 1, "text": "library \"foo\"; fn A();"} +]] +// TODO: Once we support searching for imported files based on library name, +// configure this test so that this import finds the file we introduced above. +[[@LSP-NOTIFY:textDocument/didOpen: + "textDocument": {"uri": "file:/bar.carbon", "languageId": "carbon", + "version": 1, "text": "import library \"foo\"; fn B() { return A(); }"} +]] +[[@LSP-CALL:shutdown]] +[[@LSP-NOTIFY:exit]] + +// --- AUTOUPDATE-SPLIT + +// CHECK:STDOUT: Content-Length: 146{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 1, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "capabilities": { +// CHECK:STDOUT: "documentSymbolProvider": true, +// CHECK:STDOUT: "textDocumentSync": 2 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 161{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "method": "textDocument/publishDiagnostics", +// CHECK:STDOUT: "params": { +// CHECK:STDOUT: "diagnostics": [], +// CHECK:STDOUT: "uri": "file:///foo.carbon", +// CHECK:STDOUT: "version": 1 +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 827{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "method": "textDocument/publishDiagnostics", +// CHECK:STDOUT: "params": { +// CHECK:STDOUT: "diagnostics": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "message": "imported API 'Main//foo' not found", +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 21, +// CHECK:STDOUT: "line": 0 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 0, +// CHECK:STDOUT: "line": 0 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "severity": 1, +// CHECK:STDOUT: "source": "carbon" +// CHECK:STDOUT: }, +// CHECK:STDOUT: { +// CHECK:STDOUT: "message": "no return expression should be provided in this context", +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 42, +// CHECK:STDOUT: "line": 0 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 31, +// CHECK:STDOUT: "line": 0 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "severity": 1, +// CHECK:STDOUT: "source": "carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ], +// CHECK:STDOUT: "uri": "file:///bar.carbon", +// CHECK:STDOUT: "version": 1 +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 51{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 2, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": null +// CHECK:STDOUT: }