Skip to content

Commit 38e540a

Browse files
committed
#3467 lsp: fix performance problems on large notes
Signed-off-by: Patrizio Bekerle <patrizio@bekerle.com>
1 parent 191ec82 commit 38e540a

10 files changed

Lines changed: 525 additions & 27 deletions

CHANGELOG.md

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

3+
## 26.4.25
4+
5+
- Major performance improvement for the Markdown LSP integration on large notes
6+
(for [#3467](https://github.com/pbek/QOwnNotes/issues/3467))
7+
- Added a new `MarkdownLspDocumentTracker` class that uses Qt's
8+
`QTextDocument::contentsChange` signal to track edits incrementally instead
9+
of calling `toPlainText()` on every keystroke; the per-keystroke cost drops
10+
from O(n) (full-document copy) to O(log n + delta)
11+
- The client now reads the server's `textDocumentSync` capability from the
12+
initialize response and sends **incremental** `didChange` payloads
13+
(`TextDocumentSyncKind=2`) when the server supports it (e.g. `marksman`),
14+
falling back to full-text sync for servers like `rumdl`
15+
- Optimized the LSP diagnostic wave-underline painter in the highlighter to
16+
batch contiguous characters with the same base format into single
17+
`setFormat()` calls, reducing format-range fragmentation from O(characters)
18+
to O(format-runs)
19+
- When the Markdown LSP feature is disabled, **zero additional work** is
20+
performed on the text-change hot path — no `toPlainText()`, no timer
21+
restarts, no signal connections fire
22+
323
## 26.4.24
424

525
- Fixed a URI percent-encoding mismatch that prevented Markdown LSP diagnostics

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@ set(SOURCE_FILES
393393
services/harpertypes.h
394394
services/markdownlspclient.cpp
395395
services/markdownlspclient.h
396+
services/markdownlspdocumenttracker.cpp
397+
services/markdownlspdocumenttracker.h
396398
services/settingsservice.cpp
397399
services/settingsservice.h
398400
libraries/piwiktracker/piwiktracker.h

src/QOwnNotes.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ SOURCES += main.cpp\
187187
services/webappclientservice.cpp \
188188
services/openaiservice.cpp \
189189
services/markdownlspclient.cpp \
190+
services/markdownlspdocumenttracker.cpp \
190191
services/mcpservice.cpp \
191192
services/settingsservice.cpp \
192193
dialogs/masterdialog.cpp \
@@ -351,6 +352,7 @@ HEADERS += mainwindow.h \
351352
services/cryptoservice.h \
352353
services/openaiservice.h \
353354
services/markdownlspclient.h \
355+
services/markdownlspdocumenttracker.h \
354356
services/mcpservice.h \
355357
services/settingsservice.h \
356358
dialogs/masterdialog.h \

src/helpers/qownnotesmarkdownhighlighter.cpp

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -712,16 +712,49 @@ void QOwnNotesMarkdownHighlighter::highlightMarkdownLsp(const QString &text) {
712712
/**
713713
* Applies a wave underline to a character range in the current block, merging
714714
* with the existing per-character format to preserve syntax highlighting colors.
715+
*
716+
* Batches runs of characters that share the same base format into single
717+
* setFormat() calls so the text layout sees O(runs) format ranges instead
718+
* of O(characters), which matters for long diagnostic ranges.
715719
*/
716720
void QOwnNotesMarkdownHighlighter::setMarkdownLspUnderline(int start, int count,
717721
const QColor &color,
718722
const QString &toolTip) {
719-
for (int i = start; i < start + count; ++i) {
720-
QTextCharFormat format = QSyntaxHighlighter::format(i);
721-
format.setFontUnderline(true);
722-
format.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
723-
format.setUnderlineColor(color);
724-
format.setToolTip(toolTip);
725-
setFormat(i, 1, format);
723+
if (count <= 0) {
724+
return;
725+
}
726+
727+
const int end = start + count;
728+
int runStart = start;
729+
730+
// Read the base format at the first character and build the merged format
731+
QTextCharFormat baseFormat = QSyntaxHighlighter::format(runStart);
732+
QTextCharFormat merged = baseFormat;
733+
merged.setFontUnderline(true);
734+
merged.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
735+
merged.setUnderlineColor(color);
736+
merged.setToolTip(toolTip);
737+
738+
for (int i = start + 1; i < end; ++i) {
739+
const QTextCharFormat fmt = QSyntaxHighlighter::format(i);
740+
if (fmt == baseFormat) {
741+
// Same base format — extend the current run
742+
continue;
743+
}
744+
745+
// Flush the current run
746+
setFormat(runStart, i - runStart, merged);
747+
748+
// Start a new run with the new base format
749+
runStart = i;
750+
baseFormat = fmt;
751+
merged = baseFormat;
752+
merged.setFontUnderline(true);
753+
merged.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
754+
merged.setUnderlineColor(color);
755+
merged.setToolTip(toolTip);
726756
}
757+
758+
// Flush the last run
759+
setFormat(runStart, end - runStart, merged);
727760
}

src/services/markdownlspclient.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,52 @@ void MarkdownLspClient::didOpen(const QString &uri, const QString &languageId, c
166166
sendMessage(obj);
167167
}
168168

169+
MarkdownLspClient::TextDocumentSyncKind MarkdownLspClient::serverSyncKind() const {
170+
return _serverSyncKind;
171+
}
172+
173+
void MarkdownLspClient::didChangeIncremental(const QString &uri,
174+
const QVector<IncrementalChange> &changes,
175+
int version) {
176+
if (!_initialized || changes.isEmpty()) {
177+
return;
178+
}
179+
180+
QJsonObject doc;
181+
doc.insert(QStringLiteral("uri"), uri);
182+
doc.insert(QStringLiteral("version"), version);
183+
184+
QJsonArray changesArray;
185+
for (const IncrementalChange &change : changes) {
186+
QJsonObject start;
187+
start.insert(QStringLiteral("line"), change.startLine);
188+
start.insert(QStringLiteral("character"), change.startCharacter);
189+
190+
QJsonObject end;
191+
end.insert(QStringLiteral("line"), change.endLine);
192+
end.insert(QStringLiteral("character"), change.endCharacter);
193+
194+
QJsonObject range;
195+
range.insert(QStringLiteral("start"), start);
196+
range.insert(QStringLiteral("end"), end);
197+
198+
QJsonObject changeObj;
199+
changeObj.insert(QStringLiteral("range"), range);
200+
changeObj.insert(QStringLiteral("text"), change.text);
201+
changesArray.append(changeObj);
202+
}
203+
204+
QJsonObject params;
205+
params.insert(QStringLiteral("textDocument"), doc);
206+
params.insert(QStringLiteral("contentChanges"), changesArray);
207+
208+
QJsonObject obj;
209+
obj.insert(QStringLiteral("jsonrpc"), QStringLiteral("2.0"));
210+
obj.insert(QStringLiteral("method"), QStringLiteral("textDocument/didChange"));
211+
obj.insert(QStringLiteral("params"), params);
212+
sendMessage(obj);
213+
}
214+
169215
void MarkdownLspClient::didChange(const QString &uri, const QString &text, int version) {
170216
if (!_initialized) {
171217
_pendingDocument.uri = uri;
@@ -498,6 +544,34 @@ void MarkdownLspClient::handleResponse(const QJsonObject &object) {
498544
if (id == _initializeRequestId) {
499545
_initialized = true;
500546
_initializeRequestId = -1;
547+
548+
// Extract textDocumentSync capability from the server response.
549+
// The sync kind can appear as a bare integer or inside an options object.
550+
_serverSyncKind = SyncFull;
551+
const QJsonObject result = object.value(QStringLiteral("result")).toObject();
552+
const QJsonObject capabilities = result.value(QStringLiteral("capabilities")).toObject();
553+
const QJsonValue syncValue = capabilities.value(QStringLiteral("textDocumentSync"));
554+
if (syncValue.isDouble()) {
555+
const int kind = syncValue.toInt(1);
556+
if (kind == 2) {
557+
_serverSyncKind = SyncIncremental;
558+
} else if (kind == 0) {
559+
_serverSyncKind = SyncNone;
560+
}
561+
} else if (syncValue.isObject()) {
562+
const QJsonObject syncObj = syncValue.toObject();
563+
const int kind = syncObj.value(QStringLiteral("change")).toInt(1);
564+
if (kind == 2) {
565+
_serverSyncKind = SyncIncremental;
566+
} else if (kind == 0) {
567+
_serverSyncKind = SyncNone;
568+
}
569+
}
570+
571+
if (_verboseLogging) {
572+
qDebug() << "Markdown LSP server sync kind:" << static_cast<int>(_serverSyncKind);
573+
}
574+
501575
emit serverInitialized();
502576
sendInitializedNotification();
503577
flushPendingDocument();

src/services/markdownlspclient.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
#include <QStringList>
1111
#include <QVector>
1212

13+
class MarkdownLspDocumentTracker;
14+
1315
class MarkdownLspClient : public QObject {
1416
Q_OBJECT
1517

1618
public:
19+
/// LSP TextDocumentSyncKind advertised by the server.
20+
enum TextDocumentSyncKind { SyncNone = 0, SyncFull = 1, SyncIncremental = 2 };
21+
1722
struct DiagnosticRange {
1823
int startLine = 0;
1924
int startCharacter = 0;
@@ -41,6 +46,15 @@ class MarkdownLspClient : public QObject {
4146
QString message;
4247
};
4348

49+
/// Incremental content change used by MarkdownLspDocumentTracker.
50+
struct IncrementalChange {
51+
int startLine = 0;
52+
int startCharacter = 0;
53+
int endLine = 0;
54+
int endCharacter = 0;
55+
QString text;
56+
};
57+
4458
explicit MarkdownLspClient(QObject *parent = nullptr);
4559
~MarkdownLspClient() override;
4660

@@ -50,11 +64,16 @@ class MarkdownLspClient : public QObject {
5064
void shutdown();
5165
bool isRunning() const;
5266

67+
/// Returns the sync kind advertised by the server after initialization.
68+
TextDocumentSyncKind serverSyncKind() const;
69+
5370
void initialize(const QString &rootPath, const QString &clientName,
5471
const QString &clientVersion);
5572

5673
void didOpen(const QString &uri, const QString &languageId, const QString &text, int version);
5774
void didChange(const QString &uri, const QString &text, int version);
75+
void didChangeIncremental(const QString &uri, const QVector<IncrementalChange> &changes,
76+
int version);
5877
void didClose(const QString &uri);
5978

6079
int requestCompletion(const QString &uri, int line, int character);
@@ -104,6 +123,7 @@ class MarkdownLspClient : public QObject {
104123
int _nextRequestId = 1;
105124
int _initializeRequestId = -1;
106125
bool _initialized = false;
126+
TextDocumentSyncKind _serverSyncKind = SyncFull;
107127
PendingDocument _pendingDocument;
108128
QSet<int> _completionRequestIds;
109129
QHash<int, QString> _codeActionUriByRequest;

0 commit comments

Comments
 (0)