Skip to content

Fix slowness and lag when editing large buffers#757

Open
S4deghN wants to merge 1 commit into
yegappan:mainfrom
S4deghN:main
Open

Fix slowness and lag when editing large buffers#757
S4deghN wants to merge 1 commit into
yegappan:mainfrom
S4deghN:main

Conversation

@S4deghN

@S4deghN S4deghN commented Feb 3, 2026

Copy link
Copy Markdown

This resolved slow buffer updates and lags when editing large files for me.
Of course, this is only correct to do if the whole buffer is sent to the server, which is currently the case.

@mmrwoods mmrwoods left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a really good idea to me, even if the range based buffer update issue is fixed.

I think it should be a configurable option though, like Neovim's debounce_text_changes option, and the code should be updated to keep to the existing style (use {get,set}bufvar, see other timers in the yegappan/lsp code).

Here's a diff from main including those changes:

diff --git a/autoload/lsp/lsp.vim b/autoload/lsp/lsp.vim
index 57c08ad..ec72700 100644
--- a/autoload/lsp/lsp.vim
+++ b/autoload/lsp/lsp.vim
@@ -465,7 +465,15 @@ def BufferInit(lspserverId: number, bnr: number): void
 
   # add a listener to track changes to this buffer
   listener_add((_bnr: number, start: number, end: number, added: number, changes: list<dict<number>>) => {
-    lspserver.textdocDidChange(bnr, start, end, added, changes)
+    var timerid: number = getbufvar(bnr, 'LspDidChangeTimer', 0)
+    if timerid != 0
+      return
+    endif
+    timerid = timer_start(opt.lspOptions.textChangeDelay, (_) => {
+      lspserver.textdocDidChange(bnr, start, end, added, changes)
+      setbufvar(bnr, 'LspDidChangeTimer', 0)
+    })
+    setbufvar(bnr, 'LspDidChangeTimer', timerid)
   }, bnr)
 
   AddBufLocalAutocmds(lspserver, bnr)
diff --git a/autoload/lsp/options.vim b/autoload/lsp/options.vim
index d1f5fb8..265454a 100644
--- a/autoload/lsp/options.vim
+++ b/autoload/lsp/options.vim
@@ -177,6 +177,9 @@ export var lspOptions: dict<any> = {
   # enable snippet completion support
   snippetSupport: false,
 
+  # Delay in milliseconds for sending didChange notifications to the server
+  textChangeDelay: 150,
+
   # enable SirVer/ultisnips completion support
   ultisnipsSupport: false,
 

@yegappan

yegappan commented Mar 2, 2026

Copy link
Copy Markdown
Owner

Thanks for the patch. Can you rebase this PR to the latest version of the plugin and incorporate the suggestions from @mmrwoods?

@mmrwoods

Copy link
Copy Markdown
Contributor

@yegappan I've had another look at this and concluded it's the wrong thing to do without taking into account full vs incremental changes... as @S4deghN indicated, debouncing like this without also tracking the accumulated changes only makes sense when the TextDocumentSyncKind is Full rather than Incremental (the code currently assumes Full, even though this probably breaks servers that request Incremental).

To revisit :-)

@mmrwoods

Copy link
Copy Markdown
Contributor

I've had another look at this, and FWIW here is a diff that adds the feature, with option, but as it stands this causes problems with omnicompletion, I'm not sure why yet (note the small change in debounce logic that causes the last pending change to be sent, not the first)...

diff --git a/autoload/lsp/lsp.vim b/autoload/lsp/lsp.vim
index 6b2bdd2..89b41c0 100644
--- a/autoload/lsp/lsp.vim
+++ b/autoload/lsp/lsp.vim
@@ -488,7 +488,19 @@ def BufferInit(lspserverId: number, bnr: number): void
 
   # add a listener to track changes to this buffer
   listener_add((_bnr: number, start: number, end: number, added: number, changes: list<dict<number>>) => {
-    lspserver.textdocDidChange(bnr, start, end, added, changes)
+    var timerid: number = getbufvar(bnr, 'LspDidChangeTimer', 0)
+    # always notify on the latest change, discard other pending notifications
+    # WARNING: this only works when TextDocumentSyncKind is Full, or if pending
+    # chagnes are accumulated or computed via diff with original on notfication.
+    # Currently lspserver.TextdocDidChange() assumes TextDocumentSyncKind Full
+    if timerid != 0
+      timer_stop(timerid)
+    endif
+    timerid = timer_start(opt.lspOptions.textChangeDelay, (_) => {
+      lspserver.textdocDidChange(bnr, start, end, added, changes)
+      setbufvar(bnr, 'LspDidChangeTimer', 0)
+    })
+    setbufvar(bnr, 'LspDidChangeTimer', timerid)
   }, bnr)
 
   AddBufLocalAutocmds(lspserver, bnr)
diff --git a/autoload/lsp/options.vim b/autoload/lsp/options.vim
index 6667d57..f785057 100644
--- a/autoload/lsp/options.vim
+++ b/autoload/lsp/options.vim
@@ -191,6 +191,9 @@ export var lspOptions: dict<any> = {
   # enable snippet completion support
   snippetSupport: false,
 
+  # Delay in milliseconds for sending didChange notifications to the server
+  textChangeDelay: 150,
+
   # enable SirVer/ultisnips completion support
   ultisnipsSupport: false,
 
diff --git a/doc/lsp.txt b/doc/lsp.txt
index 93f3868..d127748 100644
--- a/doc/lsp.txt
+++ b/doc/lsp.txt
@@ -1921,6 +1921,12 @@ snippetSupport		|Boolean| option.  Enable snippet completion support.
 			Need a snippet completion plugin like vim-vsnip.
 			By default this is set to false.
 
+						*lsp-opt-textChangeDelay*
+textChangeDelay		|Number| option. Delay in milliseconds for sending
+			didChange notifications to the server. Prevents
+			excessive requests during rapid text changes.
+			By default this is set to 150.
+
 						*lsp-opt-ultisnipsSupport*
 ultisnipsSupport	|Boolean| option.  Enable SirVer/ultisnips support.
 			Need a snippet completion plugin SirVer/ultisnips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants