Skip to content

Commit

Permalink
feat: Select Smaller/Larger/Next/Previous Syntax Nodes (#105)
Browse files Browse the repository at this point in the history
* refactor: Remove duplication in tst_codedocument

By moving the project setup into a macro we can save quite a few lines
of code.

* feat: Add TreeCursor to the treesitter wrapper

* feat: Add "Select Smaller/Larger Syntax Node"

This allows to navigate the code semantically.

* refactor: Reduce code duplication in MainWindow

Most functions were the same with only different function name and document type.
We can replace those implementations with a macro.

* feat: Allow navigating to sibling syntax nodes

This patch adds functions to select the next/previous syntax node.
Combined with the previously added functions to select the
larger/smaller syntax node, this allows for very useful semantic code
navigation.

* chore(docs): Update CodeDocument docs

* fix: Use LOG_AND_MERGE for syntax node movement

This aggregates the counts, which improves the history a lot.

* chore: Incorporate requested changes

Use LOG_RETURN in the select*SyntaxNode methods.

Add a warning if selectSmallerSyntaxNode doesn't find anything to
select.
  • Loading branch information
LeonMatthesKDAB authored Jul 15, 2024
1 parent 5746b64 commit cd81b3b
Show file tree
Hide file tree
Showing 14 changed files with 580 additions and 139 deletions.
40 changes: 40 additions & 0 deletions docs/API/knut/codedocument.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Inherited properties: [TextDocument properties](../knut/textdocument.md#properti
|array<[QueryMatch](../knut/querymatch.md)> |**[query](#query)**(string query)|
|[QueryMatch](../knut/querymatch.md) |**[queryFirst](#queryFirst)**(string query)|
|array<[QueryMatch](../knut/querymatch.md)> |**[queryInRange](#queryInRange)**([RangeMark](../knut/rangemark.md) range, string query)|
|int |**[selectLargerSyntaxNode](#selectLargerSyntaxNode)**(int count = 1)|
|int |**[selectNextSyntaxNode](#selectNextSyntaxNode)**(int count = 1)|
|int |**[selectPreviousSyntaxNode](#selectPreviousSyntaxNode)**(int count = 1)|
|int |**[selectSmallerSyntaxNode](#selectSmallerSyntaxNode)**(int count = 1)|
||**[selectSymbol](#selectSymbol)**(string name, int options = TextDocument.NoFindFlags)|
|[Symbol](../knut/symbol.md) |**[symbolUnderCursor](#symbolUnderCursor)**()|
|array<[Symbol](../knut/symbol.md)> |**[symbols](#symbols)**()|
Expand Down Expand Up @@ -80,6 +84,42 @@ Also see: [Tree-sitter in Knut](../../getting-started/treesitter.md)
Searches for the given `query`, but only in the provided `range`.


#### <a name="selectLargerSyntaxNode"></a>int **selectLargerSyntaxNode**(int count = 1)

Selects the text of the next larger syntax node that the selection is in.

It does so `count` times and returns the resulting cursor position.

#### <a name="selectNextSyntaxNode"></a>int **selectNextSyntaxNode**(int count = 1)

Selects the next syntax node following the current selection.

If there is no next syntax node in the current level, it increases the selection to the next larger syntax node and
searches from there. See also: `CodeDocument::selectLargerSyntaxNode`

It does so `count` times and returns the resulting cursor position.

Note that this only selects "named" Tree-sitter nodes, so punctuation and other unnamed nodes are skipped.

#### <a name="selectPreviousSyntaxNode"></a>int **selectPreviousSyntaxNode**(int count = 1)

Selects the previous syntax node before the current selection.

If there is no previous syntax node in the current level, it increases the selection to the next larger syntax node
and searches from there. See also: `CodeDocument::selectLargerSyntaxNode`

It does so `count` times and returns the resulting cursor position.

Note that this only selects "named" Tree-sitter nodes, so punctuation and other unnamed nodes are skipped.

#### <a name="selectSmallerSyntaxNode"></a>int **selectSmallerSyntaxNode**(int count = 1)

Selects the left-most next smaller syntax node within the current selection.

It does so `count` times and returns the resulting cursor position.

Note that this only selects "named" Tree-sitter nodes, so punctuation and other unnamed nodes are skipped.

#### <a name="selectSymbol"></a>**selectSymbol**(string name, int options = TextDocument.NoFindFlags)

Selects a symbol based on its `name`, using different find `options`.
Expand Down
157 changes: 157 additions & 0 deletions src/core/codedocument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,163 @@ void CodeDocument::selectSymbol(const QString &name, int options)
selectRange(symbol->selectionRange());
}

/*!
* \qmlmethod int CodeDocument::selectLargerSyntaxNode(int count = 1)
*
* Selects the text of the next larger syntax node that the selection is in.
*
* It does so `count` times and returns the resulting cursor position.
*/
int CodeDocument::selectLargerSyntaxNode(int count /* = 1*/)
{
LOG_AND_MERGE("CodeDocument::selectLargerSyntaxNode", LOG_ARG("count", count));

auto currentNode = m_treeSitterHelper->nodeCoveringRange(selectionStart(), selectionEnd());

auto matchesCurrentSelection = static_cast<int>(currentNode.startPosition()) == selectionStart()
&& static_cast<int>(currentNode.endPosition()) == selectionEnd();
if (!matchesCurrentSelection) {
// finding the node that covers the current selection already produced a larger syntax node.
// So we're already up one level.
--count;
}

for (/*count already initialized*/; count > 0 && !currentNode.parent().isNull(); --count) {
auto largerNode = currentNode.parent();
if (largerNode.startPosition() == currentNode.startPosition()
&& largerNode.endPosition() == currentNode.endPosition()) {
// If the parent node matches the current selection exactly, that's not a real change, so search one
// additional time.
++count;
}
currentNode = largerNode;
}

selectRegion(currentNode.startPosition(), currentNode.endPosition());

LOG_RETURN("pos", position());
}

/*!
* \qmlmethod int CodeDocument::selectSmallerSyntaxNode(int count = 1)
*
* Selects the left-most next smaller syntax node within the current selection.
*
* It does so `count` times and returns the resulting cursor position.
*
* Note that this only selects "named" Tree-sitter nodes, so punctuation and other unnamed nodes are skipped.
*/
int CodeDocument::selectSmallerSyntaxNode(int count /* = 1*/)
{
LOG_AND_MERGE("CodeDocument::selectSmallerSyntaxNode", LOG_ARG("count", count));

auto smallerNodes =
kdalgorithms::filtered(m_treeSitterHelper->nodesInRange(createRangeMark()), [](const auto &node) {
return node.isNamed();
});

std::optional<treesitter::Node> node;
for (/*count already initialized*/; count > 0; --count) {
if (smallerNodes.isEmpty()) {
break;
}
node = smallerNodes.first();
auto matchesCurrentSelection = static_cast<int>(node->startPosition()) == selectionStart()
&& static_cast<int>(node->endPosition()) == selectionEnd();
// If the first found node matches the current selection exactly, we need to search one additional time.
if (matchesCurrentSelection) {
++count;
}
smallerNodes = node->namedChildren();
}

if (node.has_value()) {
selectRegion(node->startPosition(), node->endPosition());
} else {
spdlog::warn(
"CodeDocument::selectSmallerSyntaxNode: No smaller node found! Do you currently not have a selection?");
}

LOG_RETURN("pos", position());
}

static std::optional<treesitter::Node>
findSibling(const treesitter::Node &start, treesitter::Node (treesitter::Node::*nextFunction)() const, int count)
{
auto node = start;

std::optional<treesitter::Node> target = node;
for (/*count already initialized*/; count > 0; --count) {
auto next = std::invoke(nextFunction, node);
while (next.isNull() && !node.parent().isNull()) {
node = node.parent();
next = std::invoke(nextFunction, node);
}
if (next.isNull()) {
// We're already at the very end/top, nowhere else to go
break;
}
node = next;
target = next;
}

return target;
}

/*!
* \qmlmethod int CodeDocument::selectNextSyntaxNode(int count = 1)
*
* Selects the next syntax node following the current selection.
*
* If there is no next syntax node in the current level, it increases the selection to the next larger syntax node and
* searches from there. See also: `CodeDocument::selectLargerSyntaxNode`
*
* It does so `count` times and returns the resulting cursor position.
*
* Note that this only selects "named" Tree-sitter nodes, so punctuation and other unnamed nodes are skipped.
*/
int CodeDocument::selectNextSyntaxNode(int count /*= 1*/)
{
LOG_AND_MERGE("CodeDocument::selectNextSyntaxNode", LOG_ARG("count", count));

auto node = m_treeSitterHelper->nodeCoveringRange(selectionStart(), selectionEnd());

auto target = findSibling(node, &treesitter::Node::nextNamedSibling, count);

if (target.has_value()) {
selectRegion(target->startPosition(), target->endPosition());
}

LOG_RETURN("pos", position());
}

/*!
* \qmlmethod int CodeDocument::selectPreviousSyntaxNode(int count = 1)
*
* Selects the previous syntax node before the current selection.
*
* If there is no previous syntax node in the current level, it increases the selection to the next larger syntax node
* and searches from there. See also: `CodeDocument::selectLargerSyntaxNode`
*
* It does so `count` times and returns the resulting cursor position.
*
* Note that this only selects "named" Tree-sitter nodes, so punctuation and other unnamed nodes are skipped.
*/
int CodeDocument::selectPreviousSyntaxNode(int count /*= 1*/)
{
LOG_AND_MERGE("CodeDocument::selectPreviousSyntaxNode", LOG_ARG("count", count));

auto node = m_treeSitterHelper->nodeCoveringRange(selectionStart(), selectionEnd());

auto target = findSibling(node, &treesitter::Node::previousNamedSibling, count);

if (target.has_value()) {
selectRegion(target->startPosition(), target->endPosition());
}

LOG_RETURN("pos", position());
}

/*!
* \qmlmethod Symbol CodeDocument::findSymbol(string name, int options = TextDocument.NoFindFlags)
* Finds a symbol based on its `name`, using different find `options`.
Expand Down
5 changes: 5 additions & 0 deletions src/core/codedocument.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ class CodeDocument : public TextDocument
public slots:
void selectSymbol(const QString &name, int options = NoFindFlags);

int selectLargerSyntaxNode(int count = 1);
int selectSmallerSyntaxNode(int count = 1);
int selectNextSyntaxNode(int count = 1);
int selectPreviousSyntaxNode(int count = 1);

protected:
explicit CodeDocument(Type type, QObject *parent = nullptr);

Expand Down
24 changes: 24 additions & 0 deletions src/core/codedocument_p.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "codedocument_p.h"
#include "codedocument.h"
#include "treesitter/languages.h"
#include "treesitter/tree_cursor.h"
#include "utils/log.h"

#include <kdalgorithms.h>
Expand Down Expand Up @@ -115,6 +116,29 @@ QList<treesitter::Node> TreeSitterHelper::nodesInRange(const RangeMark &range)
return nodesInRange;
}

treesitter::Node TreeSitterHelper::nodeCoveringRange(int start, int end)
{
auto coversRange = [start, end](const treesitter::Node &node) {
return static_cast<int>(node.startPosition()) <= start && end <= static_cast<int>(node.endPosition());
};

auto coveringNode = syntaxTree()->rootNode();

auto cursor = treesitter::TreeCursor(coveringNode);

bool node_changed = cursor.gotoFirstChild();
while (node_changed) {
if (coversRange(cursor.currentNode())) {
coveringNode = cursor.currentNode();
node_changed = cursor.gotoFirstChild();
} else {
node_changed = cursor.gotoNextSibling();
}
}

return coveringNode;
}

void TreeSitterHelper::assignSymbolContexts()
{
auto contextForSymbol = [this](Symbol *symbol) {
Expand Down
1 change: 1 addition & 0 deletions src/core/codedocument_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class TreeSitterHelper

std::shared_ptr<treesitter::Query> constructQuery(const QString &query);
QList<treesitter::Node> nodesInRange(const RangeMark &range);
treesitter::Node nodeCoveringRange(int start, int end);

const QList<Core::Symbol *> &symbols();

Expand Down
Loading

0 comments on commit cd81b3b

Please sign in to comment.