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

>From 45639edea417cbe0274bbfd62f5d56bfeafae0d3 Mon Sep 17 00:00:00 2001
From: argothiel <[email protected]>
Date: Fri, 20 Mar 2026 01:07:02 +0100
Subject: [PATCH 1/3] [clangd] NFC: Rename completion ranges for
 insertReplaceSupport

When insertReplaceSupport from LSP 3.16 is implemented, a completion
range could be either an insert range or a replace range. This commit
renames the existing completion ranges as insert ranges, so that the
replace ranges can be added in a non-confusing way.

This commit only makes sense with the follow-up implementation of the
insertReplaceSupport.

Included renames:
- CodeCompleteFlow::ReplacedRange -> InsertRange
- CodeCompleteResult::CompletionRange -> InsertRange
- CodeCompletion::CompletionTokenRange -> CompletionInsertRange
- codeCompleteComment: Offset -> OutsideStartOffset
- codeCompleteComment: CompletionRange -> InsertRange
---
 clang-tools-extra/clangd/CodeComplete.cpp     | 39 ++++++++++---------
 clang-tools-extra/clangd/CodeComplete.h       |  4 +-
 .../clangd/unittests/CodeCompleteTests.cpp    | 26 ++++++-------
 3 files changed, 35 insertions(+), 34 deletions(-)

diff --git a/clang-tools-extra/clangd/CodeComplete.cpp 
b/clang-tools-extra/clangd/CodeComplete.cpp
index 7c390f9c8219d..6bbd35c648276 100644
--- a/clang-tools-extra/clangd/CodeComplete.cpp
+++ b/clang-tools-extra/clangd/CodeComplete.cpp
@@ -1615,7 +1615,7 @@ class CodeCompleteFlow {
   bool Incomplete = false; // Would more be available with a higher limit?
   CompletionPrefix HeuristicPrefix;
   std::optional<FuzzyMatcher> Filter; // Initialized once Sema runs.
-  Range ReplacedRange;
+  Range InsertRange;
   std::vector<std::string> QueryScopes;      // Initialized once Sema runs.
   std::vector<std::string> AccessibleScopes; // Initialized once Sema runs.
   // Initialized once QueryScopes is initialized, if there are scopes.
@@ -1750,8 +1750,8 @@ class CodeCompleteFlow {
     IsUsingDeclaration = false;
     Filter = FuzzyMatcher(HeuristicPrefix.Name);
     auto Pos = offsetToPosition(Content, Offset);
-    ReplacedRange.start = ReplacedRange.end = Pos;
-    ReplacedRange.start.character -= HeuristicPrefix.Name.size();
+    InsertRange.start = InsertRange.end = Pos;
+    InsertRange.start.character -= HeuristicPrefix.Name.size();
 
     llvm::StringMap<SourceParams> ProxSources;
     ProxSources[FileName].Cost = 0;
@@ -1838,13 +1838,13 @@ class CodeCompleteFlow {
     // Then the range will be invalid and we will be doing insertion, use
     // current cursor position in such cases as range.
     if (CodeCompletionRange.isValid()) {
-      ReplacedRange = halfOpenToRange(Recorder->CCSema->getSourceManager(),
-                                      CodeCompletionRange);
+      InsertRange = halfOpenToRange(Recorder->CCSema->getSourceManager(),
+                                    CodeCompletionRange);
     } else {
       const auto &Pos = sourceLocToPosition(
           Recorder->CCSema->getSourceManager(),
           Recorder->CCSema->getPreprocessor().getCodeCompletionLoc());
-      ReplacedRange.start = ReplacedRange.end = Pos;
+      InsertRange.start = InsertRange.end = Pos;
     }
     Filter = FuzzyMatcher(
         Recorder->CCSema->getPreprocessor().getCodeCompletionFilter());
@@ -1885,7 +1885,7 @@ class CodeCompleteFlow {
     for (auto &C : Scored) {
       Output.Completions.push_back(toCodeCompletion(C.first));
       Output.Completions.back().Score = C.second;
-      Output.Completions.back().CompletionTokenRange = ReplacedRange;
+      Output.Completions.back().CompletionInsertRange = InsertRange;
       if (Opts.Index && !Output.Completions.back().Documentation) {
         for (auto &Cand : C.first) {
           if (Cand.SemaResult &&
@@ -1909,7 +1909,7 @@ class CodeCompleteFlow {
     }
     Output.HasMore = Incomplete;
     Output.Context = CCContextKind;
-    Output.CompletionRange = ReplacedRange;
+    Output.InsertRange = InsertRange;
 
     // Look up documentation from the index.
     if (Opts.Index) {
@@ -2231,9 +2231,10 @@ CompletionPrefix guessCompletionPrefix(llvm::StringRef 
Content,
 }
 
 // Code complete the argument name on "/*" inside function call.
-// Offset should be pointing to the start of the comment, i.e.:
+// OutsideStartOffset should be pointing before the comment, i.e.:
 // foo(^/*, rather than foo(/*^) where the cursor probably is.
-CodeCompleteResult codeCompleteComment(PathRef FileName, unsigned Offset,
+CodeCompleteResult codeCompleteComment(PathRef FileName,
+                                       unsigned OutsideStartOffset,
                                        llvm::StringRef Prefix,
                                        const PreambleData *Preamble,
                                        const ParseInputs &ParseInput) {
@@ -2250,20 +2251,20 @@ CodeCompleteResult codeCompleteComment(PathRef 
FileName, unsigned Offset,
   // full patch.
   semaCodeComplete(
       std::make_unique<ParamNameCollector>(Options, ParamNames), Options,
-      {FileName, Offset, *Preamble,
+      {FileName, OutsideStartOffset, *Preamble,
        PreamblePatch::createFullPatch(FileName, ParseInput, *Preamble),
        ParseInput});
   if (ParamNames.empty())
     return CodeCompleteResult();
 
   CodeCompleteResult Result;
-  Range CompletionRange;
+  Range InsertRange;
   // Skip /*
-  Offset += 2;
-  CompletionRange.start = offsetToPosition(ParseInput.Contents, Offset);
-  CompletionRange.end =
-      offsetToPosition(ParseInput.Contents, Offset + Prefix.size());
-  Result.CompletionRange = CompletionRange;
+  OutsideStartOffset += 2;
+  InsertRange.start = offsetToPosition(ParseInput.Contents, 
OutsideStartOffset);
+  InsertRange.end =
+      offsetToPosition(ParseInput.Contents, OutsideStartOffset + 
Prefix.size());
+  Result.InsertRange = InsertRange;
   Result.Context = CodeCompletionContext::CCC_NaturalLanguage;
   for (llvm::StringRef Name : ParamNames) {
     if (!Name.starts_with(Prefix))
@@ -2272,7 +2273,7 @@ CodeCompleteResult codeCompleteComment(PathRef FileName, 
unsigned Offset,
     Item.Name = Name.str() + "=*/";
     Item.FilterText = Item.Name;
     Item.Kind = CompletionItemKind::Text;
-    Item.CompletionTokenRange = CompletionRange;
+    Item.CompletionInsertRange = InsertRange;
     Item.Origin = SymbolOrigin::AST;
     Result.Completions.push_back(Item);
   }
@@ -2423,7 +2424,7 @@ CompletionItem CodeCompletion::render(const 
CodeCompleteOptions &Opts) const {
   }
   LSP.sortText = sortText(Score.Total, FilterText);
   LSP.filterText = FilterText;
-  LSP.textEdit = {CompletionTokenRange, RequiredQualifier + Name, ""};
+  LSP.textEdit = {CompletionInsertRange, RequiredQualifier + Name, ""};
   // Merge continuous additionalTextEdits into main edit. The main motivation
   // behind this is to help LSP clients, it seems most of them are confused 
when
   // they are provided with additionalTextEdits that are consecutive to main
diff --git a/clang-tools-extra/clangd/CodeComplete.h 
b/clang-tools-extra/clangd/CodeComplete.h
index cde22a8212e6a..a6e190e2e3413 100644
--- a/clang-tools-extra/clangd/CodeComplete.h
+++ b/clang-tools-extra/clangd/CodeComplete.h
@@ -222,7 +222,7 @@ struct CodeCompletion {
   std::vector<TextEdit> FixIts;
 
   /// Holds the range of the token we are going to replace with this 
completion.
-  Range CompletionTokenRange;
+  Range CompletionInsertRange;
 
   // Scores are used to rank completion items.
   struct Scores {
@@ -262,7 +262,7 @@ struct CodeCompleteResult {
   // Example: foo.pb^ -> foo.push_back()
   //              ~~
   // Typically matches the textEdit.range of Completions, but not guaranteed 
to.
-  std::optional<Range> CompletionRange;
+  std::optional<Range> InsertRange;
   // Usually the source will be parsed with a real C++ parser.
   // But heuristics may be used instead if e.g. the preamble is not ready.
   bool RanParser = true;
diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp 
b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
index 31f2d8bd68703..898481b464fcb 100644
--- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
@@ -92,7 +92,7 @@ MATCHER_P(snippetSuffix, Text, "") { return arg.SnippetSuffix 
== Text; }
 MATCHER_P(origin, OriginSet, "") { return arg.Origin == OriginSet; }
 MATCHER_P(signature, S, "") { return arg.Signature == S; }
 MATCHER_P(replacesRange, Range, "") {
-  return arg.CompletionTokenRange == Range;
+  return arg.CompletionInsertRange == Range;
 }
 
 // Shorthand for Contains(named(Name)).
@@ -2520,7 +2520,7 @@ TEST(CompletionTest, RenderWithFixItMerged) {
   C.Name = "x";
   C.RequiredQualifier = "Foo::";
   C.FixIts = {FixIt};
-  C.CompletionTokenRange.start.character = 5;
+  C.CompletionInsertRange.start.character = 5;
 
   CodeCompleteOptions Opts;
   Opts.IncludeFixIts = true;
@@ -2540,7 +2540,7 @@ TEST(CompletionTest, RenderWithFixItNonMerged) {
   C.Name = "x";
   C.RequiredQualifier = "Foo::";
   C.FixIts = {FixIt};
-  C.CompletionTokenRange.start.character = 5;
+  C.CompletionInsertRange.start.character = 5;
 
   CodeCompleteOptions Opts;
   Opts.IncludeFixIts = true;
@@ -2551,7 +2551,7 @@ TEST(CompletionTest, RenderWithFixItNonMerged) {
   EXPECT_THAT(R.additionalTextEdits, UnorderedElementsAre(FixIt));
 }
 
-TEST(CompletionTest, CompletionTokenRange) {
+TEST(CompletionTest, CompletionInsertRange) {
   MockFS FS;
   MockCompilationDatabase CDB;
   TestTU TU;
@@ -2593,7 +2593,7 @@ TEST(CompletionTest, CompletionTokenRange) {
       ADD_FAILURE() << "Results.Completions.size() != 1" << Text;
       continue;
     }
-    EXPECT_THAT(Results.Completions.front().CompletionTokenRange,
+    EXPECT_THAT(Results.Completions.front().CompletionInsertRange,
                 TestCode.range());
   }
 }
@@ -3981,23 +3981,23 @@ TEST(CompletionTest, DelayedTemplateParsing) {
 TEST(CompletionTest, CompletionRange) {
   const char *WithRange = "auto x = [[abc]]^";
   auto Completions = completions(WithRange);
-  EXPECT_EQ(Completions.CompletionRange, Annotations(WithRange).range());
+  EXPECT_EQ(Completions.InsertRange, Annotations(WithRange).range());
   Completions = completionsNoCompile(WithRange);
-  EXPECT_EQ(Completions.CompletionRange, Annotations(WithRange).range());
+  EXPECT_EQ(Completions.InsertRange, Annotations(WithRange).range());
 
   const char *EmptyRange = "auto x = [[]]^";
   Completions = completions(EmptyRange);
-  EXPECT_EQ(Completions.CompletionRange, Annotations(EmptyRange).range());
+  EXPECT_EQ(Completions.InsertRange, Annotations(EmptyRange).range());
   Completions = completionsNoCompile(EmptyRange);
-  EXPECT_EQ(Completions.CompletionRange, Annotations(EmptyRange).range());
+  EXPECT_EQ(Completions.InsertRange, Annotations(EmptyRange).range());
 
   // Sema doesn't trigger at all here, while the no-sema completion runs
   // heuristics as normal and reports a range. It'd be nice to be consistent.
   const char *NoCompletion = "/* foo [[]]^ */";
   Completions = completions(NoCompletion);
-  EXPECT_EQ(Completions.CompletionRange, std::nullopt);
+  EXPECT_EQ(Completions.InsertRange, std::nullopt);
   Completions = completionsNoCompile(NoCompletion);
-  EXPECT_EQ(Completions.CompletionRange, Annotations(NoCompletion).range());
+  EXPECT_EQ(Completions.InsertRange, Annotations(NoCompletion).range());
 }
 
 TEST(NoCompileCompletionTest, Basic) {
@@ -4300,7 +4300,7 @@ TEST(CompletionTest, CommentParamName) {
   {
     std::string CompletionRangeTest(Code + "fun(/*[[^]]");
     auto Results = completions(CompletionRangeTest);
-    EXPECT_THAT(Results.CompletionRange,
+    EXPECT_THAT(Results.InsertRange,
                 llvm::ValueIs(Annotations(CompletionRangeTest).range()));
     EXPECT_THAT(
         Results.Completions,
@@ -4311,7 +4311,7 @@ TEST(CompletionTest, CommentParamName) {
   {
     std::string CompletionRangeTest(Code + "fun(/*[[fo^]]");
     auto Results = completions(CompletionRangeTest);
-    EXPECT_THAT(Results.CompletionRange,
+    EXPECT_THAT(Results.InsertRange,
                 llvm::ValueIs(Annotations(CompletionRangeTest).range()));
     EXPECT_THAT(
         Results.Completions,

>From 1f22f0b341f4d211b2b429554302d001ad463afa Mon Sep 17 00:00:00 2001
From: argothiel <[email protected]>
Date: Fri, 20 Mar 2026 01:06:42 +0100
Subject: [PATCH 2/3] [clangd] Add insertReplaceSupport for code completion

Handle new insertReplaceSupport capability (LSP 3.16). Add
InsertReplaceEdit to the protocol layer and pass it around to the code
completion logic. Update CompletionItem::textEdit to become the union
type as per the LSP specification.

Add a new helper function to the Lexer to find the end of the identifier
with full context lexing, to avoid duplicating the logic. Use the helper
both in Sema flow, and in the comment completion flow. Use a simpler
ASCII-only scan in no-compile mode.

Add LIT tests to verify auto-triggered completions, mid-word
replacement, unicode, and snippets. Add unit tests to verify
insert/replace ranges with and without Sema, including comments and the
feature-off case.

Update the release notes to document the new capability.

Fixes https://github.com/clangd/clangd/issues/2190
---
 clang-tools-extra/clangd/ClangdLSPServer.cpp  |   1 +
 clang-tools-extra/clangd/CodeComplete.cpp     | 160 ++++++++++++++---
 clang-tools-extra/clangd/CodeComplete.h       |  12 +-
 clang-tools-extra/clangd/Protocol.cpp         |  13 +-
 clang-tools-extra/clangd/Protocol.h           |  27 ++-
 .../test/completion-auto-trigger-replace.test | 113 ++++++++++++
 .../clangd/test/completion-replace.test       | 166 ++++++++++++++++++
 .../test/completion-snippets-replace.test     |  68 +++++++
 .../clangd/unittests/CodeCompleteTests.cpp    | 128 +++++++++++++-
 clang-tools-extra/docs/ReleaseNotes.rst       |   4 +
 clang/include/clang/Lex/Lexer.h               |   8 +
 clang/lib/Lex/Lexer.cpp                       |  22 +++
 clang/unittests/Lex/LexerTest.cpp             |  34 ++++
 13 files changed, 724 insertions(+), 32 deletions(-)
 create mode 100644 
clang-tools-extra/clangd/test/completion-auto-trigger-replace.test
 create mode 100644 clang-tools-extra/clangd/test/completion-replace.test
 create mode 100644 
clang-tools-extra/clangd/test/completion-snippets-replace.test

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp 
b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index ebd42abd2dd61..65da685be1278 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -518,6 +518,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams 
&Params,
 
   Opts.CodeComplete.EnableSnippets = Params.capabilities.CompletionSnippets;
   Opts.CodeComplete.IncludeFixIts = Params.capabilities.CompletionFixes;
+  Opts.CodeComplete.EnableInsertReplace = Params.capabilities.InsertReplace;
   if (!Opts.CodeComplete.BundleOverloads)
     Opts.CodeComplete.BundleOverloads = Params.capabilities.HasSignatureHelp;
   Opts.CodeComplete.DocumentationFormat =
diff --git a/clang-tools-extra/clangd/CodeComplete.cpp 
b/clang-tools-extra/clangd/CodeComplete.cpp
index 6bbd35c648276..c58ef139b7437 100644
--- a/clang-tools-extra/clangd/CodeComplete.cpp
+++ b/clang-tools-extra/clangd/CodeComplete.cpp
@@ -1382,14 +1382,17 @@ void loadMainFilePreambleMacros(const Preprocessor &PP,
 bool semaCodeComplete(std::unique_ptr<CodeCompleteConsumer> Consumer,
                       const clang::CodeCompleteOptions &Options,
                       const SemaCompleteInput &Input,
-                      IncludeStructure *Includes = nullptr) {
+                      IncludeStructure *Includes = nullptr,
+                      std::unique_ptr<CompilerInvocation> CI = nullptr) {
   trace::Span Tracer("Sema completion");
 
   IgnoreDiagnostics IgnoreDiags;
-  auto CI = buildCompilerInvocation(Input.ParseInput, IgnoreDiags);
   if (!CI) {
-    elog("Couldn't create CompilerInvocation");
-    return false;
+    CI = buildCompilerInvocation(Input.ParseInput, IgnoreDiags);
+    if (!CI) {
+      elog("Couldn't create CompilerInvocation");
+      return false;
+    }
   }
   auto &FrontendOpts = CI->getFrontendOpts();
   FrontendOpts.SkipFunctionBodies = true;
@@ -1616,6 +1619,7 @@ class CodeCompleteFlow {
   CompletionPrefix HeuristicPrefix;
   std::optional<FuzzyMatcher> Filter; // Initialized once Sema runs.
   Range InsertRange;
+  std::optional<Range> ReplaceRange;
   std::vector<std::string> QueryScopes;      // Initialized once Sema runs.
   std::vector<std::string> AccessibleScopes; // Initialized once Sema runs.
   // Initialized once QueryScopes is initialized, if there are scopes.
@@ -1753,6 +1757,17 @@ class CodeCompleteFlow {
     InsertRange.start = InsertRange.end = Pos;
     InsertRange.start.character -= HeuristicPrefix.Name.size();
 
+    if (Opts.EnableInsertReplace) {
+      ReplaceRange.emplace();
+      ReplaceRange->start = InsertRange.start;
+      // Scan forward past ASCII identifier characters to find replace end.
+      size_t ReplaceEnd = Offset;
+      while (ReplaceEnd < Content.size() &&
+             isAsciiIdentifierContinue(Content[ReplaceEnd]))
+        ++ReplaceEnd;
+      ReplaceRange->end = offsetToPosition(Content, ReplaceEnd);
+    }
+
     llvm::StringMap<SourceParams> ProxSources;
     ProxSources[FileName].Cost = 0;
     FileProximity.emplace(ProxSources);
@@ -1832,20 +1847,27 @@ class CodeCompleteFlow {
   CodeCompleteResult runWithSema() {
     const auto &CodeCompletionRange = CharSourceRange::getCharRange(
         Recorder->CCSema->getPreprocessor().getCodeCompletionTokenRange());
+
+    const SourceManager &SM = Recorder->CCSema->getSourceManager();
+
     // When we are getting completions with an empty identifier, for example
     //    std::vector<int> asdf;
     //    asdf.^;
     // Then the range will be invalid and we will be doing insertion, use
     // current cursor position in such cases as range.
     if (CodeCompletionRange.isValid()) {
-      InsertRange = halfOpenToRange(Recorder->CCSema->getSourceManager(),
-                                    CodeCompletionRange);
+      InsertRange = halfOpenToRange(SM, CodeCompletionRange);
     } else {
       const auto &Pos = sourceLocToPosition(
-          Recorder->CCSema->getSourceManager(),
-          Recorder->CCSema->getPreprocessor().getCodeCompletionLoc());
+          SM, Recorder->CCSema->getPreprocessor().getCodeCompletionLoc());
       InsertRange.start = InsertRange.end = Pos;
     }
+
+    if (Opts.EnableInsertReplace) {
+      ReplaceRange.emplace();
+      ReplaceRange->start = InsertRange.start;
+      ReplaceRange->end = getEndOfCodeCompletionReplace(SM);
+    }
     Filter = FuzzyMatcher(
         Recorder->CCSema->getPreprocessor().getCodeCompletionFilter());
     auto SpecifiedScopes = getQueryScopes(
@@ -1874,6 +1896,26 @@ class CodeCompleteFlow {
     return toCodeCompleteResult(Top);
   }
 
+  // Returns the LSP position at the end of the identifier suffix after the
+  // code completion cursor.
+  Position getEndOfCodeCompletionReplace(const SourceManager &SM) {
+    const Preprocessor &PP = Recorder->CCSema->getPreprocessor();
+    const LangOptions &LangOpts = Recorder->CCSema->getLangOpts();
+
+    // Skip past the code completion NUL byte and scan forward through
+    // identifier continuation characters (letters, digits, _, $, UCN,
+    // unicode). This handles all cases uniformly: with prefix ("vac^1abc"),
+    // without prefix ("vec.^asdf"), and digit-starting ("vec.^1abc").
+    const SourceLocation SuffixBegin =
+        PP.getCodeCompletionLoc().getLocWithOffset(1);
+    Position End = sourceLocToPosition(
+        SM, Lexer::findEndOfIdentifierContinuation(SuffixBegin, SM, LangOpts));
+    // Adjust for the NUL byte inserted at the cursor by code completion,
+    // which inflates the column by 1.
+    End.character--;
+    return End;
+  }
+
   CodeCompleteResult
   toCodeCompleteResult(const std::vector<ScoredBundle> &Scored) {
     CodeCompleteResult Output;
@@ -1886,6 +1928,7 @@ class CodeCompleteFlow {
       Output.Completions.push_back(toCodeCompletion(C.first));
       Output.Completions.back().Score = C.second;
       Output.Completions.back().CompletionInsertRange = InsertRange;
+      Output.Completions.back().CompletionReplaceRange = ReplaceRange;
       if (Opts.Index && !Output.Completions.back().Documentation) {
         for (auto &Cand : C.first) {
           if (Cand.SemaResult &&
@@ -1910,6 +1953,7 @@ class CodeCompleteFlow {
     Output.HasMore = Incomplete;
     Output.Context = CCContextKind;
     Output.InsertRange = InsertRange;
+    Output.ReplaceRange = ReplaceRange;
 
     // Look up documentation from the index.
     if (Opts.Index) {
@@ -2230,17 +2274,54 @@ CompletionPrefix guessCompletionPrefix(llvm::StringRef 
Content,
   return Result;
 }
 
+// If Offset is inside what looks like argument comment (e.g.
+// "/*^*/" or "/* foo = ^*/"), returns the offset pointing past the closing 
"*/".
+static std::optional<unsigned>
+maybeFunctionArgumentCommentEnd(const PathRef FileName, const unsigned Offset,
+                                const llvm::StringRef Content,
+                                const LangOptions &LangOpts) {
+  if (Offset > Content.size())
+    return std::nullopt;
+
+  SourceManagerForFile FileSM(FileName, Content);
+  const SourceManager &SM = FileSM.get();
+  const SourceLocation Cursor =
+      SM.getComposedLoc(SM.getMainFileID(), static_cast<unsigned>(Offset));
+  const SourceLocation EndOfSuffix =
+      Lexer::findEndOfIdentifierContinuation(Cursor, SM, LangOpts);
+  const unsigned EndOfSuffixOffset = SM.getFileOffset(EndOfSuffix);
+
+  const llvm::StringRef Rest = Content.drop_front(EndOfSuffixOffset);
+  llvm::StringRef RestTrimmed = Rest.ltrim();
+  // Comment argument pattern: `/* name = */` — skip past optional `=`.
+  if (RestTrimmed.starts_with("="))
+    RestTrimmed = RestTrimmed.drop_front(1).ltrim();
+  if (RestTrimmed.starts_with("*/"))
+    return EndOfSuffixOffset + (Rest.size() - RestTrimmed.size()) + 2;
+  return std::nullopt;
+}
+
 // Code complete the argument name on "/*" inside function call.
 // OutsideStartOffset should be pointing before the comment, i.e.:
 // foo(^/*, rather than foo(/*^) where the cursor probably is.
-CodeCompleteResult codeCompleteComment(PathRef FileName,
-                                       unsigned OutsideStartOffset,
-                                       llvm::StringRef Prefix,
-                                       const PreambleData *Preamble,
-                                       const ParseInputs &ParseInput) {
+CodeCompleteResult
+codeCompleteComment(PathRef FileName, const unsigned CursorOffset,
+                    unsigned OutsideStartOffset, llvm::StringRef Prefix,
+                    const PreambleData *Preamble, const ParseInputs 
&ParseInput,
+                    const CodeCompleteOptions &Opts) {
   if (Preamble == nullptr) // Can't run without Sema.
     return CodeCompleteResult();
 
+  IgnoreDiagnostics IgnoreDiags;
+  auto CI = buildCompilerInvocation(ParseInput, IgnoreDiags);
+  if (!CI)
+    return CodeCompleteResult();
+
+  std::optional<unsigned> OutsideEndOffset;
+  if (Opts.EnableInsertReplace)
+    OutsideEndOffset = maybeFunctionArgumentCommentEnd(
+        FileName, CursorOffset, ParseInput.Contents, CI->getLangOpts());
+
   clang::CodeCompleteOptions Options;
   Options.IncludeGlobals = false;
   Options.IncludeMacros = false;
@@ -2253,18 +2334,29 @@ CodeCompleteResult codeCompleteComment(PathRef FileName,
       std::make_unique<ParamNameCollector>(Options, ParamNames), Options,
       {FileName, OutsideStartOffset, *Preamble,
        PreamblePatch::createFullPatch(FileName, ParseInput, *Preamble),
-       ParseInput});
+       ParseInput},
+      /*Includes=*/nullptr, std::move(CI));
   if (ParamNames.empty())
     return CodeCompleteResult();
 
   CodeCompleteResult Result;
   Range InsertRange;
   // Skip /*
-  OutsideStartOffset += 2;
-  InsertRange.start = offsetToPosition(ParseInput.Contents, 
OutsideStartOffset);
+  const unsigned InsideStartOffset = OutsideStartOffset + 2;
+  InsertRange.start = offsetToPosition(ParseInput.Contents, InsideStartOffset);
   InsertRange.end =
-      offsetToPosition(ParseInput.Contents, OutsideStartOffset + 
Prefix.size());
+      offsetToPosition(ParseInput.Contents, InsideStartOffset + Prefix.size());
   Result.InsertRange = InsertRange;
+
+  if (Opts.EnableInsertReplace) {
+    Range ReplaceRange;
+    ReplaceRange.start = InsertRange.start;
+    ReplaceRange.end = OutsideEndOffset ? offsetToPosition(ParseInput.Contents,
+                                                           *OutsideEndOffset)
+                                        : InsertRange.end;
+    Result.ReplaceRange = ReplaceRange;
+  }
+
   Result.Context = CodeCompletionContext::CCC_NaturalLanguage;
   for (llvm::StringRef Name : ParamNames) {
     if (!Name.starts_with(Prefix))
@@ -2274,6 +2366,7 @@ CodeCompleteResult codeCompleteComment(PathRef FileName,
     Item.FilterText = Item.Name;
     Item.Kind = CompletionItemKind::Text;
     Item.CompletionInsertRange = InsertRange;
+    Item.CompletionReplaceRange = Result.ReplaceRange;
     Item.Origin = SymbolOrigin::AST;
     Result.Completions.push_back(Item);
   }
@@ -2313,8 +2406,8 @@ CodeCompleteResult codeComplete(PathRef FileName, 
Position Pos,
     // parsing, so we must move back the position before running it, extract
     // information we need and construct completion items ourselves.
     auto CommentPrefix = Content.substr(*OffsetBeforeComment + 2).trim();
-    return codeCompleteComment(FileName, *OffsetBeforeComment, CommentPrefix,
-                               Preamble, ParseInput);
+    return codeCompleteComment(FileName, *Offset, *OffsetBeforeComment,
+                               CommentPrefix, Preamble, ParseInput, Opts);
   }
 
   auto Flow = CodeCompleteFlow(
@@ -2424,7 +2517,9 @@ CompletionItem CodeCompletion::render(const 
CodeCompleteOptions &Opts) const {
   }
   LSP.sortText = sortText(Score.Total, FilterText);
   LSP.filterText = FilterText;
-  LSP.textEdit = {CompletionInsertRange, RequiredQualifier + Name, ""};
+  TextEdit Edit;
+  Edit.range = CompletionInsertRange;
+  Edit.newText = RequiredQualifier + Name;
   // Merge continuous additionalTextEdits into main edit. The main motivation
   // behind this is to help LSP clients, it seems most of them are confused 
when
   // they are provided with additionalTextEdits that are consecutive to main
@@ -2433,19 +2528,34 @@ CompletionItem CodeCompletion::render(const 
CodeCompleteOptions &Opts) const {
   // is mainly to help LSP clients again, so that changes do not effect each
   // other.
   for (const auto &FixIt : FixIts) {
-    if (FixIt.range.end == LSP.textEdit->range.start) {
-      LSP.textEdit->newText = FixIt.newText + LSP.textEdit->newText;
-      LSP.textEdit->range.start = FixIt.range.start;
+    if (FixIt.range.end == Edit.range.start) {
+      Edit.newText = FixIt.newText + Edit.newText;
+      Edit.range.start = FixIt.range.start;
     } else {
       LSP.additionalTextEdits.push_back(FixIt);
     }
   }
   if (Opts.EnableSnippets)
-    LSP.textEdit->newText += SnippetSuffix;
+    Edit.newText += SnippetSuffix;
 
   // FIXME(kadircet): Do not even fill insertText after making sure textEdit is
   // compatible with most of the editors.
-  LSP.insertText = LSP.textEdit->newText;
+  LSP.insertText = Edit.newText;
+  if (Opts.EnableInsertReplace) {
+    assert(CompletionReplaceRange &&
+           "CompletionReplaceRange must be already set before render() "
+           "when EnableInsertReplace is on");
+    InsertReplaceEdit IRE;
+    IRE.newText = std::move(Edit.newText);
+    IRE.insert = Edit.range;
+    IRE.replace = *CompletionReplaceRange;
+    // FixIt merging may have extended the insert range start; keep replace
+    // range as a superset per LSP spec.
+    IRE.replace.start = IRE.insert.start;
+    LSP.textEdit = std::move(IRE);
+  } else {
+    LSP.textEdit = std::move(Edit);
+  }
   // Some clients support snippets but work better with plaintext.
   // So if the snippet is trivial, let the client know.
   // https://github.com/clangd/clangd/issues/922
diff --git a/clang-tools-extra/clangd/CodeComplete.h 
b/clang-tools-extra/clangd/CodeComplete.h
index a6e190e2e3413..99ae48c30907c 100644
--- a/clang-tools-extra/clangd/CodeComplete.h
+++ b/clang-tools-extra/clangd/CodeComplete.h
@@ -71,6 +71,10 @@ struct CodeCompleteOptions {
   /// Whether to present doc comments as plain-text or markdown.
   MarkupKind DocumentationFormat = MarkupKind::PlainText;
 
+  /// Whether to present the completion as a single textEdit range or as two
+  /// ranges (insert/replace).
+  bool EnableInsertReplace = false;
+
   Config::HeaderInsertionPolicy InsertIncludes =
       Config::HeaderInsertionPolicy::IWYU;
 
@@ -223,6 +227,8 @@ struct CodeCompletion {
 
   /// Holds the range of the token we are going to replace with this 
completion.
   Range CompletionInsertRange;
+  /// If set, the range to use when the client's insert mode is "replace".
+  std::optional<Range> CompletionReplaceRange;
 
   // Scores are used to rank completion items.
   struct Scores {
@@ -261,8 +267,12 @@ struct CodeCompleteResult {
   // The text that is being directly completed.
   // Example: foo.pb^ -> foo.push_back()
   //              ~~
-  // Typically matches the textEdit.range of Completions, but not guaranteed 
to.
+  // Typically matches the textEdit.range (or textEdit.insert range) of
+  // Completions, but not guaranteed to.
   std::optional<Range> InsertRange;
+  // If not empty, typically matches the textEdit.replace range of Completions,
+  // but not guaranteed to.
+  std::optional<Range> ReplaceRange;
   // Usually the source will be parsed with a real C++ parser.
   // But heuristics may be used instead if e.g. the preamble is not ready.
   bool RanParser = true;
diff --git a/clang-tools-extra/clangd/Protocol.cpp 
b/clang-tools-extra/clangd/Protocol.cpp
index 793db7b052990..f77b0773d445a 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -202,6 +202,14 @@ llvm::json::Value toJSON(const TextEdit &P) {
   return Result;
 }
 
+llvm::json::Value toJSON(const InsertReplaceEdit &P) {
+  return llvm::json::Object{
+      {"newText", P.newText},
+      {"insert", P.insert},
+      {"replace", P.replace},
+  };
+}
+
 bool fromJSON(const llvm::json::Value &Params, ChangeAnnotation &R,
               llvm::json::Path P) {
   llvm::json::ObjectMapper O(Params, P);
@@ -414,6 +422,8 @@ bool fromJSON(const llvm::json::Value &Params, 
ClientCapabilities &R,
               break;
           }
         }
+        if (auto IRSupport = Item->getBoolean("insertReplaceSupport"))
+          R.InsertReplace = *IRSupport;
       }
       if (auto *ItemKind = Completion->getObject("completionItemKind")) {
         if (auto *ValueSet = ItemKind->get("valueSet")) {
@@ -1184,7 +1194,8 @@ llvm::json::Value toJSON(const CompletionItem &CI) {
   if (CI.insertTextFormat != InsertTextFormat::Missing)
     Result["insertTextFormat"] = static_cast<int>(CI.insertTextFormat);
   if (CI.textEdit)
-    Result["textEdit"] = *CI.textEdit;
+    Result["textEdit"] = std::visit(
+        [](const auto &V) { return llvm::json::Value(V); }, *CI.textEdit);
   if (!CI.additionalTextEdits.empty())
     Result["additionalTextEdits"] = llvm::json::Array(CI.additionalTextEdits);
   if (CI.deprecated)
diff --git a/clang-tools-extra/clangd/Protocol.h 
b/clang-tools-extra/clangd/Protocol.h
index 7a99721a1e856..9c1bb9d9bb059 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -34,6 +34,7 @@
 #include <memory>
 #include <optional>
 #include <string>
+#include <variant>
 #include <vector>
 
 // This file is using the LSP syntax for identifier names which is different
@@ -261,6 +262,18 @@ bool fromJSON(const llvm::json::Value &, TextEdit &, 
llvm::json::Path);
 llvm::json::Value toJSON(const TextEdit &);
 llvm::raw_ostream &operator<<(llvm::raw_ostream &, const TextEdit &);
 
+struct InsertReplaceEdit {
+  /// The string to be inserted.
+  std::string newText;
+
+  /// The range if the insert is requested.
+  Range insert;
+
+  /// The range if the replace is requested.
+  Range replace;
+};
+llvm::json::Value toJSON(const InsertReplaceEdit &);
+
 struct ChangeAnnotation {
   /// A human-readable string describing the actual change. The string
   /// is rendered prominent in the user interface.
@@ -510,6 +523,10 @@ struct ClientCapabilities {
   /// textDocument.completion.completionItem.documentationFormat
   MarkupKind CompletionDocumentationFormat = MarkupKind::PlainText;
 
+  /// Client supports insert replace edit to control different behavior if a
+  /// completion item is inserted in the text or should replace text.
+  bool InsertReplace = false;
+
   /// The client has support for completion item label details.
   /// textDocument.completion.completionItem.labelDetailsSupport.
   bool CompletionLabelDetail = false;
@@ -1372,9 +1389,13 @@ struct CompletionItem {
   /// An edit which is applied to a document when selecting this completion.
   /// When an edit is provided `insertText` is ignored.
   ///
-  /// Note: The range of the edit must be a single line range and it must
-  /// contain the position at which completion has been requested.
-  std::optional<TextEdit> textEdit;
+  /// Note 1: The text edit's range as well as both ranges from an insert
+  /// replace edit must be a single line range and must contain the position
+  /// at which completion has been requested.
+  /// Note 2: If an `InsertReplaceEdit` is returned, the edit's insert range
+  /// must be a prefix of the edit's replace range, meaning it must be
+  /// contained in and starting at the same position.
+  std::optional<std::variant<TextEdit, InsertReplaceEdit>> textEdit;
 
   /// An optional array of additional text edits that are applied when 
selecting
   /// this completion. Edits must not overlap with the main edit nor with
diff --git a/clang-tools-extra/clangd/test/completion-auto-trigger-replace.test 
b/clang-tools-extra/clangd/test/completion-auto-trigger-replace.test
new file mode 100644
index 0000000000000..7201171c47641
--- /dev/null
+++ b/clang-tools-extra/clangd/test/completion-auto-trigger-replace.test
@@ -0,0 +1,113 @@
+# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s
+# Tests InsertReplaceEdit with auto-triggered completions.
+{
+  "jsonrpc": "2.0",
+  "id": 0,
+  "method": "initialize",
+  "params": {
+    "processId": 123,
+    "rootPath": "clangd",
+    "capabilities": {
+      "textDocument": {
+        "completion": {
+          "completionItem": {
+            "insertReplaceSupport": true
+          }
+        }
+      }
+    },
+    "trace": "off"
+  }
+}
+---
+{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"struct
 vector { int size; };\nvoid test(vector *a, vector *b) {\n  if (a > b) {} \n  
a->size = 10;\n  a->\n}"}}}
+---
+# Case 1 (rejected trigger): ">" in "a > b" is comparison, not "->".
+# insertReplaceSupport doesn't break trigger rejection.
+{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":9},"context":{"triggerKind":2,"triggerCharacter":">"}}}
+#      CHECK:  "id": 1,
+# CHECK-NEXT:  "jsonrpc": "2.0"
+# CHECK-NEXT:  "result": {
+# CHECK-NEXT:    "isIncomplete": false,
+# CHECK-NEXT:    "items": []
+# CHECK-NEXT:  }
+---
+# Case 2 (trigger with word after cursor): "a->^size = 10;"
+# Cursor right after "->", word "size" follows. Insert range is empty.
+# insert: [5,5], replace: [5,9]
+{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":3,"character":5},"context":{"triggerKind":2,"triggerCharacter":">"}}}
+#      CHECK:  "id": 2,
+# CHECK-NEXT:  "jsonrpc": "2.0"
+# CHECK-NEXT:  "result": {
+# CHECK-NEXT:    "isIncomplete": false,
+# CHECK-NEXT:    "items": [
+# CHECK-NEXT:       {
+# CHECK-NEXT:        "detail": "int",
+# CHECK-NEXT:        "filterText": "size",
+# CHECK-NEXT:        "insertText": "size",
+# CHECK-NEXT:        "insertTextFormat": 1,
+# CHECK-NEXT:        "kind": 5,
+# CHECK-NEXT:        "label": " size",
+# CHECK-NEXT:        "score": {{[0-9]+.[0-9]+}},
+# CHECK-NEXT:        "sortText": "{{.*}}size",
+# CHECK-NEXT:        "textEdit": {
+# CHECK-NEXT:          "insert": {
+# CHECK-NEXT:            "end": {
+# CHECK-NEXT:              "character": 5,
+# CHECK-NEXT:              "line": 3
+# CHECK-NEXT:            },
+# CHECK-NEXT:            "start": {
+# CHECK-NEXT:              "character": 5,
+# CHECK-NEXT:              "line": 3
+# CHECK-NEXT:            }
+# CHECK-NEXT:          },
+# CHECK-NEXT:          "newText": "size",
+# CHECK-NEXT:          "replace": {
+# CHECK-NEXT:            "end": {
+# CHECK-NEXT:              "character": 9,
+# CHECK-NEXT:              "line": 3
+# CHECK-NEXT:            },
+# CHECK-NEXT:            "start": {
+# CHECK-NEXT:              "character": 5,
+# CHECK-NEXT:              "line": 3
+# CHECK-NEXT:            }
+# CHECK-NEXT:          }
+# CHECK-NEXT:        }
+# CHECK-NEXT:      }
+# CHECK-NEXT:     ]
+# CHECK-NEXT:   }
+---
+# Case 3 (trigger with nothing after cursor): "a->^"
+# Cursor right after "->", nothing follows. insert == replace.
+# insert: [5,5], replace: [5,5]
+{"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":4,"character":5},"context":{"triggerKind":2,"triggerCharacter":">"}}}
+#      CHECK:  "id": 3,
+# CHECK-NEXT:  "jsonrpc": "2.0"
+# CHECK-NEXT:  "result": {
+# CHECK:      "textEdit": {
+# CHECK-NEXT:          "insert": {
+# CHECK-NEXT:            "end": {
+# CHECK-NEXT:              "character": 5,
+# CHECK-NEXT:              "line": 4
+# CHECK-NEXT:            },
+# CHECK-NEXT:            "start": {
+# CHECK-NEXT:              "character": 5,
+# CHECK-NEXT:              "line": 4
+# CHECK-NEXT:            }
+# CHECK-NEXT:          },
+# CHECK-NEXT:          "newText": {{.*}},
+# CHECK-NEXT:          "replace": {
+# CHECK-NEXT:            "end": {
+# CHECK-NEXT:              "character": 5,
+# CHECK-NEXT:              "line": 4
+# CHECK-NEXT:            },
+# CHECK-NEXT:            "start": {
+# CHECK-NEXT:              "character": 5,
+# CHECK-NEXT:              "line": 4
+# CHECK-NEXT:            }
+# CHECK-NEXT:          }
+# CHECK-NEXT:        }
+---
+{"jsonrpc":"2.0","id":4,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
diff --git a/clang-tools-extra/clangd/test/completion-replace.test 
b/clang-tools-extra/clangd/test/completion-replace.test
new file mode 100644
index 0000000000000..52902b7d2f77f
--- /dev/null
+++ b/clang-tools-extra/clangd/test/completion-replace.test
@@ -0,0 +1,166 @@
+# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s
+# RUN: clangd -lit-test -pch-storage=memory < %s | FileCheck 
-strict-whitespace %s
+# Tests InsertReplaceEdit ranges when insertReplaceSupport is true.
+# insert range = [token_start, cursor), replace range = [token_start, 
token_end).
+{
+  "jsonrpc": "2.0",
+  "id": 0,
+  "method": "initialize",
+  "params": {
+    "processId": 123,
+    "rootPath": "clangd",
+    "capabilities": {
+      "textDocument": {
+        "completion": {
+          "completionItem": {
+            "insertReplaceSupport": true
+          }
+        }
+      }
+    },
+    "trace": "off"
+  }
+}
+---
+# Case 1 (cursor at end of token): "S().a^"
+# Prefix typed, nothing after cursor. insert == replace.
+# insert: [4,5], replace: [4,5]
+{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"struct
 S { int a; int abc; int chrząszcz; };\nint main() {\nS().a;\n}"}}}
+---
+{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":5}}}
+#      CHECK:  "id": 1
+# CHECK-NEXT:  "jsonrpc": "2.0",
+# CHECK-NEXT:  "result": {
+# CHECK:      "filterText": "a",
+# CHECK-NEXT:      "insertText": "a",
+# CHECK:      "textEdit": {
+# CHECK-NEXT:        "insert": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 5,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        },
+# CHECK-NEXT:        "newText": "a",
+# CHECK-NEXT:        "replace": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 5,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        }
+# CHECK-NEXT:      }
+---
+# Case 2 (mid-word cursor): "S().ab^c"
+# Cursor mid-word. Replace range extends past cursor to cover suffix.
+# insert: [4,6], replace: [4,7]
+{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///main.cpp","version":2},"contentChanges":[{"text":"struct
 S { int a; int abc; int chrząszcz; };\nint main() {\nS().abc;\n}"}]}}
+---
+{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":6}}}
+#      CHECK:  "id": 2
+# CHECK-NEXT:  "jsonrpc": "2.0",
+# CHECK-NEXT:  "result": {
+# CHECK:      "filterText": "abc",
+# CHECK-NEXT:      "insertText": "abc",
+# CHECK:      "textEdit": {
+# CHECK-NEXT:        "insert": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 6,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        },
+# CHECK-NEXT:        "newText": "abc",
+# CHECK-NEXT:        "replace": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 7,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        }
+# CHECK-NEXT:      }
+---
+# Case 3 (no prefix, word after cursor): "S().^xyz"
+# Cursor right after dot, entire existing word is suffix. Insert range is 
empty.
+# insert: [4,4], replace: [4,7]
+{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///main.cpp","version":3},"contentChanges":[{"text":"struct
 S { int a; int abc; int chrząszcz; };\nint main() {\nS().xyz;\n}"}]}}
+---
+{"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":4}}}
+#      CHECK:  "id": 3
+# CHECK-NEXT:  "jsonrpc": "2.0",
+# CHECK-NEXT:  "result": {
+# CHECK:      "textEdit": {
+# CHECK-NEXT:        "insert": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        },
+# CHECK-NEXT:        "newText": {{.*}},
+# CHECK-NEXT:        "replace": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 7,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        }
+# CHECK-NEXT:      }
+---
+# Case 4 (unicode mid-word): "S().chrz^ąszcz"
+# Cursor mid-word with multi-byte suffix. Tests that ranges handle unicode.
+# "chrząszcz" is 9 characters starting at column 4.
+# insert: [4,8], replace: [4,13]
+{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///main.cpp","version":4},"contentChanges":[{"text":"struct
 S { int a; int abc; int chrząszcz; };\nint main() {\nS().chrząszcz;\n}"}]}}
+---
+{"jsonrpc":"2.0","id":4,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":8}}}
+#      CHECK:  "id": 4
+# CHECK-NEXT:  "jsonrpc": "2.0",
+# CHECK-NEXT:  "result": {
+# CHECK:      "filterText": "chrząszcz",
+# CHECK-NEXT:      "insertText": "chrząszcz",
+# CHECK:      "textEdit": {
+# CHECK-NEXT:        "insert": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 8,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        },
+# CHECK-NEXT:        "newText": "chrząszcz",
+# CHECK-NEXT:        "replace": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 13,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 4,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        }
+# CHECK-NEXT:      }
+---
+{"jsonrpc":"2.0","id":5,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
diff --git a/clang-tools-extra/clangd/test/completion-snippets-replace.test 
b/clang-tools-extra/clangd/test/completion-snippets-replace.test
new file mode 100644
index 0000000000000..6d694aca0793b
--- /dev/null
+++ b/clang-tools-extra/clangd/test/completion-snippets-replace.test
@@ -0,0 +1,68 @@
+# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s
+# RUN: clangd -lit-test -pch-storage=memory < %s | FileCheck 
-strict-whitespace %s
+{
+  "jsonrpc": "2.0",
+  "id": 0,
+  "method": "initialize",
+  "params": {
+    "processId": 123,
+    "rootPath": "clangd",
+    "capabilities": {
+      "textDocument": {
+        "completion": {
+          "completionItem": {
+            "snippetSupport": true,
+            "insertReplaceSupport": true
+          }
+        }
+      }
+    },
+    "trace": "off"
+  }
+}
+---
+{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"int
 func_with_args(int a, int b);\nint main() {\nfunc_with\n}"}}}
+---
+{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":7}}}
+#      CHECK:  "id": 1
+# CHECK-NEXT:  "jsonrpc": "2.0",
+# CHECK-NEXT:  "result": {
+# CHECK-NEXT:    "isIncomplete": {{.*}}
+# CHECK-NEXT:    "items": [
+# CHECK:           "filterText": "func_with_args",
+# CHECK-NEXT:      "insertText": "func_with_args(${1:int a}, ${2:int b})",
+# CHECK-NEXT:      "insertTextFormat": 2,
+# CHECK-NEXT:      "kind": 3,
+# CHECK-NEXT:      "label": " func_with_args(int a, int b)",
+# CHECK-NEXT:      "score": {{[0-9]+.[0-9]+}},
+# CHECK-NEXT:      "sortText": "{{.*}}func_with_args"
+# CHECK-NEXT:      "textEdit": {
+# CHECK-NEXT:        "insert": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 7,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          },
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 0,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        },
+# CHECK-NEXT:        "newText": "func_with_args(${1:int a}, ${2:int b})",
+# CHECK-NEXT:        "replace": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 9,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          },
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 0,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        }
+# CHECK-NEXT:      }
+# CHECK-NEXT:    }
+# CHECK-NEXT:    ]
+# CHECK-NEXT:  }
+---
+{"jsonrpc":"2.0","id":4,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp 
b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
index 898481b464fcb..f273e188e4056 100644
--- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
@@ -2527,7 +2527,7 @@ TEST(CompletionTest, RenderWithFixItMerged) {
 
   auto R = C.render(Opts);
   EXPECT_TRUE(R.textEdit);
-  EXPECT_EQ(R.textEdit->newText, "->Foo::x");
+  EXPECT_EQ(std::get<TextEdit>(*R.textEdit).newText, "->Foo::x");
   EXPECT_TRUE(R.additionalTextEdits.empty());
 }
 
@@ -2547,7 +2547,7 @@ TEST(CompletionTest, RenderWithFixItNonMerged) {
 
   auto R = C.render(Opts);
   EXPECT_TRUE(R.textEdit);
-  EXPECT_EQ(R.textEdit->newText, "Foo::x");
+  EXPECT_EQ(std::get<TextEdit>(*R.textEdit).newText, "Foo::x");
   EXPECT_THAT(R.additionalTextEdits, UnorderedElementsAre(FixIt));
 }
 
@@ -4000,6 +4000,92 @@ TEST(CompletionTest, CompletionRange) {
   EXPECT_EQ(Completions.InsertRange, Annotations(NoCompletion).range());
 }
 
+TEST(CompletionTest, ReplaceRange) {
+  clangd::CodeCompleteOptions Opts;
+  Opts.EnableInsertReplace = true;
+
+  // Cursor at end of token: insert == replace.
+  const char *EndOfToken =
+      "struct S { int abc; }; void f() { S s; s.[[abc]]^; }";
+  CodeCompleteResult Completions =
+      completions(EndOfToken, /*IndexSymbols=*/{}, Opts);
+  Annotations A(EndOfToken);
+  EXPECT_EQ(Completions.InsertRange, A.range());
+  EXPECT_EQ(Completions.ReplaceRange, A.range());
+
+  // Cursor mid-word: replace extends past cursor.
+  const char *MidWord = "struct S { int abcd; }; void f() { S s; "
+                        "s.$replace[[$insert[[ab^]]cd]]; }";
+  Completions = completions(MidWord, /*IndexSymbols=*/{}, Opts);
+  A = Annotations(MidWord);
+  EXPECT_EQ(Completions.InsertRange, A.range("insert"));
+  EXPECT_EQ(Completions.ReplaceRange, A.range("replace"));
+
+  // Empty prefix: insert range is empty, replace covers the word.
+  const char *EmptyPrefix = "struct S { int abcd; }; void f() { S s; "
+                            "s.$replace[[$insert[[^]]abcd]]; }";
+  Completions = completions(EmptyPrefix, /*IndexSymbols=*/{}, Opts);
+  A = Annotations(EmptyPrefix);
+  EXPECT_EQ(Completions.InsertRange, A.range("insert"));
+  EXPECT_EQ(Completions.ReplaceRange, A.range("replace"));
+
+  // Cursor mid-word with UTF-8 continuation: replace extends past UTF-8.
+  const char *MidWordUTF8 = "struct S { int ab🙂cd; }; void f() { S s; "
+                            "s.$replace[[$insert[[ab^]]🙂cd]]; }";
+  Completions = completions(MidWordUTF8, /*IndexSymbols=*/{}, Opts);
+  A = Annotations(MidWordUTF8);
+  EXPECT_EQ(Completions.InsertRange, A.range("insert"));
+  EXPECT_EQ(Completions.ReplaceRange, A.range("replace"));
+
+  // EnableInsertReplace off: ReplaceRange should not be set.
+  Opts.EnableInsertReplace = false;
+  const char *NoReplace = "auto x = [[abc]]^";
+  Completions = completions(NoReplace, /*IndexSymbols=*/{}, Opts);
+  EXPECT_EQ(Completions.InsertRange, Annotations(NoReplace).range());
+  EXPECT_EQ(Completions.ReplaceRange, std::nullopt);
+}
+
+TEST(CompletionTest, ReplaceRangeNoCompile) {
+  clangd::CodeCompleteOptions Opts;
+  Opts.EnableInsertReplace = true;
+
+  // Cursor at end of token: insert == replace.
+  const char *EndOfToken = "auto x = [[abc]]^";
+  Annotations A(EndOfToken);
+  CodeCompleteResult Results =
+      completionsNoCompile(EndOfToken, /*IndexSymbols=*/{}, Opts);
+  EXPECT_EQ(Results.InsertRange, A.range());
+  EXPECT_EQ(Results.ReplaceRange, A.range());
+
+  // Cursor mid-word: replace extends past cursor.
+  const char *MidWord = "auto x = $replace[[$insert[[ab^]]cd]]";
+  Results = completionsNoCompile(MidWord, /*IndexSymbols=*/{}, Opts);
+  A = Annotations(MidWord);
+  EXPECT_EQ(Results.InsertRange, A.range("insert"));
+  EXPECT_EQ(Results.ReplaceRange, A.range("replace"));
+
+  // Empty prefix: insert range is empty, replace covers the word.
+  const char *EmptyPrefix = "auto x = $replace[[$insert[[^]]abcd]]";
+  Results = completionsNoCompile(EmptyPrefix, /*IndexSymbols=*/{}, Opts);
+  A = Annotations(EmptyPrefix);
+  EXPECT_EQ(Results.InsertRange, A.range("insert"));
+  EXPECT_EQ(Results.ReplaceRange, A.range("replace"));
+
+  // ASCII heuristic stops at non-ASCII: replace doesn't extend past UTF-8.
+  const char *MidWordUTF8 = "auto x = $replace[[$insert[[ab^]]]]🙂cd";
+  Results = completionsNoCompile(MidWordUTF8, /*IndexSymbols=*/{}, Opts);
+  A = Annotations(MidWordUTF8);
+  EXPECT_EQ(Results.InsertRange, A.range("insert"));
+  EXPECT_EQ(Results.ReplaceRange, A.range("replace"));
+
+  // EnableInsertReplace off: ReplaceRange should not be set.
+  Opts.EnableInsertReplace = false;
+  const char *NoReplace = "auto x = [[abc]]^";
+  Results = completionsNoCompile(NoReplace, /*IndexSymbols=*/{}, Opts);
+  EXPECT_EQ(Results.InsertRange, Annotations(NoReplace).range());
+  EXPECT_EQ(Results.ReplaceRange, std::nullopt);
+}
+
 TEST(NoCompileCompletionTest, Basic) {
   auto Results = completionsNoCompile(R"cpp(
     void func() {
@@ -4319,6 +4405,44 @@ TEST(CompletionTest, CommentParamName) {
             AllOf(replacesRange(Annotations(CompletionRangeTest).range()),
                   origin(SymbolOrigin::AST), kind(CompletionItemKind::Text))));
   }
+
+  // Test replace ranges (comment completion replaces up to */).
+  clangd::CodeCompleteOptions ReplaceOpts;
+  ReplaceOpts.EnableInsertReplace = true;
+  {
+    // With */ (no =): replace extends past suffix to */.
+    const std::string NoEquals(Code + "fun(/*$replace[[$insert[[fo^]]o*/]])");
+    const CodeCompleteResult Results = completions(NoEquals, {}, ReplaceOpts);
+    const Annotations A(NoEquals);
+    EXPECT_EQ(Results.InsertRange, A.range("insert"));
+    EXPECT_EQ(Results.ReplaceRange, A.range("replace"));
+  }
+  {
+    // With = and */: replace extends past = to */.
+    const std::string WithEquals(Code +
+                                 "fun(/*$replace[[$insert[[fo^]]o=*/]])");
+    const CodeCompleteResult Results = completions(WithEquals, {}, 
ReplaceOpts);
+    const Annotations A(WithEquals);
+    EXPECT_EQ(Results.InsertRange, A.range("insert"));
+    EXPECT_EQ(Results.ReplaceRange, A.range("replace"));
+  }
+  {
+    // Without */: replace == insert.
+    const std::string NoClose(Code + "fun(/*[[fo^]]");
+    const CodeCompleteResult Results = completions(NoClose, {}, ReplaceOpts);
+    const Annotations A(NoClose);
+    EXPECT_EQ(Results.InsertRange, A.range());
+    EXPECT_EQ(Results.ReplaceRange, A.range());
+  }
+  {
+    // With */ and UTF-8 suffix: replace extends past UTF-8 to */.
+    const std::string WithUTF8(Code +
+                               "fun(/*$replace[[$insert[[fo^]]o🙂=*/]])");
+    const CodeCompleteResult Results = completions(WithUTF8, {}, ReplaceOpts);
+    const Annotations A(WithUTF8);
+    EXPECT_EQ(Results.InsertRange, A.range("insert"));
+    EXPECT_EQ(Results.ReplaceRange, A.range("replace"));
+  }
 }
 
 TEST(CompletionTest, Concepts) {
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst 
b/clang-tools-extra/docs/ReleaseNotes.rst
index c5d0c617a9fd7..3c4d561a5d9ca 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -73,6 +73,10 @@ Code completion
 - Now also provides include files without extension, if they are in a directory
   only called ``include``.
 
+- Added support for ``InsertReplaceEdit`` in code completion (LSP 3.16),
+  allowing clients that advertise ``insertReplaceSupport`` to receive both
+  insert and replace ranges for completion items.
+
 Code actions
 ^^^^^^^^^^^^
 
diff --git a/clang/include/clang/Lex/Lexer.h b/clang/include/clang/Lex/Lexer.h
index 0459a863bc08d..fb65266c8b990 100644
--- a/clang/include/clang/Lex/Lexer.h
+++ b/clang/include/clang/Lex/Lexer.h
@@ -372,6 +372,14 @@ class Lexer : public PreprocessorLexer {
                                      const SourceManager &SM,
                                      const LangOptions &LangOpts);
 
+  /// Finds the end of an identifier-continuation sequence starting at \p Loc.
+  /// This consumes identifier continuation characters (letters, digits,
+  /// underscores, dollar signs if enabled, UCNs, and unicode), and returns
+  /// the source location immediately after the consumed sequence.
+  static SourceLocation
+  findEndOfIdentifierContinuation(SourceLocation Loc, const SourceManager &SM,
+                                  const LangOptions &LangOpts);
+
   /// Relex the token at the specified location.
   /// \returns true if there was a failure, false on success.
   static bool getRawToken(SourceLocation Loc, Token &Result,
diff --git a/clang/lib/Lex/Lexer.cpp b/clang/lib/Lex/Lexer.cpp
index 10246552bb13d..39be64a8427cf 100644
--- a/clang/lib/Lex/Lexer.cpp
+++ b/clang/lib/Lex/Lexer.cpp
@@ -514,6 +514,28 @@ unsigned Lexer::MeasureTokenLength(SourceLocation Loc,
   return TheTok.getLength();
 }
 
+SourceLocation Lexer::findEndOfIdentifierContinuation(
+    SourceLocation Loc, const SourceManager &SM, const LangOptions &LangOpts) {
+  Loc = SM.getExpansionLoc(Loc);
+  const FileIDAndOffset LocInfo = SM.getDecomposedLoc(Loc);
+  bool Invalid = false;
+  const StringRef Buffer = SM.getBufferData(LocInfo.first, &Invalid);
+  if (Invalid)
+    return Loc;
+
+  const char *StrData = Buffer.data() + LocInfo.second;
+  if (StrData >= Buffer.end())
+    return Loc;
+
+  // Use the lexer continuation rules directly, without requiring identifier
+  // start at Loc.
+  Lexer TheLexer(SM.getLocForStartOfFile(LocInfo.first), LangOpts,
+                 Buffer.begin(), StrData, Buffer.end());
+  Token Tok;
+  TheLexer.LexIdentifierContinue(Tok, StrData);
+  return Loc.getLocWithOffset(Tok.getLength());
+}
+
 /// Relex the token at the specified location.
 /// \returns true if there was a failure, false on success.
 bool Lexer::getRawToken(SourceLocation Loc, Token &Result,
diff --git a/clang/unittests/Lex/LexerTest.cpp 
b/clang/unittests/Lex/LexerTest.cpp
index da335d6e81820..5dbb861b59804 100644
--- a/clang/unittests/Lex/LexerTest.cpp
+++ b/clang/unittests/Lex/LexerTest.cpp
@@ -850,4 +850,38 @@ TEST_F(LexerTest, CheckFirstPPToken) {
     EXPECT_TRUE(Tok.getRawIdentifier() == "FOO");
   }
 }
+
+TEST_F(LexerTest, FindEndOfIdentifierContinuation) {
+  const auto Measure = [&](const StringRef Code,
+                           const unsigned Offset) -> unsigned {
+    auto Buf = llvm::MemoryBuffer::getMemBuffer(Code);
+    SourceMgr.setMainFileID(SourceMgr.createFileID(std::move(Buf)));
+    const auto Loc = SourceMgr.getLocForStartOfFile(SourceMgr.getMainFileID())
+                         .getLocWithOffset(Offset);
+    const auto End =
+        Lexer::findEndOfIdentifierContinuation(Loc, SourceMgr, LangOpts);
+    const unsigned Length = SourceMgr.getFileOffset(End) - Offset;
+    SourceMgr.clearIDTables();
+    return Length;
+  };
+
+  // ASCII identifiers.
+  EXPECT_EQ(Measure("abcd", 0), 4u);  // Full identifier.
+  EXPECT_EQ(Measure("abcd", 2), 2u);  // Mid-identifier.
+  EXPECT_EQ(Measure("ab12", 2), 2u);  // At digit.
+  EXPECT_EQ(Measure("ab cd", 2), 0u); // At space.
+  EXPECT_EQ(Measure("ab+cd", 2), 0u); // At non-identifier.
+
+  // UTF-8 identifier characters.
+  LangOpts.CPlusPlus = true;
+  EXPECT_EQ(Measure("ab🙂cd", 2), 6u); // '🙂' (4 bytes) + "cd".
+  EXPECT_EQ(Measure("🙂cd", 0), 6u);   // Starts with '🙂'.
+
+  // Dollar sign (requires DollarIdents).
+  LangOpts.DollarIdents = true;
+  EXPECT_EQ(Measure("ab$cd", 2), 3u); // '$' is identifier continue.
+  LangOpts.DollarIdents = false;
+  EXPECT_EQ(Measure("ab$cd", 2), 0u); // '$' is not identifier continue.
+}
+
 } // anonymous namespace

>From 7598ee577e64ae14924e320e765dd1728f7c855a Mon Sep 17 00:00:00 2001
From: argothiel <[email protected]>
Date: Fri, 20 Mar 2026 23:49:29 +0100
Subject: [PATCH 3/3] fixup! [clangd] Add InsertReplaceEdit for code completion

code formatting fix
---
 clang-tools-extra/clangd/CodeComplete.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/clang-tools-extra/clangd/CodeComplete.cpp 
b/clang-tools-extra/clangd/CodeComplete.cpp
index c58ef139b7437..f5882abb94394 100644
--- a/clang-tools-extra/clangd/CodeComplete.cpp
+++ b/clang-tools-extra/clangd/CodeComplete.cpp
@@ -2275,7 +2275,8 @@ CompletionPrefix guessCompletionPrefix(llvm::StringRef 
Content,
 }
 
 // If Offset is inside what looks like argument comment (e.g.
-// "/*^*/" or "/* foo = ^*/"), returns the offset pointing past the closing 
"*/".
+// "/*^*/" or "/* foo = ^*/"), returns the offset pointing past the closing
+// "*/".
 static std::optional<unsigned>
 maybeFunctionArgumentCommentEnd(const PathRef FileName, const unsigned Offset,
                                 const llvm::StringRef Content,

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to