Skip to content

Commit 75af8e8

Browse files
authored
[clangd] Find better insertion locations in DefineOutline tweak (#128164)
If possible, put the definition next to the definition of an adjacent declaration. For example: struct S { void f^oo1() {} void foo2(); void f^oo3() {} }; // S::foo1() goes here void S::foo2() {} // S::foo3() goes here
1 parent 9ef2103 commit 75af8e8

File tree

4 files changed

+455
-58
lines changed

4 files changed

+455
-58
lines changed

clang-tools-extra/clangd/SourceCode.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,26 @@ EligibleRegion getEligiblePoints(llvm::StringRef Code,
12171217
return ER;
12181218
}
12191219

1220+
std::string getNamespaceAtPosition(StringRef Code, const Position &Pos,
1221+
const LangOptions &LangOpts) {
1222+
std::vector<std::string> Enclosing = {""};
1223+
parseNamespaceEvents(Code, LangOpts, [&](NamespaceEvent Event) {
1224+
if (Pos < Event.Pos)
1225+
return;
1226+
if (Event.Trigger == NamespaceEvent::UsingDirective)
1227+
return;
1228+
if (!Event.Payload.empty())
1229+
Event.Payload.append("::");
1230+
if (Event.Trigger == NamespaceEvent::BeginNamespace) {
1231+
Enclosing.emplace_back(std::move(Event.Payload));
1232+
} else {
1233+
Enclosing.pop_back();
1234+
assert(Enclosing.back() == Event.Payload);
1235+
}
1236+
});
1237+
return Enclosing.back();
1238+
}
1239+
12201240
bool isHeaderFile(llvm::StringRef FileName,
12211241
std::optional<LangOptions> LangOpts) {
12221242
// Respect the langOpts, for non-file-extension cases, e.g. standard library

clang-tools-extra/clangd/SourceCode.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,11 @@ EligibleRegion getEligiblePoints(llvm::StringRef Code,
309309
llvm::StringRef FullyQualifiedName,
310310
const LangOptions &LangOpts);
311311

312+
/// Returns the fully qualified name of the namespace at \p Pos in the \p Code.
313+
/// Employs pseudo-parsing to determine the start and end of namespaces.
314+
std::string getNamespaceAtPosition(llvm::StringRef Code, const Position &Pos,
315+
const LangOptions &LangOpts);
316+
312317
struct DefinedMacro {
313318
llvm::StringRef Name;
314319
const MacroInfo *Info;

clang-tools-extra/clangd/refactor/tweaks/DefineOutline.cpp

Lines changed: 239 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//===----------------------------------------------------------------------===//
88

99
#include "AST.h"
10+
#include "FindSymbols.h"
1011
#include "FindTarget.h"
1112
#include "HeaderSourceSwitch.h"
1213
#include "ParsedAST.h"
@@ -34,6 +35,7 @@
3435
#include <cstddef>
3536
#include <optional>
3637
#include <string>
38+
#include <tuple>
3739

3840
namespace clang {
3941
namespace clangd {
@@ -362,6 +364,12 @@ struct InsertionPoint {
362364
size_t Offset;
363365
};
364366

367+
enum class RelativeInsertPos { Before, After };
368+
struct InsertionAnchor {
369+
Location Loc;
370+
RelativeInsertPos RelInsertPos = RelativeInsertPos::Before;
371+
};
372+
365373
// Returns the range that should be deleted from declaration, which always
366374
// contains function body. In addition to that it might contain constructor
367375
// initializers.
@@ -489,8 +497,14 @@ class DefineOutline : public Tweak {
489497

490498
Expected<Effect> apply(const Selection &Sel) override {
491499
const SourceManager &SM = Sel.AST->getSourceManager();
492-
auto CCFile = SameFile ? Sel.AST->tuPath().str()
493-
: getSourceFile(Sel.AST->tuPath(), Sel);
500+
std::optional<Path> CCFile;
501+
auto Anchor = getDefinitionOfAdjacentDecl(Sel);
502+
if (Anchor) {
503+
CCFile = Anchor->Loc.uri.file();
504+
} else {
505+
CCFile = SameFile ? Sel.AST->tuPath().str()
506+
: getSourceFile(Sel.AST->tuPath(), Sel);
507+
}
494508
if (!CCFile)
495509
return error("Couldn't find a suitable implementation file.");
496510
assert(Sel.FS && "FS Must be set in apply");
@@ -499,21 +513,62 @@ class DefineOutline : public Tweak {
499513
// doesn't exist?
500514
if (!Buffer)
501515
return llvm::errorCodeToError(Buffer.getError());
516+
502517
auto Contents = Buffer->get()->getBuffer();
503-
auto InsertionPoint = getInsertionPoint(Contents, Sel);
504-
if (!InsertionPoint)
505-
return InsertionPoint.takeError();
518+
SourceManagerForFile SMFF(*CCFile, Contents);
519+
520+
std::optional<Position> InsertionPos;
521+
if (Anchor) {
522+
if (auto P = getInsertionPointFromExistingDefinition(
523+
SMFF, **Buffer, Anchor->Loc, Anchor->RelInsertPos, Sel.AST)) {
524+
InsertionPos = *P;
525+
}
526+
}
527+
528+
std::optional<std::size_t> Offset;
529+
const DeclContext *EnclosingNamespace = nullptr;
530+
std::string EnclosingNamespaceName;
531+
532+
if (InsertionPos) {
533+
EnclosingNamespaceName = getNamespaceAtPosition(Contents, *InsertionPos,
534+
Sel.AST->getLangOpts());
535+
} else if (SameFile) {
536+
auto P = getInsertionPointInMainFile(Sel.AST);
537+
if (!P)
538+
return P.takeError();
539+
Offset = P->Offset;
540+
EnclosingNamespace = P->EnclosingNamespace;
541+
} else {
542+
auto Region = getEligiblePoints(
543+
Contents, Source->getQualifiedNameAsString(), Sel.AST->getLangOpts());
544+
assert(!Region.EligiblePoints.empty());
545+
EnclosingNamespaceName = Region.EnclosingNamespace;
546+
InsertionPos = Region.EligiblePoints.back();
547+
}
548+
549+
if (InsertionPos) {
550+
auto O = positionToOffset(Contents, *InsertionPos);
551+
if (!O)
552+
return O.takeError();
553+
Offset = *O;
554+
auto TargetContext =
555+
findContextForNS(EnclosingNamespaceName, Source->getDeclContext());
556+
if (!TargetContext)
557+
return error("define outline: couldn't find a context for target");
558+
EnclosingNamespace = *TargetContext;
559+
}
560+
561+
assert(Offset);
562+
assert(EnclosingNamespace);
506563

507564
auto FuncDef = getFunctionSourceCode(
508-
Source, InsertionPoint->EnclosingNamespace, Sel.AST->getTokens(),
565+
Source, EnclosingNamespace, Sel.AST->getTokens(),
509566
Sel.AST->getHeuristicResolver(),
510567
SameFile && isHeaderFile(Sel.AST->tuPath(), Sel.AST->getLangOpts()));
511568
if (!FuncDef)
512569
return FuncDef.takeError();
513570

514-
SourceManagerForFile SMFF(*CCFile, Contents);
515-
const tooling::Replacement InsertFunctionDef(
516-
*CCFile, InsertionPoint->Offset, 0, *FuncDef);
571+
const tooling::Replacement InsertFunctionDef(*CCFile, *Offset, 0, *FuncDef);
517572
auto Effect = Effect::mainFileEdit(
518573
SMFF.get(), tooling::Replacements(InsertFunctionDef));
519574
if (!Effect)
@@ -548,59 +603,188 @@ class DefineOutline : public Tweak {
548603
return std::move(*Effect);
549604
}
550605

551-
// Returns the most natural insertion point for \p QualifiedName in \p
552-
// Contents. This currently cares about only the namespace proximity, but in
553-
// feature it should also try to follow ordering of declarations. For example,
554-
// if decls come in order `foo, bar, baz` then this function should return
555-
// some point between foo and baz for inserting bar.
556-
// FIXME: The selection can be made smarter by looking at the definition
557-
// locations for adjacent decls to Source. Unfortunately pseudo parsing in
558-
// getEligibleRegions only knows about namespace begin/end events so we
559-
// can't match function start/end positions yet.
560-
llvm::Expected<InsertionPoint> getInsertionPoint(llvm::StringRef Contents,
561-
const Selection &Sel) {
562-
// If the definition goes to the same file and there is a namespace,
563-
// we should (and, in the case of anonymous namespaces, need to)
564-
// put the definition into the original namespace block.
565-
if (SameFile) {
566-
auto *Klass = Source->getDeclContext()->getOuterLexicalRecordContext();
567-
if (!Klass)
568-
return error("moving to same file not supported for free functions");
569-
const SourceLocation EndLoc = Klass->getBraceRange().getEnd();
570-
const auto &TokBuf = Sel.AST->getTokens();
571-
auto Tokens = TokBuf.expandedTokens();
572-
auto It = llvm::lower_bound(
573-
Tokens, EndLoc, [](const syntax::Token &Tok, SourceLocation EndLoc) {
574-
return Tok.location() < EndLoc;
575-
});
576-
while (It != Tokens.end()) {
577-
if (It->kind() != tok::semi) {
578-
++It;
579-
continue;
606+
std::optional<InsertionAnchor>
607+
getDefinitionOfAdjacentDecl(const Selection &Sel) {
608+
if (!Sel.Index)
609+
return {};
610+
std::optional<Location> Anchor;
611+
std::string TuURI = URI::createFile(Sel.AST->tuPath()).toString();
612+
auto CheckCandidate = [&](Decl *Candidate) {
613+
assert(Candidate != Source);
614+
if (auto Func = llvm::dyn_cast_or_null<FunctionDecl>(Candidate);
615+
!Func || Func->isThisDeclarationADefinition()) {
616+
return;
617+
}
618+
std::optional<Location> CandidateLoc;
619+
Sel.Index->lookup({{getSymbolID(Candidate)}}, [&](const Symbol &S) {
620+
if (S.Definition) {
621+
if (auto Loc = indexToLSPLocation(S.Definition, Sel.AST->tuPath()))
622+
CandidateLoc = *Loc;
623+
else
624+
log("getDefinitionOfAdjacentDecl: {0}", Loc.takeError());
580625
}
581-
unsigned Offset = Sel.AST->getSourceManager()
582-
.getDecomposedLoc(It->endLocation())
583-
.second;
584-
return InsertionPoint{Klass->getEnclosingNamespaceContext(), Offset};
626+
});
627+
if (!CandidateLoc)
628+
return;
629+
630+
// If our definition is constrained to the same file, ignore
631+
// definitions that are not located there.
632+
// If our definition is not constrained to the same file, but
633+
// our anchor definition is in the same file, then we also put our
634+
// definition there, because that appears to be the user preference.
635+
// Exception: If the existing definition is a template, then the
636+
// location is likely due to technical necessity rather than preference,
637+
// so ignore that definition.
638+
bool CandidateSameFile = TuURI == CandidateLoc->uri.uri();
639+
if (SameFile && !CandidateSameFile)
640+
return;
641+
if (!SameFile && CandidateSameFile) {
642+
if (Candidate->isTemplateDecl())
643+
return;
644+
SameFile = true;
585645
}
586-
return error(
587-
"failed to determine insertion location: no end of class found");
646+
Anchor = *CandidateLoc;
647+
};
648+
649+
// Try to find adjacent function declarations.
650+
// Determine the closest one by alternatingly going "up" and "down"
651+
// from our function in increasing steps.
652+
const DeclContext *ParentContext = Source->getParent();
653+
const auto SourceIt = llvm::find_if(
654+
ParentContext->decls(), [this](const Decl *D) { return D == Source; });
655+
if (SourceIt == ParentContext->decls_end())
656+
return {};
657+
const int Preceding = std::distance(ParentContext->decls_begin(), SourceIt);
658+
const int Following =
659+
std::distance(SourceIt, ParentContext->decls_end()) - 1;
660+
for (int Offset = 1; Offset <= Preceding || Offset <= Following; ++Offset) {
661+
if (Offset <= Preceding)
662+
CheckCandidate(
663+
*std::next(ParentContext->decls_begin(), Preceding - Offset));
664+
if (Anchor)
665+
return InsertionAnchor{*Anchor, RelativeInsertPos::After};
666+
if (Offset <= Following)
667+
CheckCandidate(*std::next(SourceIt, Offset));
668+
if (Anchor)
669+
return InsertionAnchor{*Anchor, RelativeInsertPos::Before};
588670
}
671+
return {};
672+
}
589673

590-
auto Region = getEligiblePoints(
591-
Contents, Source->getQualifiedNameAsString(), Sel.AST->getLangOpts());
674+
// We don't know the actual start or end of the definition, only the position
675+
// of the name. Therefore, we heuristically try to locate the last token
676+
// before or in this function, respectively. Adapt as required by user code.
677+
std::optional<Position> getInsertionPointFromExistingDefinition(
678+
SourceManagerForFile &SMFF, const llvm::MemoryBuffer &Buffer,
679+
const Location &Loc, RelativeInsertPos RelInsertPos, ParsedAST *AST) {
680+
auto StartOffset = positionToOffset(Buffer.getBuffer(), Loc.range.start);
681+
if (!StartOffset)
682+
return {};
683+
SourceLocation InsertionLoc;
684+
SourceManager &SM = SMFF.get();
685+
686+
auto InsertBefore = [&] {
687+
// Go backwards until we encounter one of the following:
688+
// - An opening brace (of a namespace).
689+
// - A closing brace (of a function definition).
690+
// - A semicolon (of a declaration).
691+
// If no such token was found, then the first token in the file starts the
692+
// definition.
693+
auto Tokens = syntax::tokenize(
694+
syntax::FileRange(SM.getMainFileID(), 0, *StartOffset), SM,
695+
AST->getLangOpts());
696+
if (Tokens.empty())
697+
return;
698+
for (auto I = std::rbegin(Tokens);
699+
InsertionLoc.isInvalid() && I != std::rend(Tokens); ++I) {
700+
switch (I->kind()) {
701+
case tok::l_brace:
702+
case tok::r_brace:
703+
case tok::semi:
704+
if (I != std::rbegin(Tokens))
705+
InsertionLoc = std::prev(I)->location();
706+
else
707+
InsertionLoc = I->endLocation();
708+
break;
709+
default:
710+
break;
711+
}
712+
}
713+
if (InsertionLoc.isInvalid())
714+
InsertionLoc = Tokens.front().location();
715+
};
592716

593-
assert(!Region.EligiblePoints.empty());
594-
auto Offset = positionToOffset(Contents, Region.EligiblePoints.back());
595-
if (!Offset)
596-
return Offset.takeError();
717+
if (RelInsertPos == RelativeInsertPos::Before) {
718+
InsertBefore();
719+
} else {
720+
// Skip over one top-level pair of parentheses (for the parameter list)
721+
// and one pair of curly braces (for the code block).
722+
// If that fails, insert before the function instead.
723+
auto Tokens =
724+
syntax::tokenize(syntax::FileRange(SM.getMainFileID(), *StartOffset,
725+
Buffer.getBuffer().size()),
726+
SM, AST->getLangOpts());
727+
bool SkippedParams = false;
728+
int OpenParens = 0;
729+
int OpenBraces = 0;
730+
std::optional<syntax::Token> Tok;
731+
for (const auto &T : Tokens) {
732+
tok::TokenKind StartKind = SkippedParams ? tok::l_brace : tok::l_paren;
733+
tok::TokenKind EndKind = SkippedParams ? tok::r_brace : tok::r_paren;
734+
int &Count = SkippedParams ? OpenBraces : OpenParens;
735+
if (T.kind() == StartKind) {
736+
++Count;
737+
} else if (T.kind() == EndKind) {
738+
if (--Count == 0) {
739+
if (SkippedParams) {
740+
Tok = T;
741+
break;
742+
}
743+
SkippedParams = true;
744+
} else if (Count < 0) {
745+
break;
746+
}
747+
}
748+
}
749+
if (Tok)
750+
InsertionLoc = Tok->endLocation();
751+
else
752+
InsertBefore();
753+
}
597754

598-
auto TargetContext =
599-
findContextForNS(Region.EnclosingNamespace, Source->getDeclContext());
600-
if (!TargetContext)
601-
return error("define outline: couldn't find a context for target");
755+
if (!InsertionLoc.isValid())
756+
return {};
757+
return sourceLocToPosition(SM, InsertionLoc);
758+
}
602759

603-
return InsertionPoint{*TargetContext, *Offset};
760+
// Returns the most natural insertion point in this file.
761+
// This is a fallback for when we failed to find an existing definition to
762+
// place the new one next to. It only considers namespace proximity.
763+
llvm::Expected<InsertionPoint> getInsertionPointInMainFile(ParsedAST *AST) {
764+
// If the definition goes to the same file and there is a namespace,
765+
// we should (and, in the case of anonymous namespaces, need to)
766+
// put the definition into the original namespace block.
767+
auto *Klass = Source->getDeclContext()->getOuterLexicalRecordContext();
768+
if (!Klass)
769+
return error("moving to same file not supported for free functions");
770+
const SourceLocation EndLoc = Klass->getBraceRange().getEnd();
771+
const auto &TokBuf = AST->getTokens();
772+
auto Tokens = TokBuf.expandedTokens();
773+
auto It = llvm::lower_bound(
774+
Tokens, EndLoc, [](const syntax::Token &Tok, SourceLocation EndLoc) {
775+
return Tok.location() < EndLoc;
776+
});
777+
while (It != Tokens.end()) {
778+
if (It->kind() != tok::semi) {
779+
++It;
780+
continue;
781+
}
782+
unsigned Offset =
783+
AST->getSourceManager().getDecomposedLoc(It->endLocation()).second;
784+
return InsertionPoint{Klass->getEnclosingNamespaceContext(), Offset};
785+
}
786+
return error(
787+
"failed to determine insertion location: no end of class found");
604788
}
605789

606790
private:

0 commit comments

Comments
 (0)