Skip to content

Commit 6c3c034

Browse files
committed
Remove duplicate and consecutive duplicate lines
Closes #893
1 parent 73644df commit 6c3c034

File tree

6 files changed

+182
-0
lines changed

6 files changed

+182
-0
lines changed

src/NotepadNext/ByteArrayUtils.h

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* This file is part of Notepad Next.
3+
* Copyright 2025 Justin Dailey
4+
*
5+
* Notepad Next is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* Notepad Next is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with Notepad Next. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
#pragma once
19+
20+
#include <QByteArray>
21+
#include <QByteArrayView>
22+
#include <QList>
23+
#include <QSet>
24+
#include <cstring>
25+
#include <unordered_set>
26+
27+
namespace ByteArrayUtils
28+
{
29+
30+
// Zero-copy split: returns views into 'source'.
31+
// The source QByteArray must remain alive while using the views.
32+
inline QList<QByteArrayView> split(const QByteArray& source, const QByteArray& delimiter)
33+
{
34+
QList<QByteArrayView> out;
35+
36+
const char* base = source.constData();
37+
qsizetype len = source.size();
38+
qsizetype dlen = delimiter.size();
39+
40+
qsizetype start = 0;
41+
42+
while (true) {
43+
qsizetype pos = source.indexOf(delimiter, start);
44+
if (pos < 0) {
45+
out.append(QByteArrayView(base + start, len - start));
46+
break;
47+
}
48+
49+
out.append(QByteArrayView(base + start, pos - start));
50+
start = pos + dlen;
51+
}
52+
53+
return out;
54+
}
55+
56+
// Remove duplicates and preserve order.
57+
inline void removeDuplicates(QList<QByteArrayView>& parts)
58+
{
59+
std::unordered_set<QByteArray, std::hash<QByteArray>> seen;
60+
61+
auto newEnd = std::remove_if(parts.begin(), parts.end(), [&seen](const QByteArrayView& v) {
62+
QByteArray key(v.data(), v.size()); // copy once per unique value
63+
if (seen.find(key) != seen.end())
64+
return true; // duplicate, remove
65+
seen.insert(std::move(key));
66+
return false; // first occurrence, keep
67+
});
68+
69+
parts.erase(newEnd, parts.end());
70+
}
71+
72+
73+
// Remove only *consecutive* duplicates.
74+
// Example: A A B B B C A → A B C A
75+
inline void removeConsecutiveDuplicates(QList<QByteArrayView>& parts)
76+
{
77+
auto newEnd = std::unique(parts.begin(), parts.end(), [](const QByteArrayView& a, const QByteArrayView& b) {
78+
return a == b;
79+
});
80+
81+
parts.erase(newEnd, parts.end());
82+
}
83+
84+
85+
inline QByteArray join(const QList<QByteArrayView>& parts, const QByteArray& delimiter)
86+
{
87+
if (parts.isEmpty())
88+
return QByteArray();
89+
90+
const qsizetype dlen = delimiter.size();
91+
92+
// Compute total size
93+
qsizetype total = 0;
94+
for (const auto& v : parts)
95+
total += v.size();
96+
total += dlen * (parts.size() - 1);
97+
98+
// Allocate final QByteArray
99+
QByteArray out;
100+
out.resize(total);
101+
102+
// Copy segments
103+
char* dst = out.data();
104+
for (int i = 0; i < parts.size(); ++i) {
105+
const auto& v = parts[i];
106+
107+
memcpy(dst, v.data(), v.size());
108+
dst += v.size();
109+
110+
if (i + 1 < parts.size()) {
111+
memcpy(dst, delimiter.constData(), dlen);
112+
dst += dlen;
113+
}
114+
}
115+
116+
return out;
117+
}
118+
119+
} // namespace ByteArrayViewUtils

src/NotepadNext/NotepadNext.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ SOURCES += \
142142
HEADERS += \
143143
ActionUtils.h \
144144
ApplicationSettings.h \
145+
ByteArrayUtils.h \
145146
ColorPickerDelegate.h \
146147
ComboBoxDelegate.h \
147148
Converter.h \

src/NotepadNext/ScintillaNext.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818

1919

2020
#include "ScintillaNext.h"
21+
#include "Finder.h"
2122
#include "ScintillaCommenter.h"
2223

24+
#include "ByteArrayUtils.h"
2325
#include "uchardet.h"
2426
#include <cinttypes>
2527

@@ -498,6 +500,45 @@ void ScintillaNext::uncommentLineSelection()
498500
sc.uncommentSelection();
499501
}
500502

503+
void ScintillaNext::removeDuplicateLines()
504+
{
505+
QByteArray data = QByteArray::fromRawData((char*) characterPointer(), textLength());
506+
const QByteArray delim = eolString();
507+
508+
auto lines = ByteArrayUtils::split(data, delim);
509+
int originalLineCount = lines.length();
510+
ByteArrayUtils::removeDuplicates(lines);
511+
512+
if (originalLineCount == lines.length()){
513+
return; // No lines were removed
514+
}
515+
516+
QByteArray result = ByteArrayUtils::join(lines, delim);
517+
518+
const UndoAction ua(this);
519+
setTargetRange(0, textLength());
520+
replaceTarget(result.length(), result.constData());
521+
}
522+
523+
void ScintillaNext::removeConsecutiveDuplicateLines()
524+
{
525+
QByteArray data = QByteArray::fromRawData((char*) characterPointer(), textLength());
526+
const QByteArray delim = eolString();
527+
528+
auto lines = ByteArrayUtils::split(data, delim);
529+
int originalLineCount = lines.length();
530+
ByteArrayUtils::removeConsecutiveDuplicates(lines);
531+
QByteArray result = ByteArrayUtils::join(lines, delim);
532+
533+
if (originalLineCount == lines.length()){
534+
return; // No lines were removed
535+
}
536+
537+
const UndoAction ua(this);
538+
setTargetRange(0, textLength());
539+
replaceTarget(result.length(), result.constData());
540+
}
541+
501542
void ScintillaNext::dragEnterEvent(QDragEnterEvent *event)
502543
{
503544
// Ignore all drag and drop events with urls and let the main application handle it

src/NotepadNext/ScintillaNext.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ public slots:
133133
void commentLineSelection();
134134
void uncommentLineSelection();
135135

136+
void removeDuplicateLines();
137+
void removeConsecutiveDuplicateLines();
138+
136139
signals:
137140
void aboutToSave();
138141
void saved();

src/NotepadNext/dialogs/MainWindow.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ MainWindow::MainWindow(NotepadNextApplication *app) :
250250
// Regex will also not delete the final blank line
251251
editor->deleteTrailingEmptyLines();
252252
});
253+
connect(ui->actionRemoveDuplicateLines, &QAction::triggered, this, [=]() {
254+
currentEditor()->removeDuplicateLines();
255+
});
256+
connect(ui->actionRemoveConsecutiveDuplicateLines, &QAction::triggered, this, [=]() {
257+
currentEditor()->removeConsecutiveDuplicateLines();
258+
});
253259

254260
connect(ui->actionColumnMode, &QAction::triggered, this, [=]() {
255261
ColumnEditorDialog *columnEditor = findChild<ColumnEditorDialog *>(QString(), Qt::FindDirectChildrenOnly);

src/NotepadNext/dialogs/MainWindow.ui

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@
137137
<addaction name="actionMoveSelectedLinesDown"/>
138138
<addaction name="separator"/>
139139
<addaction name="actionRemoveEmptyLines"/>
140+
<addaction name="actionRemoveDuplicateLines"/>
141+
<addaction name="actionRemoveConsecutiveDuplicateLines"/>
140142
</widget>
141143
<widget class="QMenu" name="menuCommentUncomment">
142144
<property name="title">
@@ -1472,6 +1474,16 @@
14721474
<string>Clear All Styles</string>
14731475
</property>
14741476
</action>
1477+
<action name="actionRemoveDuplicateLines">
1478+
<property name="text">
1479+
<string>Remove Duplicate Lines</string>
1480+
</property>
1481+
</action>
1482+
<action name="actionRemoveConsecutiveDuplicateLines">
1483+
<property name="text">
1484+
<string>Remove Consecutive Duplicate Lines</string>
1485+
</property>
1486+
</action>
14751487
</widget>
14761488
<layoutdefault spacing="6" margin="11"/>
14771489
<customwidgets>

0 commit comments

Comments
 (0)