Skip to content

Commit 08f3c3e

Browse files
committed
[clangd] Find better insertion locations in DefineOutline tweak
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 9f5811e commit 08f3c3e

File tree

4 files changed

+450
-58
lines changed

4 files changed

+450
-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: 237 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,186 @@ 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;
580623
}
581-
unsigned Offset = Sel.AST->getSourceManager()
582-
.getDecomposedLoc(It->endLocation())
583-
.second;
584-
return InsertionPoint{Klass->getEnclosingNamespaceContext(), Offset};
624+
});
625+
if (!CandidateLoc)
626+
return;
627+
628+
// If our definition is constrained to the same file, ignore
629+
// definitions that are not located there.
630+
// If our definition is not constrained to the same file, but
631+
// our anchor definition is in the same file, then we also put our
632+
// definition there, because that appears to be the user preference.
633+
// Exception: If the existing definition is a template, then the
634+
// location is likely due to technical necessity rather than preference,
635+
// so ignore that definition.
636+
bool CandidateSameFile = TuURI == CandidateLoc->uri.uri();
637+
if (SameFile && !CandidateSameFile)
638+
return;
639+
if (!SameFile && CandidateSameFile) {
640+
if (Candidate->isTemplateDecl())
641+
return;
642+
SameFile = true;
585643
}
586-
return error(
587-
"failed to determine insertion location: no end of class found");
644+
Anchor = *CandidateLoc;
645+
};
646+
647+
// Try to find adjacent function declarations.
648+
// Determine the closest one by alternatingly going "up" and "down"
649+
// from our function in increasing steps.
650+
const DeclContext *ParentContext = Source->getParent();
651+
const auto SourceIt = llvm::find_if(
652+
ParentContext->decls(), [this](const Decl *D) { return D == Source; });
653+
if (SourceIt == ParentContext->decls_end())
654+
return {};
655+
const int Preceding = std::distance(ParentContext->decls_begin(), SourceIt);
656+
const int Following =
657+
std::distance(SourceIt, ParentContext->decls_end()) - 1;
658+
for (int Offset = 1; Offset <= Preceding || Offset <= Following; ++Offset) {
659+
if (Offset <= Preceding)
660+
CheckCandidate(
661+
*std::next(ParentContext->decls_begin(), Preceding - Offset));
662+
if (Anchor)
663+
return InsertionAnchor{*Anchor, RelativeInsertPos::After};
664+
if (Offset <= Following)
665+
CheckCandidate(*std::next(SourceIt, Offset));
666+
if (Anchor)
667+
return InsertionAnchor{*Anchor, RelativeInsertPos::Before};
588668
}
669+
return {};
670+
}
589671

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

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

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

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

606788
private:

0 commit comments

Comments
 (0)