Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ release's notes are that section.
Versions are three-part semantic versions, `MAJOR.MINOR.PATCH`, and every editor plugin carries the
product version unchanged.

## [0.0.11] — 2026-10-04

### Completion

- **Completion comes up in files that import modules.** On such a file clangd 23.1 answers every
completion in 0.95–1.05 s even when the BMIs are long built and cached (upstream defect UP-25,
issue #24) — just past the 1 s answer budget, which cancelled the engine's answer at the line and
answered with the fallback instead; after `.`, `->` or `::` the fallback is empty by design, so
the popup sometimes never came at all. The budget now follows the file: where the core engine
keeps answering just past the budget, completion and signature help wait up to 2.5 s for those
answers instead of cancelling them — and where the engine answers rarely or quickly (a broken
module rebuilding, a fan-out save, an ordinary file), the budget, the fallback and the
user-experience scenarios calibrated on them stay exactly as they were.

## [0.0.10] — 2026-10-03

A workspace's module cache grew without bound when clangd kept crashing: every crash left its
Expand Down
4 changes: 3 additions & 1 deletion docs/50-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ it is always `producer`, the fingerprint is not matching — the report's `proje
build files' timestamps are where to look.

**Completion shows only words from the file, or hover says modules are being prepared.** A request
has a budget for clangd — completion and signature help 1 s, hover 2 s, go-to-definition 10 s — and
has a budget for clangd — completion and signature help 1 s, and up to 2.5 s for a file whose
completions clangd keeps answering just past that (a module importer on clangd 23.1, upstream defect
UP-25), so those answers are not cancelled at the line — hover 2 s, go-to-definition 10 s — and
past it mcppls answers with what it has. Completion is then the words of the file nearest the
cursor, an incomplete list, so the editor asks again as you type; hover, while modules are being
prepared, is a line saying so. It is clangd being busy with modules, not a failure. From 0.0.8,
Expand Down
2 changes: 1 addition & 1 deletion docs/zh-CN/50-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ mcppls report --bundle problem.zip --root path/to/project # 可加 --hide-proj

**每次启动都很慢。** 第二次会话应该很快:模型连同构建工具所读一切内容的指纹一起被缓存,与之匹配的会话会立即套用计划,并在后台确认;已经构建好的模块会复用,不会重建(0.0.6 及更早版本在热启动时会把每个模块都重建一遍,issue #30)。`project.firstOrigin` 会说明发生了哪种情况。如果它一直是 `producer`,说明指纹没有匹配上——该看报告里的 `project.producerRun` 和构建文件的时间戳。

**补全只有文件里的词,或悬停提示说正在准备模块。** 每种请求给 clangd 的都有预算——补全和签名帮助 1 秒,悬停 2 秒,跳转到定义 10 秒——超过之后 mcppls 用手上有的东西作答。补全这时给出的是文件里离光标最近的那些词,是一份不完整的列表,所以你继续输入时编辑器会再问一次;悬停在模块准备期间给出的是一行说明。这是 clangd 正忙着处理模块,不是故障。从 0.0.8 起,clangd 迟到的补全不再丢弃:它最多再算 10 秒,你在同一个词里继续输入时发出的请求都等它,一到就交给它们,所以在 clangd 重建得慢的文件里,列表仍会在你打完这个词之前出现。报告里的 `requests.<method>.answeredBy` 按方法统计了各由哪个引擎作答;`engineP50Ms`、`engineP95Ms` 是引擎作答的那些请求在引擎里花的时间,`overheadP50Ms`、`overheadP95Ms` 是 mcppls 在其外加的时间,由此看出一次慢的补全慢在谁;`completion.late` 统计 clangd 迟到的答案和用上它们的请求;`slowestFiles` 列出最慢的十个文件,以及每个文件有多少次补全只拿到了词;`engines[].details.buildTimes` 说明 clangd 构建每个文件花在哪里(preamble、导入的模块、AST 构建次数)。
**补全只有文件里的词,或悬停提示说正在准备模块。** 每种请求给 clangd 的都有预算——补全和签名帮助 1 秒,若某文件的补全 clangd 总是刚好超过 1 秒才回答(clangd 23.1 的模块导入方,上游缺陷 UP-25),预算最多放宽到 2.5 秒,让这些答案不会正好在预算线上被取消——悬停 2 秒,跳转到定义 10 秒——超过之后 mcppls 用手上有的东西作答。补全这时给出的是文件里离光标最近的那些词,是一份不完整的列表,所以你继续输入时编辑器会再问一次;悬停在模块准备期间给出的是一行说明。这是 clangd 正忙着处理模块,不是故障。从 0.0.8 起,clangd 迟到的补全不再丢弃:它最多再算 10 秒,你在同一个词里继续输入时发出的请求都等它,一到就交给它们,所以在 clangd 重建得慢的文件里,列表仍会在你打完这个词之前出现。报告里的 `requests.<method>.answeredBy` 按方法统计了各由哪个引擎作答;`engineP50Ms`、`engineP95Ms` 是引擎作答的那些请求在引擎里花的时间,`overheadP50Ms`、`overheadP95Ms` 是 mcppls 在其外加的时间,由此看出一次慢的补全慢在谁;`completion.late` 统计 clangd 迟到的答案和用上它们的请求;`slowestFiles` 列出最慢的十个文件,以及每个文件有多少次补全只拿到了词;`engines[].details.buildTimes` 说明 clangd 构建每个文件花在哪里(preamble、导入的模块、AST 构建次数)。

**模块单元里出现"未使用的头文件"警告。** 这是 clangd 的 include cleaner,默认开启,mcppls 不关它:在模块接口的全局模块片段、实现单元和导入方里,它和在普通文件里一样,只报没有任何东西用到的头文件(有一个 conformance fixture 在 clangd 升级时守着这一点)。要关掉,在项目的 `.clangd` 或你的 clangd `config.yaml` 里写 `Diagnostics: { UnusedIncludes: None }`;mcppls 启动的 clangd 两处都会读。

Expand Down
2 changes: 1 addition & 1 deletion editors/claude-code/.claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"displayName": "C++ Modules Language Server",
"source": "./mcppls-lsp",
"description": "Registers mcppls as the language server for C and C++ sources, including C++20/23 named modules.",
"version": "0.0.10",
"version": "0.0.11",
"author": {
"name": "Sunrisepeak",
"url": "https://github.com/Sunrisepeak/mcpp-language-server"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mcppls-lsp",
"displayName": "C++ Modules Language Server",
"version": "0.0.10",
"version": "0.0.11",
"description": "Registers mcppls as the language server for C and C++ sources, including C++20/23 named modules, and its MCP tools (symbols, references, modules, verification, review). Replaces clangd-lsp for a project; do not enable both at once.",
"author": {
"name": "Sunrisepeak",
Expand Down
2 changes: 1 addition & 1 deletion editors/clion/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# ones within the same major line; sinceBuild/untilBuild in plugin.xml is what actually gates it.
platformType = CL
platformVersion = 2026.2.3
pluginVersion = 0.0.10
pluginVersion = 0.0.11
org.gradle.jvmargs = -Xmx2g
# The IDE ships the Kotlin standard library; bundling a second copy in the plugin is what JetBrains
# asks plugins not to do.
Expand Down
4 changes: 2 additions & 2 deletions editors/vscode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "mcpp-language-server",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.0.10",
"version": "0.0.11",
"publisher": "sunrisepeak",
"license": "Apache-2.0",
"icon": "icon.png",
Expand Down
2 changes: 1 addition & 1 deletion editors/zed/extension.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id = "mcppls"
name = "C++ Modules Language Server"
version = "0.0.10"
version = "0.0.11"
schema_version = 1
description = "mcppls - C++20/23 named modules that just work: navigation, completion, hover and diagnostics across modules for any compiler"
repository = "https://github.com/Sunrisepeak/mcpp-language-server"
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ libarchive = "3.8.7"

[package]
name = "mcpp-language-server"
version = "0.0.10"
version = "0.0.11"
description = "Compiler-agnostic C++ modules language server"
license = "Apache-2.0"
authors = ["Sunrisepeak"]
Expand Down
2 changes: 1 addition & 1 deletion modules/base/src/version.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export namespace mcppls::base {
// checked against mcpp.toml (the one source) by `mcppls-devtools version --check`, not kept in step by
// hand. Three constants that lived here and nothing read were removed rather than left to drift:
// the S1 profile version is spec::PROFILE_VERSION, the kit manifest version is spec::KIT_VERSION.
inline constexpr std::string_view VERSION { "0.0.10" };
inline constexpr std::string_view VERSION { "0.0.11" };
// The clangd the payload ships. Checked against packaging/payload.lock.json by the same command.
inline constexpr std::string_view CLANGD_VERSION { "23.1.0" };
// The oldest mcpp that answers `mcpp emit build-database` — the `mcpp.build-database` kind, which
Expand Down
13 changes: 12 additions & 1 deletion src/engine/clangd/workarounds.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace mcppls::engine::clangd {

namespace {

constexpr std::array<Workaround, 11> REGISTRY { {
constexpr std::array<Workaround, 12> REGISTRY { {
{
.id = TRAILING_DOT_MODULE_NAME,
.title = "a module name ending in '.' at the end of its line spins clangd forever; clangd is given the line with ';' after the dot",
Expand Down Expand Up @@ -129,6 +129,17 @@ constexpr std::array<Workaround, 11> REGISTRY { {
.canary = "conformance/fixtures/cache-budget: copies a dead generation left are gone after the next start, while the published BMIs stay",
.premise = "the cache directory under <cdb>/.cache/clangd belongs to this server while it holds the workspace lease, so anything the previous clangd left there and no reader holds may be removed (the same premise as RD12, which clears the module locks there)",
},
{
.id = MODULE_IMPORTER_COMPLETION_BUDGET,
.title = "clangd 23.1 answers a module importer's completions in about a second, just past the budget; on such a file the budget waits up to 2.5 s for those answers instead of cancelling each at the line",
.fixedIn = "",
.upstream = "unfiled (UP-25 in issue #24): the module context is re-loaded per request, so a file that imports modules pays it on every completion while a file without imports answers in 60 ms",
.evidence = "LSP replay probe on the qt-demo CDB, 2026-10-04: 950-1050 ms per completion on the importer at every position, 60 ms on a file without imports of the same project; tests/test_completion.cpp (EnginePace); conformance fixtures completion-keywords, ux-xlings, ux-mcpp, ux-heavy-headers",
.added = "0.0.11",
.removeWhen = "clangd keeps a file's module context between requests, or carries the imports in its preamble, so a module importer's completion costs no more than another file's",
.canary = "",
.premise = "the engine's answers for one file arrive reliably and just past the flat budget: two answers in the last sixty seconds, all within two seconds of the ask and all past the flat budget, extend that file's budget to their slowest plus 500 ms (2.5 s at most). Answers far beyond that are not counted -- a busy engine is not a slow-and-steady one -- and an engine that answers rarely (a broken module rebuilding, a fan-out save) or quickly keeps the flat budget, and the fallback with it",
},
} };

// "23.1.0" -> {23, 1, 0}; anything else -> nullopt.
Expand Down
1 change: 1 addition & 0 deletions src/engine/clangd/workarounds.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ inline constexpr std::string_view BACKGROUND_INDEX_WITHOUT_MODULES { "WA-CLANGD-
inline constexpr std::string_view MODULE_SCAN_PER_REQUEST { "WA-CLANGD-009" };
inline constexpr std::string_view CONST_CORRECTNESS_VIEWS { "WA-CLANGD-010" };
inline constexpr std::string_view LEFT_BEHIND_MODULE_COPIES { "WA-CLANGD-011" };
inline constexpr std::string_view MODULE_IMPORTER_COMPLETION_BUDGET { "WA-CLANGD-012" };

std::span<const Workaround> workarounds();
const Workaround* find_workaround(std::string_view id);
Expand Down
23 changes: 23 additions & 0 deletions src/orchestrator/completion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,27 @@ Json retarget(const Json& result, base::Position position) {
return out;
}

void EnginePace::answered(std::chrono::steady_clock::time_point at, std::chrono::milliseconds latency,
std::chrono::milliseconds cap) {
if (latency > cap - MARGIN) return; // beyond any budget this file could earn: the engine is busy, not steady
recent_.push_back(Answer { at, latency });
if (recent_.size() > KEEP) recent_.pop_front();
}

std::chrono::milliseconds EnginePace::budget(std::chrono::steady_clock::time_point now, std::chrono::milliseconds base,
std::chrono::milliseconds cap) const {
const auto window { std::chrono::duration_cast<std::chrono::steady_clock::duration>(WINDOW) };
std::size_t fresh { 0 };
std::chrono::milliseconds slowest { 0 };
std::chrono::milliseconds fastest { std::chrono::milliseconds::max() };
for (const auto& answer : recent_) {
if (now - answer.at > window) continue;
++fresh;
slowest = std::max(slowest, answer.latency);
fastest = std::min(fastest, answer.latency);
}
if (fresh < 2 || fastest <= base) return base;
return std::min(cap, slowest + MARGIN);
}

} // namespace mcppls::orchestrator::completion
30 changes: 30 additions & 0 deletions src/orchestrator/completion.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,34 @@ bool typed_on(const WordKey& earlier, const WordKey& later);
// `position` drops its item. The result is a CompletionList, with the engine's `isIncomplete`.
Json retarget(const Json& result, base::Position position);

// UP-25 (issue #24): what the core engine has been answering for one file, and the budget those answers earn. clangd
// 23.1 answers a module importer's completions in about a second -- just past the flat budget -- so the flat budget
// alone cancelled every answer the engine was about to give. The budget for such a file waits past the flat one;
// an engine that answers rarely (a broken module rebuilding, a fan-out save) or quickly keeps the flat budget, and
// the fallback must not get slower for it. Pure state: the workspace records each core-engine answer and reads the
// budget before it routes the next completion of that file.
class EnginePace {
public:
// the core engine answered one completion of this file, `latency` after the person asked. Answers far beyond
// `cap` say the engine is busy, not slow-and-steady, and are not counted: counting them would hold every
// fallback of this file past a budget those answers would never meet anyway.
void answered(std::chrono::steady_clock::time_point at, std::chrono::milliseconds latency,
std::chrono::milliseconds cap);
// the budget for the next completion of this file: `base`, unless the answers of the recent window all landed
// past `base` -- at least two of them, so one slow answer is not a pattern -- and then long enough for them
// (`cap` at most). The window outlives a think pause; a longer pause relearns with the answers C-2 already keeps.
std::chrono::milliseconds budget(std::chrono::steady_clock::time_point now, std::chrono::milliseconds base,
std::chrono::milliseconds cap) const;

private:
struct Answer {
std::chrono::steady_clock::time_point at;
std::chrono::milliseconds latency;
};
std::deque<Answer> recent_;
static constexpr std::size_t KEEP { 4 };
static constexpr std::chrono::seconds WINDOW { 60 };
static constexpr std::chrono::milliseconds MARGIN { 500 };
};

} // namespace mcppls::orchestrator::completion
Loading
Loading