Files
clice/src/server/worker/stateless_worker.cpp
ykiko 3305465d1f feat(formatting): wire up textDocument/formatting and rangeFormatting (#441)
## Summary

- Wire up the existing `document_format` feature to LSP via stateless
workers
- Add `Format` kind to stateless worker dispatch, with a lightweight
`forward_format` path in `Compiler` (no compilation/deps needed — just
file path + content)
- Register `textDocument/formatting` and `textDocument/rangeFormatting`
handlers with `scoped_pause`
- Style lookup uses `clang::format::getStyle` which walks parent
directories for `.clang-format`, matching clangd's behavior

## Test plan

- [x] 4 unit tests: simple format, range format, idempotent (no edits),
include sort
- [x] 3 integration tests: full document format (verifies applied edits
match expected output), range format, already-formatted no-op
- [x] Capability assertions added to `test_capabilities`
- [x] All existing tests pass (554 unit, 170 integration, 2 smoke)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added document formatting and range formatting capabilities to the LSP
server
* Formatting can target the entire document or a specific range of code
  * Server now advertises formatting support to LSP clients

* **Tests**
  * Added comprehensive test coverage for formatting functionality

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 19:15:07 +08:00

339 lines
11 KiB
C++

#include "server/worker/stateless_worker.h"
#include "compile/compilation.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol/worker.h"
#include "server/worker/worker_common.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/transport.h"
#include "llvm/Support/raw_ostream.h"
namespace clice {
/// RAII guard that lowers the current process's scheduling priority and
/// restores it on destruction.
struct ScopedNice {
int saved;
explicit ScopedNice(int increment = 10) {
auto p = kota::sys::priority();
saved = p ? *p : 0;
kota::sys::set_priority(saved + increment);
}
~ScopedNice() {
kota::sys::set_priority(saved);
}
};
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::BincodePeer::RequestContext;
/// Extract error messages from compilation diagnostics.
static std::string collect_errors(CompilationUnit& unit) {
std::string errors;
for(auto& diag: unit.diagnostics()) {
if(diag.id.level >= DiagnosticLevel::Error) {
if(!errors.empty())
errors += "; ";
errors += diag.message;
}
}
return errors;
}
/// Build a TUIndex, serialize it, and return as a string.
static std::string serialize_tu_index(CompilationUnit& unit, bool interested_only = false) {
auto tu_index = index::TUIndex::build(unit, interested_only);
if(!interested_only) {
tu_index.main_file_index = index::FileIndex();
}
std::string serialized;
llvm::raw_string_ostream os(serialized);
tu_index.serialize(os);
return serialized;
}
/// Write compilation output to disk, handling tmp+rename pattern.
/// Returns the final path on success, or empty string on failure.
static std::string finalize_output(const std::string& output_path,
const std::string& tmp_path,
const std::string& file,
const char* label) {
if(!output_path.empty()) {
auto ec = fs::rename(tmp_path, output_path);
if(ec) {
return output_path;
} else {
LOG_WARN("{}: rename {} -> {} failed: {}",
label,
tmp_path,
output_path,
ec.error().message());
return tmp_path;
}
}
return tmp_path;
}
static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::Preamble;
fill_args(cp, params.directory, params.arguments);
cp.add_remapped_file(params.file, params.text, params.preamble_bound);
std::string tmp_path;
bool has_output = !params.output_path.empty();
if(has_output) {
tmp_path = params.output_path + ".tmp";
} else {
auto tmp = fs::createTemporaryFile("clice-pch", "pch");
if(!tmp) {
LOG_ERROR("BuildPCH: failed to create temp file");
return {false, "Failed to create temporary PCH file"};
}
tmp_path = *tmp;
}
cp.output_file = tmp_path;
PCHInfo pch_info;
auto unit = compile(cp, pch_info);
bool success = unit.completed();
std::string errors;
if(!success)
errors = collect_errors(unit);
std::string tu_index_data;
std::string pch_links_json;
if(success) {
tu_index_data = serialize_tu_index(unit);
auto links = feature::document_links(unit);
auto raw = to_raw(links);
pch_links_json = std::move(raw.data);
}
// Destroy CompilationUnit to flush PCH to disk.
unit = CompilationUnit(nullptr);
if(success) {
auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCH");
LOG_INFO("BuildPCH done: file={}, output={}, {}ms", params.file, final_path, timer.ms());
worker::BuildResult result;
result.success = true;
result.output_path = std::move(final_path);
result.deps = pch_info.deps;
result.tu_index_data = std::move(tu_index_data);
result.pch_links_json = std::move(pch_links_json);
return result;
} else {
LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]", params.file, timer.ms(), errors);
fs::remove(tmp_path);
return {false, errors.empty() ? "PCH compilation failed" : errors};
}
}
static worker::BuildResult handle_build_pcm(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::ModuleInterface;
fill_args(cp, params.directory, params.arguments);
for(auto& [name, path]: params.pcms) {
cp.pcms.try_emplace(name, path);
}
std::string tmp_path;
bool has_output = !params.output_path.empty();
if(has_output) {
tmp_path = params.output_path + ".tmp";
} else {
auto tmp = fs::createTemporaryFile("clice-pcm", "pcm");
if(!tmp) {
LOG_ERROR("BuildPCM: failed to create temp file");
return {false, "Failed to create temporary PCM file"};
}
tmp_path = *tmp;
}
cp.output_file = tmp_path;
PCMInfo pcm_info;
auto unit = compile(cp, pcm_info);
bool success = unit.completed();
std::string errors;
if(!success)
errors = collect_errors(unit);
std::string tu_index_data;
if(success)
tu_index_data = serialize_tu_index(unit, true);
unit = CompilationUnit(nullptr);
if(success) {
auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCM");
LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms());
worker::BuildResult result;
result.success = true;
result.output_path = std::move(final_path);
result.deps = pcm_info.deps;
result.tu_index_data = std::move(tu_index_data);
return result;
} else {
LOG_WARN("BuildPCM failed: module={}, {}ms, errors=[{}]",
params.module_name,
timer.ms(),
errors);
fs::remove(tmp_path);
return {false, errors.empty() ? "PCM compilation failed" : errors};
}
}
static worker::BuildResult handle_index(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::Indexing;
fill_args(cp, params.directory, params.arguments);
for(auto& [name, path]: params.pcms) {
cp.pcms.try_emplace(name, path);
}
auto unit = compile(cp);
if(!unit.completed()) {
LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms());
return {false, "Index compilation failed"};
}
auto tu_index = index::TUIndex::build(unit);
std::string serialized;
llvm::raw_string_ostream os(serialized);
tu_index.serialize(os);
LOG_INFO("Index done: file={}, {} symbols, {}ms",
params.file,
tu_index.symbols.size(),
timer.ms());
worker::BuildResult result;
result.success = true;
result.tu_index_data = std::move(serialized);
return result;
}
static worker::BuildResult handle_completion(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::Completion;
fill_args(cp, params.directory, params.arguments);
if(!params.pch.first.empty()) {
cp.pch = params.pch;
}
for(auto& [name, path]: params.pcms) {
cp.pcms.try_emplace(name, path);
}
cp.add_remapped_file(params.file, params.text);
cp.completion = {params.file, params.offset};
auto items = feature::code_complete(cp);
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
worker::BuildResult result;
result.result_json = to_raw(items);
return result;
}
static worker::BuildResult handle_signature_help(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::Completion;
fill_args(cp, params.directory, params.arguments);
if(!params.pch.first.empty()) {
cp.pch = params.pch;
}
for(auto& [name, path]: params.pcms) {
cp.pcms.try_emplace(name, path);
}
cp.add_remapped_file(params.file, params.text);
cp.completion = {params.file, params.offset};
auto help = feature::signature_help(cp);
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
worker::BuildResult result;
result.result_json = to_raw(help);
return result;
}
static worker::BuildResult handle_format(const worker::BuildParams& params) {
ScopedTimer timer;
std::optional<LocalSourceRange> range;
if(params.format_range.valid()) {
range = params.format_range;
}
auto edits = feature::document_format(params.file, params.text, range);
LOG_DEBUG("Format done: {} edits, {}ms", edits.size(), timer.ms());
worker::BuildResult result;
result.result_json = to_raw(edits);
return result;
}
int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir) {
logging::stderr_logger(worker_name, logging::options);
if(!log_dir.empty()) {
logging::file_logger(worker_name, log_dir, logging::options);
}
LOG_INFO("Starting stateless worker");
kota::event_loop loop;
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
if(!transport_result) {
LOG_ERROR("Failed to open stdio transport");
return 1;
}
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
peer.on_request([&](RequestContext& ctx,
const worker::BuildParams& params) -> RequestResult<worker::BuildParams> {
using K = worker::BuildKind;
auto result = co_await kota::queue([&]() -> worker::BuildResult {
switch(params.kind) {
case K::BuildPCH: return handle_build_pch(params);
case K::BuildPCM: return handle_build_pcm(params);
case K::Index: {
ScopedNice guard;
return handle_index(params);
}
case K::Completion: return handle_completion(params);
case K::SignatureHelp: return handle_signature_help(params);
case K::Format: return handle_format(params);
}
return {false, "Unknown build kind"};
});
co_return result.value();
});
LOG_INFO("Stateless worker ready, waiting for requests");
loop.schedule(peer.run());
auto ret = loop.run();
LOG_INFO("Stateless worker exiting with code {}", ret);
return ret;
}
} // namespace clice