Skip to content

Commit 011457a

Browse files
committed
#3459 navigationwidget: improve link refactoring
Signed-off-by: Patrizio Bekerle <patrizio@bekerle.com>
1 parent 04abe21 commit 011457a

File tree

2 files changed

+96
-15
lines changed

2 files changed

+96
-15
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# QOwnNotes Changelog
22

3+
## 26.2.2
4+
5+
- Improved the refactoring of heading links when renaming headings in the Navigation panel
6+
(for [#3459](https://github.com/pbek/QOwnNotes/issues/3459))
7+
- Fixed an issue where heading backlink updates would incorrectly replace links to other notes with the same heading name
8+
- The backlink update now correctly matches the full link path, only updating links that point to the current note
9+
- Both URL-encoded and unencoded paths in links are now properly handled (e.g., `My%20Note.md` and `My Note.md`)
10+
- This prevents accidentally breaking links to other notes when renaming headings
11+
- Table of contents links within the same note (e.g., `[Section](#old-heading)`) are now also updated when renaming headings
12+
- This is useful for notes with TOC sections that link to headings within the note
13+
- Same-note links are properly detected and updated alongside backlinks from other notes
14+
315
## 26.2.1
416

517
- Added ability to rename headings directly in the Navigation panel

src/mainwindow.cpp

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9666,7 +9666,7 @@ void MainWindow::onNavigationWidgetHeadingRenamed(int position, const QString &o
96669666
QString newBlockText = headingPrefix + " " + newText;
96679667

96689668
// Check for backlinks that reference this heading and ask user if they want to update them
9669-
// We need to do this before we update the text in the document, otherwise the QMessageBox
9669+
// We need to do this BEFORE we update the text in the document, otherwise the QMessageBox
96709670
// dialog that confirms this change will crash the app
96719671
updateBacklinksAfterHeadingRename(oldText, newText);
96729672

@@ -9681,6 +9681,7 @@ void MainWindow::onNavigationWidgetHeadingRenamed(int position, const QString &o
96819681
/**
96829682
* Updates backlinks after a heading has been renamed
96839683
* Scans all notes that link to the current note and updates heading references
9684+
* Also checks the current note itself for TOC links (e.g., [text](#old-heading))
96849685
*/
96859686
void MainWindow::updateBacklinksAfterHeadingRename(const QString &oldHeading,
96869687
const QString &newHeading) {
@@ -9689,16 +9690,23 @@ void MainWindow::updateBacklinksAfterHeadingRename(const QString &oldHeading,
96899690
QString oldFragment = QString(QUrl::toPercentEncoding(oldHeading));
96909691
QString newFragment = QString(QUrl::toPercentEncoding(newHeading));
96919692

9692-
// Find all notes that have backlinks to the current note
9693-
QVector<int> backlinkNoteIds = this->currentNote.findBacklinkedNoteIds();
9694-
9695-
if (backlinkNoteIds.isEmpty()) {
9696-
return;
9697-
}
9693+
// Get the relative file path of the current note for precise link matching
9694+
QString currentNoteRelativePath = this->currentNote.relativeNoteFilePath();
96989695

96999696
// Collect notes that actually contain links with the old heading fragment
97009697
QVector<Note> notesWithHeadingLinks;
97019698

9699+
// First, check the current note itself for links to its own headings (e.g., TOC)
9700+
// NOTE: We check the current text from the UI since this function is called BEFORE
9701+
// the heading is actually updated in the document (to avoid QMessageBox crashes)
9702+
QString currentNoteText = activeNoteTextEdit()->toPlainText();
9703+
if (currentNoteText.contains(QStringLiteral("#") + oldFragment)) {
9704+
notesWithHeadingLinks.append(this->currentNote);
9705+
}
9706+
9707+
// Find all notes that have backlinks to the current note
9708+
QVector<int> backlinkNoteIds = this->currentNote.findBacklinkedNoteIds();
9709+
97029710
for (int noteId : backlinkNoteIds) {
97039711
Note backlinkNote = Note::fetch(noteId);
97049712
QString noteText = backlinkNote.getNoteText();
@@ -9734,14 +9742,75 @@ void MainWindow::updateBacklinksAfterHeadingRename(const QString &oldHeading,
97349742

97359743
for (Note &backlinkNote : notesWithHeadingLinks) {
97369744
QString noteText = backlinkNote.getNoteText();
9737-
9738-
// Replace all occurrences of the old heading fragment with the new one
9739-
QString oldFragmentPattern = QStringLiteral("#") + oldFragment;
9740-
QString newFragmentPattern = QStringLiteral("#") + newFragment;
9741-
9742-
if (noteText.contains(oldFragmentPattern)) {
9743-
noteText.replace(oldFragmentPattern, newFragmentPattern);
9744-
backlinkNote.storeNewText(std::move(noteText));
9745+
QString originalNoteText = noteText;
9746+
9747+
// Build patterns to match the full link including the note path
9748+
// We need to handle both URL-encoded and unencoded paths since links can be either
9749+
// For example: "My Note.md" or "My%20Note.md"
9750+
9751+
// Get both encoded and unencoded versions of the path
9752+
QString currentNoteRelativePathEncoded =
9753+
QString(QUrl::toPercentEncoding(currentNoteRelativePath));
9754+
9755+
// Escape both versions for use in regex (dots, slashes, etc.)
9756+
QString escapedRelativePath = QRegularExpression::escape(currentNoteRelativePath);
9757+
QString escapedRelativePathEncoded =
9758+
QRegularExpression::escape(currentNoteRelativePathEncoded);
9759+
QString oldFragmentEscaped = QRegularExpression::escape(oldFragment);
9760+
9761+
// Pattern 1: Markdown links with unencoded path [text](path#old-heading)
9762+
QRegularExpression markdownLinkPattern(QStringLiteral(R"(\[([^\]]*)\]\(%1#%2\))")
9763+
.arg(escapedRelativePath, oldFragmentEscaped));
9764+
QString markdownLinkReplacement =
9765+
QStringLiteral(R"([\1](%1#%2))").arg(currentNoteRelativePathEncoded, newFragment);
9766+
noteText.replace(markdownLinkPattern, markdownLinkReplacement);
9767+
9768+
// Pattern 2: Markdown links with encoded path [text](path%20with%20spaces#old-heading)
9769+
QRegularExpression markdownLinkPatternEncoded(
9770+
QStringLiteral(R"(\[([^\]]*)\]\(%1#%2\))")
9771+
.arg(escapedRelativePathEncoded, oldFragmentEscaped));
9772+
noteText.replace(markdownLinkPatternEncoded, markdownLinkReplacement);
9773+
9774+
// Pattern 3: Autolinks with unencoded path <path#old-heading>
9775+
QRegularExpression autolinkPattern(
9776+
QStringLiteral(R"(<(%1#%2)>)").arg(escapedRelativePath, oldFragmentEscaped));
9777+
QString autolinkReplacement =
9778+
QStringLiteral(R"(<%1#%2>)").arg(currentNoteRelativePathEncoded, newFragment);
9779+
noteText.replace(autolinkPattern, autolinkReplacement);
9780+
9781+
// Pattern 4: Autolinks with encoded path <path%20with%20spaces#old-heading>
9782+
QRegularExpression autolinkPatternEncoded(
9783+
QStringLiteral(R"(<(%1#%2)>)").arg(escapedRelativePathEncoded, oldFragmentEscaped));
9784+
noteText.replace(autolinkPatternEncoded, autolinkReplacement);
9785+
9786+
// Pattern 5: Bare markdown links without path (same note references)
9787+
// For links within the same note: [text](#old-heading)
9788+
if (backlinkNote.getId() == this->currentNote.getId()) {
9789+
QRegularExpression sameNoteLinkPattern(
9790+
QStringLiteral(R"(\[([^\]]*)\]\(#%1\))").arg(oldFragmentEscaped));
9791+
QString sameNoteLinkReplacement = QStringLiteral(R"([\1](#%1))").arg(newFragment);
9792+
noteText.replace(sameNoteLinkPattern, sameNoteLinkReplacement);
9793+
9794+
// Also handle autolinks within the same note: <#old-heading>
9795+
QRegularExpression sameNoteAutolinkPattern(
9796+
QStringLiteral(R"(<#%1>)").arg(oldFragmentEscaped));
9797+
QString sameNoteAutolinkReplacement = QStringLiteral(R"(<#%1>)").arg(newFragment);
9798+
noteText.replace(sameNoteAutolinkPattern, sameNoteAutolinkReplacement);
9799+
}
9800+
9801+
// Only update the note if changes were made
9802+
if (noteText != originalNoteText) {
9803+
// If this is the current note, update the text edit directly
9804+
// (since the heading rename hasn't been applied yet in the document)
9805+
if (backlinkNote.getId() == this->currentNote.getId()) {
9806+
// Update the text in the editor
9807+
activeNoteTextEdit()->setPlainText(noteText);
9808+
// Store the changes
9809+
this->currentNote.storeNewText(std::move(noteText));
9810+
} else {
9811+
// For other notes, just store the text to disk
9812+
backlinkNote.storeNewText(std::move(noteText));
9813+
}
97459814
updatedCount++;
97469815
}
97479816
}

0 commit comments

Comments
 (0)