Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lsp: debounced textChanges are not segmented by buffer #16424

Closed
yorickpeterse opened this issue Nov 24, 2021 · 15 comments · Fixed by #16431
Closed

lsp: debounced textChanges are not segmented by buffer #16424

yorickpeterse opened this issue Nov 24, 2021 · 15 comments · Fixed by #16431
Labels
bug issues reporting wrong behavior lsp

Comments

@yorickpeterse
Copy link
Contributor

Neovim version (nvim -v)

NVIM v0.6.0-dev 619-gcfa5d0680

Vim (not Nvim) behaves the same?

No

Operating system/version

Arch Linux

Terminal name/version

nvim-qt

$TERM environment variable

nvim-qt

Installation

Git

How to reproduce the issue

Reproducing this is a bit tricky, and I haven't been able to narrow it down to
something small. I can however reliably reproduce the issue with a project of
mine. You'll need to have rust-analyzer configured for this to work. For
reference, I use the following settings (minus settings not relevant for this
issue):

config.rust_analyzer.setup({
  flags = {
    allow_incremental_sync = true,
    debounce_text_changes = 1000,
  },
  settings = {
    ['rust-analyzer'] = {
      diagnostics = {
        enable = true,
        enableExperimental = false,
      },
      inlayHints = {
        typeHints = false,
        chainingHints = false,
        enable = false,
      },
      server = {
        path = '/usr/bin/rust-analyzer',
      },
      lruCapacity = 64,
      completion = {
        postfix = {
          enable = false,
        },
      },
    },
  },
})

First, clone said project:

git clone https://gitlab.com/inko-lang/inko.git --branch=single-ownership

Then run the following:

cd inko
git checkout fd6a75575aafc3be3b5d4a285d344ab907458f2f
cargo build

Next, follow these steps:

  1. Open compiler/src/types.rs compiler/src/type_check.rs
  2. In types.rs, navigate to the get_constant function on line 254
  3. Place the cursor on the start of the function name
  4. Run :lua vim.lsp.buf.rename()
  5. Enter blorb as the new name and confirm the rename
  6. Note that in type_check.rs you'll see a bunch of syntax errors being
    reported, in spite of the code in question being valid
  7. If you at this point try to undo the changes and rename the function again,
    nothing happens

With incremental sync disabled, I can't reproduce this. Instead I get a
different problem: after the first rename of get_constant to blorb, if I try
to rename the function again the replacement text includes a (. If you then
try to rename the function, your code gets all messed up.

Here is a recording that shows what happens when incremental sync is enabled:

asciicast

And this is what happens when you disable incremental sync:

asciicast

Expected behavior

Renaming works fine, and no syntax errors are produced

Actual behavior

Either NeoVim, rust-analyzer or both end up getting messed up when incremental sync is enabled

@yorickpeterse yorickpeterse added the bug issues reporting wrong behavior label Nov 24, 2021
@yorickpeterse
Copy link
Contributor Author

@mjlbach suggested I patch rust-analyzer to show the old content. I did so as follows:

diff --git a/crates/rust-analyzer/src/lsp_utils.rs b/crates/rust-analyzer/src/lsp_utils.rs
index 194d03032..4f1080a14 100644
--- a/crates/rust-analyzer/src/lsp_utils.rs
    b/crates/rust-analyzer/src/lsp_utils.rs
@@ -143,6  143,11 @@ pub(crate) fn apply_document_changes(
         }
     }
 
     let sys_time =
         std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis();
     let str = format!("/tmp/nvim-lsp-{}", sys_time);
     std::fs::write(str, &old_text).expect("Unable to write file");
 
     let mut index_valid = IndexValid::All;
     for change in content_changes {
         match change.range {

When incremental sync is enabled and I rename the function, the following files are produced (in this order):

Using the second file we can see the buffer state is messed up. For example, this code is incorrect in rust-analyzer, though the code shown in the corresponding nvim buffer is fine.

@yorickpeterse
Copy link
Contributor Author

It's possible this is a bug with rust-analyzer, but I had similar issues with lua-language-server in the past, though there they'd happen when typing not when renaming. I haven't been able to reproduce a similar issue with Lua.

@yorickpeterse
Copy link
Contributor Author

It's not unlikely this is a rust-analyzer issue, but I think part of it may be due to nvim's behaviour, as disabling incremental sync doesn't trigger this problem.

@yorickpeterse
Copy link
Contributor Author

LSP debug logs from a rename session, with most Rust warnings silenced to not clutter the log files: https://gist.github.com/YorickPeterse/a6c5262241ca03ebef63adac18e6e142

@yorickpeterse
Copy link
Contributor Author

It appears this may be caused my me using debounce_text_changes = 1000. When I disable this setting entirely, I'm unable to reproduce the renaming issue leading to syntax errors. Perhaps the debouncing results in a state mismatch between nvim and rust-analyzer?

@yorickpeterse
Copy link
Contributor Author

Even with debounce_text_changes = 10 the issue happens. This suggests it may be less due to the exact time used, and more due to debouncing being applied in the first case.

@mjlbach
Copy link
Contributor

mjlbach commented Nov 24, 2021

I'm not sure then, maybe RA is not respecting the order in which we send the textdocument/didChanged (batched from the debounce) and the rename request

@yorickpeterse
Copy link
Contributor Author

I managed to reduce this down to the following way to reproduce the issue:

src/main.rs:

pub mod types;

use types::{Database, ModuleId};

fn main() {
    let id = ModuleId(42);
    let db = Database;
    let cons1 = id.get_constant(&db, "Foo");
    let cons2 = id.get_constant(&db, "Foo");
    let cons3 = id.get_constant(&db, "Foo");
}

src/types.rs:

pub struct ModuleId(pub usize);

impl ModuleId {
    pub fn get_constant(self, db: &Database, name: &str) -> Option<()> {
        None
    }
}

pub struct Database;

If you then use :lua vim.lsp.buf.rename() to rename the get_constant function, you'll get a bunch of random/stray syntax errors back from rust-analyzer.

@yorickpeterse
Copy link
Contributor Author

If I use these file contents, I don't get syntax errors but instead rust-analyzer straight up crashes:

src/main.rs:

pub mod types;

use types::ModuleId;

fn main() {
    let id = ModuleId(42);
    let cons1 = id.get_constant("Foo");
    let cons2 = id.get_constant("Foo");
    let cons3 = id.get_constant("Foo");
}

src/types.rs:

pub struct ModuleId(pub usize);

impl ModuleId {
    pub fn get_constant(&self, name: &str) -> Option<()> {
        None
    }
}

If you rename get_constant, RA crashes with the following:

[START][2021-11-24 22:37:52] LSP logging initiated
[ERROR][2021-11-24 22:38:03] .../vim/lsp/rpc.lua:412	"rpc"	"rust-analyzer"	"stderr"	"Panic context:\n> \nversion: 2021-11-08\nnotification: textDocument/didChange\n\nthread 'main' panicked at 'index out of bounds: the len is 8 but the index is 8', crates/ide_db/src/line_index.rs:106:9\nstack backtrace:\n"
[ERROR][2021-11-24 22:38:03] .../vim/lsp/rpc.lua:412	"rpc"	"rust-analyzer"	"stderr"	"note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.\n"

With debug logging enabled, the logs triggered by the rename are as follows:

[DEBUG][2021-11-24 22:39:10] .../lua/vim/lsp.lua:945	"LSP[rust_analyzer]"	"client.request"	1	"textDocument/prepareRename"	{  position = {    character = 11,    line = 3  },  textDocument = {    uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs"  }}	<function 1>	10
[DEBUG][2021-11-24 22:39:10] .../vim/lsp/rpc.lua:339	"rpc.send"	{  id = 2,  jsonrpc = "2.0",  method = "textDocument/prepareRename",  params = {    position = {      character = 11,      line = 3    },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs"    }  }}
[DEBUG][2021-11-24 22:39:10] .../vim/lsp/rpc.lua:446	"rpc.receive"	{  id = 2,  jsonrpc = "2.0",  result = {    end = {      character = 23,      line = 3    },    start = {      character = 11,      line = 3    }  }}
[DEBUG][2021-11-24 22:39:11] .../lua/vim/lsp.lua:945	"LSP[rust_analyzer]"	"client.request"	1	"textDocument/rename"	{  newName = "blorb",  position = {    character = 11,    line = 3  },  textDocument = {    uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs"  }}	<function 1>	10
[DEBUG][2021-11-24 22:39:11] .../vim/lsp/rpc.lua:339	"rpc.send"	{  id = 3,  jsonrpc = "2.0",  method = "textDocument/rename",  params = {    newName = "blorb",    position = {      character = 11,      line = 3    },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs"    }  }}
[DEBUG][2021-11-24 22:39:11] .../vim/lsp/rpc.lua:446	"rpc.receive"	{  id = 3,  jsonrpc = "2.0",  result = {    documentChanges = { {        edits = { {            newText = "blorb",            range = {              end = {                character = 31,                line = 6              },              start = {                character = 19,                line = 6              }            }          }, {            newText = "blorb",            range = {              end = {                character = 31,                line = 7              },              start = {                character = 19,                line = 7              }            }          }, {            newText = "blorb",            range = {              end = {                character = 31,                line = 8              },              start = {                character = 19,                line = 8              }            }          } },        textDocument = {          uri = "file:///home/yorickpeterse/Projects/rust/playground/src/main.rs",          version = 0        }      }, {        edits = { {            newText = "blorb",            range = {              end = {                character = 23,                line = 3              },              start = {                character = 11,                line = 3              }            }          } },        textDocument = {          uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs",          version = 0        }      } }  }}
[DEBUG][2021-11-24 22:39:12] .../vim/lsp/rpc.lua:339	"rpc.send"	{  jsonrpc = "2.0",  method = "textDocument/didChange",  params = {    contentChanges = { {        range = {          end = {            character = 31,            line = 8          },          start = {            character = 19,            line = 8          }        },        rangeLength = 12,        text = "blorb"      }, {        range = {          end = {            character = 31,            line = 7          },          start = {            character = 19,            line = 7          }        },        rangeLength = 12,        text = "blorb"      }, {        range = {          end = {            character = 31,            line = 6          },          start = {            character = 19,            line = 6          }        },        rangeLength = 12,        text = "blorb"      }, {        range = {          end = {            character = 23,            line = 3          },          start = {            character = 11,            line = 3          }        },        rangeLength = 12,        text = "blorb"      } },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs",      version = 3    }  }}
[ERROR][2021-11-24 22:39:12] .../vim/lsp/rpc.lua:412	"rpc"	"rust-analyzer"	"stderr"	"Panic context:\n> \nversion: 2021-11-08\nnotification: textDocument/didChange\n\nthread 'main' panicked at 'index out of bounds: the len is 8 but the index is 8', crates/ide_db/src/line_index.rs:106:9\nstack backtrace:\n"
[ERROR][2021-11-24 22:39:12] .../vim/lsp/rpc.lua:412	"rpc"	"rust-analyzer"	"stderr"	"note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.\n"
[INFO][2021-11-24 22:39:14] .../lua/vim/lsp.lua:1261	"exit_handler"	{}

@yorickpeterse
Copy link
Contributor Author

With debouncing disabled, I can't reproduce these issues; including the crash. The debug logs for the rename in that case are as follows:

[DEBUG][2021-11-24 22:41:12] .../lua/vim/lsp.lua:945	"LSP[rust_analyzer]"	"client.request"	1	"textDocument/prepareRename"	{  position = {    character = 11,    line = 3  },  textDocument = {    uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs"  }}	<function 1>	10
[DEBUG][2021-11-24 22:41:12] .../vim/lsp/rpc.lua:339	"rpc.send"	{  id = 2,  jsonrpc = "2.0",  method = "textDocument/prepareRename",  params = {    position = {      character = 11,      line = 3    },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs"    }  }}
[DEBUG][2021-11-24 22:41:12] .../vim/lsp/rpc.lua:446	"rpc.receive"	{  id = 2,  jsonrpc = "2.0",  result = {    end = {      character = 23,      line = 3    },    start = {      character = 11,      line = 3    }  }}
[DEBUG][2021-11-24 22:41:13] .../lua/vim/lsp.lua:945	"LSP[rust_analyzer]"	"client.request"	1	"textDocument/rename"	{  newName = "blorb",  position = {    character = 11,    line = 3  },  textDocument = {    uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs"  }}	<function 1>	10
[DEBUG][2021-11-24 22:41:13] .../vim/lsp/rpc.lua:339	"rpc.send"	{  id = 3,  jsonrpc = "2.0",  method = "textDocument/rename",  params = {    newName = "blorb",    position = {      character = 11,      line = 3    },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs"    }  }}
[DEBUG][2021-11-24 22:41:13] .../vim/lsp/rpc.lua:446	"rpc.receive"	{  id = 3,  jsonrpc = "2.0",  result = {    documentChanges = { {        edits = { {            newText = "blorb",            range = {              end = {                character = 31,                line = 6              },              start = {                character = 19,                line = 6              }            }          }, {            newText = "blorb",            range = {              end = {                character = 31,                line = 7              },              start = {                character = 19,                line = 7              }            }          }, {            newText = "blorb",            range = {              end = {                character = 31,                line = 8              },              start = {                character = 19,                line = 8              }            }          } },        textDocument = {          uri = "file:///home/yorickpeterse/Projects/rust/playground/src/main.rs",          version = 0        }      }, {        edits = { {            newText = "blorb",            range = {              end = {                character = 23,                line = 3              },              start = {                character = 11,                line = 3              }            }          } },        textDocument = {          uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs",          version = 0        }      } }  }}
[DEBUG][2021-11-24 22:41:13] .../vim/lsp/rpc.lua:339	"rpc.send"	{  jsonrpc = "2.0",  method = "textDocument/didChange",  params = {    contentChanges = { {        range = {          end = {            character = 31,            line = 8          },          start = {            character = 19,            line = 8          }        },        rangeLength = 12,        text = "blorb"      } },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/main.rs",      version = 5    }  }}
[DEBUG][2021-11-24 22:41:13] .../vim/lsp/rpc.lua:339	"rpc.send"	{  jsonrpc = "2.0",  method = "textDocument/didChange",  params = {    contentChanges = { {        range = {          end = {            character = 31,            line = 7          },          start = {            character = 19,            line = 7          }        },        rangeLength = 12,        text = "blorb"      } },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/main.rs",      version = 6    }  }}
[DEBUG][2021-11-24 22:41:13] .../vim/lsp/rpc.lua:339	"rpc.send"	{  jsonrpc = "2.0",  method = "textDocument/didChange",  params = {    contentChanges = { {        range = {          end = {            character = 31,            line = 6          },          start = {            character = 19,            line = 6          }        },        rangeLength = 12,        text = "blorb"      } },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/main.rs",      version = 7    }  }}
[DEBUG][2021-11-24 22:41:13] .../vim/lsp/rpc.lua:339	"rpc.send"	{  jsonrpc = "2.0",  method = "textDocument/didChange",  params = {    contentChanges = { {        range = {          end = {            character = 23,            line = 3          },          start = {            character = 11,            line = 3          }        },        rangeLength = 12,        text = "blorb"      } },    textDocument = {      uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs",      version = 3    }  }}
[INFO][2021-11-24 22:41:16] .../lua/vim/lsp.lua:1261	"exit_handler"	{ {    _on_attach = <function 1>,    attached_buffers = { true,      [10] = true    },    cancel_request = <function 2>,    commands = {},    config = {      autostart = true,      capabilities = {        callHierarchy = {          dynamicRegistration = false        },        textDocument = {          codeAction = {            codeActionLiteralSupport = {              codeActionKind = {                valueSet = { "", "Empty", "QuickFix", "Refactor", "RefactorExtract", "RefactorInline", "RefactorRewrite", "Source", "SourceOrganizeImports", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports" }              }            },            dataSupport = true,            dynamicRegistration = false,            resolveSupport = {              properties = { "edit" }            }          },          completion = {            completionItem = {              commitCharactersSupport = false,              deprecatedSupport = false,              documentationFormat = { "markdown", "plaintext" },              preselectSupport = false,              snippetSupport = true            },            completionItemKind = {              valueSet = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }            },            contextSupport = false,            dynamicRegistration = false          },          declaration = {            linkSupport = true          },          definition = {            linkSupport = true          },          documentHighlight = {            dynamicRegistration = false          },          documentSymbol = {            dynamicRegistration = false,            hierarchicalDocumentSymbolSupport = true,            symbolKind = {              valueSet = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 }            }          },          hover = {            contentFormat = { "markdown", "plaintext" },            dynamicRegistration = false          },          implementation = {            linkSupport = true          },          publishDiagnostics = {            relatedInformation = true,            tagSupport = {              valueSet = { 1, 2 }            }          },          references = {            dynamicRegistration = false          },          rename = {            dynamicRegistration = false,            prepareSupport = true          },          signatureHelp = {            dynamicRegistration = false,            signatureInformation = {              activeParameterSupport = true,              documentationFormat = { "markdown", "plaintext" },              parameterInformation = {                labelOffsetSupport = true              }            }          },          synchronization = {            didSave = true,            dynamicRegistration = false,            willSave = false,            willSaveWaitUntil = false          },          typeDefinition = {            linkSupport = true          }        },        window = {          showDocument = {            support = false          },          showMessage = {            messageActionItem = {              additionalPropertiesSupport = false            }          },          workDoneProgress = true        },        workspace = {          applyEdit = true,          configuration = true,          symbol = {            dynamicRegistration = false,            hierarchicalWorkspaceSymbolSupport = true,            symbolKind = {              valueSet = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 }            }          },          workspaceEdit = {            resourceOperations = { "rename", "create", "delete" }          },          workspaceFolders = true        }      },      cmd = { "rust-analyzer" },      filetypes = { "rust" },      flags = {        allow_incremental_sync = true      },      get_language_id = <function 3>,      handlers = <1>{},      init_options = vim.empty_dict(),      log_level = 2,      message_level = 2,      name = "rust_analyzer",      on_attach = <function 4>,      on_exit = <function 5>,      on_init = <function 6>,      root_dir = "/home/yorickpeterse/Projects/rust/playground",      settings = {        ["rust-analyzer"] = {          completion = {            postfix = {              enable = false            }          },          diagnostics = {            enable = true,            enableExperimental = false          },          inlayHints = {            chainingHints = false,            enable = false,            typeHints = false          },          lruCapacity = 64,          server = {            path = "/usr/bin/rust-analyzer"          }        }      },      <metatable> = <2>{        __tostring = <function 7>      }    },    handlers = <table 1>,    id = 1,    initialized = true,    is_stopped = <function 8>,    messages = {      messages = {},      name = "rust_analyzer",      progress = {},      status = {}    },    name = "rust_analyzer",    notify = <function 9>,    offset_encoding = "utf-16",    request = <function 10>,    request_sync = <function 11>,    requests = {},    resolved_capabilities = {      call_hierarchy = true,      code_action = <3>{        codeActionKinds = { "", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite" },        resolveProvider = true      },      code_lens = true,      code_lens_resolve = true,      completion = true,      declaration = true,      document_formatting = true,      document_highlight = true,      document_range_formatting = false,      document_symbol = true,      execute_command = false,      find_references = true,      goto_definition = true,      hover = true,      implementation = true,      rename = true,      signature_help = true,      signature_help_trigger_characters = <4>{ "(", "," },      text_document_did_change = 2,      text_document_open_close = true,      text_document_save = <5>vim.empty_dict(),      text_document_save_include_text = false,      text_document_will_save = false,      text_document_will_save_wait_until = false,      type_definition = true,      workspace_folder_properties = {        changeNotifications = false,        supported = false      },      workspace_symbol = true    },    rpc = {      handle = <userdata 1>,      notify = <function 12>,      pid = 39961,      request = <function 13>    },    server_capabilities = {      callHierarchyProvider = true,      codeActionProvider = <table 3>,      codeLensProvider = {        resolveProvider = true      },      completionProvider = {        triggerCharacters = { ":", ".", "'" }      },      declarationProvider = true,      definitionProvider = true,      documentFormattingProvider = true,      documentHighlightProvider = true,      documentOnTypeFormattingProvider = {        firstTriggerCharacter = "=",        moreTriggerCharacter = { ".", ">", "{" }      },      documentRangeFormattingProvider = false,      documentSymbolProvider = true,      experimental = {        hoverRange = true,        joinLines = true,        onEnter = true,        openCargoToml = true,        parentModule = true,        runnables = {          kinds = { "cargo" }        },        ssr = true,        workspaceSymbolScopeKindFiltering = true      },      foldingRangeProvider = true,      hoverProvider = true,      implementationProvider = true,      referencesProvider = true,      renameProvider = {        prepareProvider = true      },      selectionRangeProvider = true,      semanticTokensProvider = {        full = {          delta = true        },        legend = {          tokenModifiers = { "documentation", "declaration", "definition", "static", "abstract", "deprecated", "readonly", "defaultLibrary", "async", "attribute", "callable", "constant", "consuming", "controlFlow", "crateRoot", "injected", "intraDocLink", "library", "mutable", "public", "reference", "trait", "unsafe" },          tokenTypes = { "comment", "keyword", "string", "number", "regexp", "operator", "namespace", "type", "struct", "class", "interface", "enum", "enumMember", "typeParameter", "function", "method", "property", "macro", "variable", "parameter", "angle", "arithmetic", "attribute", "bitwise", "boolean", "brace", "bracket", "builtinAttribute", "builtinType", "character", "colon", "comma", "comparison", "constParameter", "dot", "escapeSequence", "formatSpecifier", "generic", "label", "lifetime", "logical", "operator", "parenthesis", "punctuation", "selfKeyword", "semicolon", "typeAlias", "union", "unresolvedReference" }        },        range = true      },      signatureHelpProvider = {        triggerCharacters = <table 4>      },      textDocumentSync = {        change = 2,        openClose = true,        save = <table 5>      },      typeDefinitionProvider = true,      workspace = {        fileOperations = {          willRename = {            filters = { {                pattern = {                  glob = "**/*.rs",                  matches = "file"                },                scheme = "file"              }, {                pattern = {                  glob = "**",                  matches = "folder"                },                scheme = "file"              } }          }        }      },      workspaceSymbolProvider = true    },    stop = <function 14>,    supports_method = <function 15>,    workspaceFolders = <6>{ {        name = "/home/yorickpeterse/Projects/rust/playground",        uri = "file:///home/yorickpeterse/Projects/rust/playground"      } },    workspace_did_change_configuration = <function 16>,    workspace_folders = <table 6>  } }
[DEBUG][2021-11-24 22:41:16] .../vim/lsp/rpc.lua:339	"rpc.send"	{  id = 4,  jsonrpc = "2.0",  method = "shutdown"}
[DEBUG][2021-11-24 22:41:16] .../vim/lsp/rpc.lua:446	"rpc.receive"	{  id = 4,  jsonrpc = "2.0"}
[DEBUG][2021-11-24 22:41:16] .../vim/lsp/rpc.lua:339	"rpc.send"	{  jsonrpc = "2.0",  method = "exit"}

@yorickpeterse
Copy link
Contributor Author

Based on my testing so far, it seems one requirement is that the symbol you are renaming exists in one file, while you also have occurrences of that symbol in another file. If both the symbol and references are in the same file, I can't seem to reproduce the problem.

@yorickpeterse
Copy link
Contributor Author

With debouncing enabled, the RPC send payload before the crash looks like this:

{
	jsonrpc = "2.0",
	method = "textDocument/didChange",
	params = {
		contentChanges = {
			{
				range = {
					["end"] = {
						character = 31,
						line = 8,
					},
					start = {
						character = 19,
						line = 8,
					},
				},
				rangeLength = 12,
				text = "blorb",
			},
			{
				range = {
					["end"] = {
						character = 31,
						line = 7,
					},
					start = {
						character = 19,
						line = 7,
					},
				},
				rangeLength = 12,
				text = "blorb",
			},
			{
				range = {
					["end"] = {
						character = 31,
						line = 6,
					},
					start = {
						character = 19,
						line = 6,
					},
				},
				rangeLength = 12,
				text = "blorb",
			},
			{
				range = {
					["end"] = {
						character = 23,
						line = 3,
					},
					start = {
						character = 11,
						line = 3,
					},
				},
				rangeLength = 12,
				text = "blorb",
			},
		},
		textDocument = {
			uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs",
			version = 3,
		},
	},
}

Without debouncing, this payload looks like this:

{
	jsonrpc = "2.0",
	method = "textDocument/didChange",
	params = {
		contentChanges = {
			{
				range = {
					["end"] = { character = 31, line = 8 },
					start = { character = 19, line = 8 },
				},
				rangeLength = 12,
				text = "blorb",
			},
		},
		textDocument = {
			uri = "file:///home/yorickpeterse/Projects/rust/playground/src/main.rs",
			version = 5,
		},
	},
}

Looking at the logs, it seems that with debouncing disabled the content changes are sent as separate RPC commands, while with debouncing enabled they are batched.

One thing I find interesting is this:

When debouncing is enabled, the textDocument object is as follows:

textDocument = {
	uri = "file:///home/yorickpeterse/Projects/rust/playground/src/types.rs",
	version = 3,
},

But when debouncing is disabled, it instead is:

textDocument = {
	uri = "file:///home/yorickpeterse/Projects/rust/playground/src/main.rs",
	version = 5,
},

Could it be that perhaps nvim is reporting the wrong document here, resulting in rust-analyzer modifying the wrong buffers internally?

@mjlbach
Copy link
Contributor

mjlbach commented Nov 24, 2021 via email

@yorickpeterse
Copy link
Contributor Author

Looking at the log data, it seems nvim is incorrectly grouping requests. With debouncing, nvim seems to include the changes of all buffers in one payload, using types.rs as the file. However, this payload contains changes for both main.rs and types.rs. Take this chunk for example:

{
	range = {
		["end"] = {
			character = 23,
			line = 3,
		},
		start = {
			character = 11,
			line = 3,
		},
	},
	rangeLength = 12,
	text = "blorb",
},

These are the changes for types.rs, but the payload also includes this:

{
	range = {
		["end"] = {
			character = 31,
			line = 8,
		},
		start = {
			character = 19,
			line = 8,
		},
	},
	rangeLength = 12,
	text = "blorb",
},

These are the changes for main.rs, but the textDocument URI is still for types.rs.

Based on this I think nvim is incorrectly grouping requests across all buffers into a single one, rather than grouping them per buffer.

@mjlbach mjlbach changed the title When renaming functions using rust-analyzer with incremental sync enabled, buffer states get messed up lsp: debounced textChanges are not segmented by buffer Nov 24, 2021
@yorickpeterse
Copy link
Contributor Author

Just to add, though I think at this point it's clear this is an nvim issue:

With the above knowledge, I can also reproduce this with lua-language-server. It's a bit more tricky there as function renaming is wonky (e.g. lua-language-server often just doesn't rename occurrences), but you can reproduce it roughly like so:

  1. Set debounce_text_changes to a high value like 10 seconds
  2. Open two files and introduce a syntax error in one file, then a harmless change in the other (e.g. add local x = 10)
  3. Wait for the changes to make their way to the language server
  4. Undo the changes

Following these steps you'll likely end up with syntax errors in the buffer where you made the harmless changes, even though the code is totally fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug issues reporting wrong behavior lsp
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants