Skip to content

Commit da6a6e8

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 c5f40bf commit da6a6e8

File tree

4 files changed

+434
-54
lines changed

4 files changed

+434
-54
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ EligibleRegion getEligiblePoints(llvm::StringRef Code,
309309
llvm::StringRef FullyQualifiedName,
310310
const LangOptions &LangOpts);
311311

312+
std::string getNamespaceAtPosition(llvm::StringRef Code, const Position &Pos,
313+
const LangOptions &LangOpts);
314+
312315
struct DefinedMacro {
313316
llvm::StringRef Name;
314317
const MacroInfo *Info;

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

Lines changed: 237 additions & 54 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"
@@ -489,8 +490,18 @@ class DefineOutline : public Tweak {
489490

490491
Expected<Effect> apply(const Selection &Sel) override {
491492
const SourceManager &SM = Sel.AST->getSourceManager();
492-
auto CCFile = SameFile ? Sel.AST->tuPath().str()
493-
: getSourceFile(Sel.AST->tuPath(), Sel);
493+
std::optional<Path> CCFile;
494+
std::optional<std::pair<Symbol, RelativeInsertPos>> Anchor =
495+
getDefinitionOfAdjacentDecl(Sel);
496+
if (Anchor) {
497+
auto URI = URI::resolve(Anchor->first.Definition.FileURI);
498+
if (!URI)
499+
return URI.takeError();
500+
CCFile = *URI;
501+
} else {
502+
CCFile = SameFile ? Sel.AST->tuPath().str()
503+
: getSourceFile(Sel.AST->tuPath(), Sel);
504+
}
494505
if (!CCFile)
495506
return error("Couldn't find a suitable implementation file.");
496507
assert(Sel.FS && "FS Must be set in apply");
@@ -499,21 +510,62 @@ class DefineOutline : public Tweak {
499510
// doesn't exist?
500511
if (!Buffer)
501512
return llvm::errorCodeToError(Buffer.getError());
513+
514+
std::optional<Position> InsertionPos;
515+
if (Anchor) {
516+
if (auto P = getInsertionPointFromExistingDefinition(
517+
**Buffer, Anchor->first, Anchor->second, Sel.AST)) {
518+
InsertionPos = *P;
519+
}
520+
}
521+
502522
auto Contents = Buffer->get()->getBuffer();
503-
auto InsertionPoint = getInsertionPoint(Contents, Sel);
504-
if (!InsertionPoint)
505-
return InsertionPoint.takeError();
523+
524+
std::size_t Offset = -1;
525+
const DeclContext *EnclosingNamespace = nullptr;
526+
std::string EnclosingNamespaceName;
527+
528+
if (InsertionPos) {
529+
EnclosingNamespaceName = getNamespaceAtPosition(Contents, *InsertionPos,
530+
Sel.AST->getLangOpts());
531+
} else if (SameFile) {
532+
auto P = getInsertionPointInMainFile(Sel.AST);
533+
if (!P)
534+
return P.takeError();
535+
Offset = P->Offset;
536+
EnclosingNamespace = P->EnclosingNamespace;
537+
} else {
538+
auto Region = getEligiblePoints(
539+
Contents, Source->getQualifiedNameAsString(), Sel.AST->getLangOpts());
540+
assert(!Region.EligiblePoints.empty());
541+
EnclosingNamespaceName = Region.EnclosingNamespace;
542+
InsertionPos = Region.EligiblePoints.back();
543+
}
544+
545+
if (InsertionPos) {
546+
auto O = positionToOffset(Contents, *InsertionPos);
547+
if (!O)
548+
return O.takeError();
549+
Offset = *O;
550+
auto TargetContext =
551+
findContextForNS(EnclosingNamespaceName, Source->getDeclContext());
552+
if (!TargetContext)
553+
return error("define outline: couldn't find a context for target");
554+
EnclosingNamespace = *TargetContext;
555+
}
556+
557+
assert(Offset >= 0);
558+
assert(EnclosingNamespace);
506559

507560
auto FuncDef = getFunctionSourceCode(
508-
Source, InsertionPoint->EnclosingNamespace, Sel.AST->getTokens(),
561+
Source, EnclosingNamespace, Sel.AST->getTokens(),
509562
Sel.AST->getHeuristicResolver(),
510563
SameFile && isHeaderFile(Sel.AST->tuPath(), Sel.AST->getLangOpts()));
511564
if (!FuncDef)
512565
return FuncDef.takeError();
513566

514567
SourceManagerForFile SMFF(*CCFile, Contents);
515-
const tooling::Replacement InsertFunctionDef(
516-
*CCFile, InsertionPoint->Offset, 0, *FuncDef);
568+
const tooling::Replacement InsertFunctionDef(*CCFile, Offset, 0, *FuncDef);
517569
auto Effect = Effect::mainFileEdit(
518570
SMFF.get(), tooling::Replacements(InsertFunctionDef));
519571
if (!Effect)
@@ -548,59 +600,190 @@ class DefineOutline : public Tweak {
548600
return std::move(*Effect);
549601
}
550602

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;
603+
enum class RelativeInsertPos { Before, After };
604+
std::optional<std::pair<Symbol, RelativeInsertPos>>
605+
getDefinitionOfAdjacentDecl(const Selection &Sel) {
606+
if (!Sel.Index)
607+
return {};
608+
std::optional<Symbol> Anchor;
609+
std::string TuURI = URI::createFile(Sel.AST->tuPath()).toString();
610+
auto CheckCandidate = [&](Decl *Candidate) {
611+
assert(Candidate != Source);
612+
if (auto Func = llvm::dyn_cast_or_null<FunctionDecl>(Candidate);
613+
!Func || Func->isThisDeclarationADefinition()) {
614+
return;
615+
}
616+
std::optional<Symbol> CandidateSymbol;
617+
Sel.Index->lookup({{getSymbolID(Candidate)}}, [&](const Symbol &S) {
618+
if (S.Definition) {
619+
CandidateSymbol = S;
580620
}
581-
unsigned Offset = Sel.AST->getSourceManager()
582-
.getDecomposedLoc(It->endLocation())
583-
.second;
584-
return InsertionPoint{Klass->getEnclosingNamespaceContext(), Offset};
621+
});
622+
if (!CandidateSymbol)
623+
return;
624+
625+
// If our definition is constrained to the same file, ignore
626+
// definitions that are not located there.
627+
// If our definition is not constrained to the same file, but
628+
// our anchor definition is in the same file, then we also put our
629+
// definition there, because that appears to be the user preference.
630+
// Exception: If the existing definition is a template, then the
631+
// location is likely due to technical necessity rather than preference,
632+
// so ignore that definition.
633+
bool CandidateSameFile = TuURI == CandidateSymbol->Definition.FileURI;
634+
if (SameFile && !CandidateSameFile)
635+
return;
636+
if (!SameFile && CandidateSameFile) {
637+
if (Candidate->isTemplateDecl())
638+
return;
639+
SameFile = true;
585640
}
586-
return error(
587-
"failed to determine insertion location: no end of class found");
641+
Anchor = *CandidateSymbol;
642+
};
643+
644+
// Try to find adjacent function declarations.
645+
// Determine the closest one by alternatingly going "up" and "down"
646+
// from our function in increasing steps.
647+
const DeclContext *ParentContext = Source->getParent();
648+
const auto SourceIt = llvm::find_if(
649+
ParentContext->decls(), [this](const Decl *D) { return D == Source; });
650+
if (SourceIt == ParentContext->decls_end())
651+
return {};
652+
const int Preceding = std::distance(ParentContext->decls_begin(), SourceIt);
653+
const int Following =
654+
std::distance(SourceIt, ParentContext->decls_end()) - 1;
655+
for (int Offset = 1; Offset <= Preceding || Offset <= Following; ++Offset) {
656+
if (Offset <= Preceding)
657+
CheckCandidate(
658+
*std::next(ParentContext->decls_begin(), Preceding - Offset));
659+
if (Anchor)
660+
return std::make_pair(*Anchor, RelativeInsertPos::After);
661+
if (Offset <= Following)
662+
CheckCandidate(*std::next(SourceIt, Offset));
663+
if (Anchor)
664+
return std::make_pair(*Anchor, RelativeInsertPos::Before);
588665
}
666+
return {};
667+
}
589668

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

593-
assert(!Region.EligiblePoints.empty());
594-
auto Offset = positionToOffset(Contents, Region.EligiblePoints.back());
595-
if (!Offset)
596-
return Offset.takeError();
718+
if (RelInsertPos == RelativeInsertPos::Before) {
719+
InsertBefore();
720+
} else {
721+
// Skip over one top-level pair of parentheses (for the parameter list)
722+
// and one pair of curly braces (for the code block).
723+
// If that fails, insert before the function instead.
724+
auto Tokens = syntax::tokenize(
725+
syntax::FileRange(F, *StartOffset, Buffer.getBuffer().size()), SM,
726+
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+
return InsertionLoc.isValid() ? sourceLocToPosition(SM, InsertionLoc)
756+
: Position();
757+
}
602758

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

606789
private:

0 commit comments

Comments
 (0)