[clang-tools-extra] [clangd] Fix header-name completion replace ranges (PR #225473)

via cfe-commits cfe-commits at lists.llvm.org
Sat Oct 3 07:24:21 PDT 2026


https://github.com/argothiel updated https://github.com/llvm/llvm-project/pull/225473

>From cc285c2f477525cc3a7d330950e3706f35bcf1df Mon Sep 17 00:00:00 2001
From: argothiel <argothiel_llvm at interia.pl>
Date: Tue, 22 Sep 2026 20:13:43 +0200
Subject: [PATCH 1/3] [clangd] Fix header-name completion replace ranges

Handle header-name completion as a special case for replace edits. Its
insert range already extends past the identifier, including delimiters
such as '"' and '>'. Reuse that range for replace edits so that the
insert range remains a prefix of the replace range, as required by the
LSP specification.

Add a unit test covering this case.

Fixes llvm/llvm-project#225326
---
 clang-tools-extra/clangd/CodeComplete.cpp     |  9 ++--
 .../clangd/unittests/CodeCompleteTests.cpp    | 54 +++++++++++++++++++
 2 files changed, 60 insertions(+), 3 deletions(-)

diff --git a/clang-tools-extra/clangd/CodeComplete.cpp b/clang-tools-extra/clangd/CodeComplete.cpp
index 80091d3a48b33f..ed5935955eafc7 100644
--- a/clang-tools-extra/clangd/CodeComplete.cpp
+++ b/clang-tools-extra/clangd/CodeComplete.cpp
@@ -1834,9 +1834,12 @@ class CodeCompleteFlow {
     }
 
     if (Opts.EnableInsertReplace) {
-      ReplaceRange.emplace();
-      ReplaceRange->start = InsertRange.start;
-      ReplaceRange->end = getEndOfCodeCompletionReplace(SM);
+      ReplaceRange = InsertRange;
+
+      // Header-name completion always provides a replace-like range,
+      // including delimiters.
+      if (CCContextKind != CodeCompletionContext::CCC_IncludedFile)
+        ReplaceRange->end = getEndOfCodeCompletionReplace(SM);
     }
     Filter = FuzzyMatcher(
         Recorder->CCSema->getPreprocessor().getCodeCompletionFilter());
diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
index 4c1cab7b11e606..6520bdf456bfb9 100644
--- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
@@ -4355,6 +4355,60 @@ TEST(CompletionTest, ReplaceRangeNoCompile) {
   EXPECT_EQ(Results.ReplaceRange, std::nullopt);
 }
 
+TEST(CompletionTest, ReplaceRangeInclude) {
+  clangd::CodeCompleteOptions Opts;
+  Opts.EnableInsertReplace = true;
+
+  TestTU TU;
+  TU.AdditionalFiles["include/sub/foo.h"] = "";
+  TU.AdditionalFiles["include/sub/u00e9/bar.h"] = "";
+  TU.ExtraArgs = {"-Iinclude"};
+
+  // Replace range starts after a preceding path component and includes the
+  // closing angle bracket.
+  const char *NestedPath = R"cpp(#include <sub/[[fo^o.h>]])cpp";
+  Annotations A(NestedPath);
+  TU.Code = A.code();
+  CodeCompleteResult Results =
+      completions(TU, A.point(), /*IndexSymbols=*/{}, Opts);
+  EXPECT_EQ(Results.InsertRange, A.range());
+  EXPECT_EQ(Results.ReplaceRange, A.range());
+
+  // Replace range starts after a preceding path component and includes the
+  // closing quote.
+  const char *WithQuote = R"cpp(#include "sub/[[fo^o.h"]])cpp";
+  A = Annotations(WithQuote);
+  TU.Code = A.code();
+  Results = completions(TU, A.point(), /*IndexSymbols=*/{}, Opts);
+  EXPECT_EQ(Results.InsertRange, A.range());
+  EXPECT_EQ(Results.ReplaceRange, A.range());
+
+  // Replace range includes the path component and its trailing separator.
+  const char *PathComponent = R"cpp(#include "[[su^b/]]foo.h")cpp";
+  A = Annotations(PathComponent);
+  TU.Code = A.code();
+  Results = completions(TU, A.point(), /*IndexSymbols=*/{}, Opts);
+  EXPECT_EQ(Results.InsertRange, A.range());
+  EXPECT_EQ(Results.ReplaceRange, A.range());
+
+  // A UCN-like escape is not interpreted in a header name.
+  const char *UnicodeLike = R"cpp(#include "[[su^\u00e9/]]foo.h")cpp";
+  A = Annotations(UnicodeLike);
+  TU.Code = A.code();
+  Results = completions(TU, A.point(), /*IndexSymbols=*/{}, Opts);
+  EXPECT_EQ(Results.InsertRange, A.range());
+  EXPECT_EQ(Results.ReplaceRange, A.range());
+
+  // In MSVC compatibility mode, backslash is a path separator.
+  TU.ExtraArgs.push_back("-fms-compatibility");
+  const char *UnicodeLikeMs = R"cpp(#include "[[su^\]]u00e9/bar.h")cpp";
+  A = Annotations(UnicodeLikeMs);
+  TU.Code = A.code();
+  Results = completions(TU, A.point(), /*IndexSymbols=*/{}, Opts);
+  EXPECT_EQ(Results.InsertRange, A.range());
+  EXPECT_EQ(Results.ReplaceRange, A.range());
+}
+
 TEST(NoCompileCompletionTest, Basic) {
   auto Results = completionsNoCompile(R"cpp(
     void func() {

>From af346a78219a405b60c4279cc2be16e0c0af0f84 Mon Sep 17 00:00:00 2001
From: argothiel <argothiel_llvm at interia.pl>
Date: Tue, 22 Sep 2026 21:16:44 +0200
Subject: [PATCH 2/3] fixup! [clangd] Fix header-name completion replace ranges

---
 clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
index 6520bdf456bfb9..edfd29136881fd 100644
--- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
@@ -4392,12 +4392,14 @@ TEST(CompletionTest, ReplaceRangeInclude) {
   EXPECT_EQ(Results.ReplaceRange, A.range());
 
   // A UCN-like escape is not interpreted in a header name.
+  TU.ExtraArgs.push_back("-fno-ms-compatibility");
   const char *UnicodeLike = R"cpp(#include "[[su^\u00e9/]]foo.h")cpp";
   A = Annotations(UnicodeLike);
   TU.Code = A.code();
   Results = completions(TU, A.point(), /*IndexSymbols=*/{}, Opts);
   EXPECT_EQ(Results.InsertRange, A.range());
   EXPECT_EQ(Results.ReplaceRange, A.range());
+  TU.ExtraArgs.pop_back();
 
   // In MSVC compatibility mode, backslash is a path separator.
   TU.ExtraArgs.push_back("-fms-compatibility");

>From a03b3f63a0e34b0a229415e3e07e1cb22666eb1a Mon Sep 17 00:00:00 2001
From: argothiel <argothiel_llvm at interia.pl>
Date: Sat, 3 Oct 2026 16:21:53 +0200
Subject: [PATCH 3/3] fixup! [clangd] Fix header-name completion replace ranges

---
 clang-tools-extra/clangd/CodeComplete.cpp | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/clang-tools-extra/clangd/CodeComplete.cpp b/clang-tools-extra/clangd/CodeComplete.cpp
index ed5935955eafc7..2c7a317d045a94 100644
--- a/clang-tools-extra/clangd/CodeComplete.cpp
+++ b/clang-tools-extra/clangd/CodeComplete.cpp
@@ -1834,12 +1834,13 @@ class CodeCompleteFlow {
     }
 
     if (Opts.EnableInsertReplace) {
-      ReplaceRange = InsertRange;
-
       // Header-name completion always provides a replace-like range,
-      // including delimiters.
-      if (CCContextKind != CodeCompletionContext::CCC_IncludedFile)
-        ReplaceRange->end = getEndOfCodeCompletionReplace(SM);
+      // including delimiters, even in the Insert mode.
+      const Position ReplaceRangeEnd =
+          CCContextKind == CodeCompletionContext::CCC_IncludedFile
+              ? InsertRange.end
+              : getEndOfCodeCompletionReplace(SM);
+      ReplaceRange = Range{InsertRange.start, ReplaceRangeEnd};
     }
     Filter = FuzzyMatcher(
         Recorder->CCSema->getPreprocessor().getCodeCompletionFilter());



More information about the cfe-commits mailing list