mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 09:44:31 +00:00 
			
		
		
		
	fix(lsp): out of bounds error in lsp.util.apply_text_edits (#20137)
Co-authored-by: Jonas Strittmatter <40792180+smjonas@users.noreply.github.com>
This commit is contained in:
		@@ -459,35 +459,52 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
 | 
				
			|||||||
      text = split(text_edit.newText, '\n', true),
 | 
					      text = split(text_edit.newText, '\n', true),
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
 | 
					 | 
				
			||||||
    local max = api.nvim_buf_line_count(bufnr)
 | 
					    local max = api.nvim_buf_line_count(bufnr)
 | 
				
			||||||
    if max <= e.start_row or max <= e.end_row then
 | 
					    -- If the whole edit is after the lines in the buffer we can simply add the new text to the end
 | 
				
			||||||
      local len = #(get_line(bufnr, max - 1) or '')
 | 
					    -- of the buffer.
 | 
				
			||||||
      if max <= e.start_row then
 | 
					    if max <= e.start_row then
 | 
				
			||||||
        e.start_row = max - 1
 | 
					      api.nvim_buf_set_lines(bufnr, max, max, false, e.text)
 | 
				
			||||||
        e.start_col = len
 | 
					    else
 | 
				
			||||||
        table.insert(e.text, 1, '')
 | 
					      local last_line_len = #(get_line(bufnr, math.min(e.end_row, max - 1)) or '')
 | 
				
			||||||
      end
 | 
					      -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't
 | 
				
			||||||
 | 
					      -- accept it so we should fix it here.
 | 
				
			||||||
      if max <= e.end_row then
 | 
					      if max <= e.end_row then
 | 
				
			||||||
        e.end_row = max - 1
 | 
					        e.end_row = max - 1
 | 
				
			||||||
        e.end_col = len
 | 
					        e.end_col = last_line_len
 | 
				
			||||||
 | 
					        has_eol_text_edit = true
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        -- If the replacement is over the end of a line (i.e. e.end_col is out of bounds and the
 | 
				
			||||||
 | 
					        -- replacement text ends with a newline We can likely assume that the replacement is assumed
 | 
				
			||||||
 | 
					        -- to be meant to replace the newline with another newline and we need to make sure this
 | 
				
			||||||
 | 
					        -- doens't add an extra empty line. E.g. when the last line to be replaced contains a '\r'
 | 
				
			||||||
 | 
					        -- in the file some servers (clangd on windows) will include that character in the line
 | 
				
			||||||
 | 
					        -- while nvim_buf_set_text doesn't count it as part of the line.
 | 
				
			||||||
 | 
					        if
 | 
				
			||||||
 | 
					          e.end_col > last_line_len
 | 
				
			||||||
 | 
					          and #text_edit.newText > 0
 | 
				
			||||||
 | 
					          and string.sub(text_edit.newText, -1) == '\n'
 | 
				
			||||||
 | 
					        then
 | 
				
			||||||
 | 
					          table.remove(e.text, #e.text)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
      has_eol_text_edit = true
 | 
					      -- Make sure we don't go out of bounds for e.end_col
 | 
				
			||||||
    end
 | 
					      e.end_col = math.min(last_line_len, e.end_col)
 | 
				
			||||||
    api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    -- Fix cursor position.
 | 
					      api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
 | 
				
			||||||
    local row_count = (e.end_row - e.start_row) + 1
 | 
					
 | 
				
			||||||
    if e.end_row < cursor.row then
 | 
					      -- Fix cursor position.
 | 
				
			||||||
      cursor.row = cursor.row + (#e.text - row_count)
 | 
					      local row_count = (e.end_row - e.start_row) + 1
 | 
				
			||||||
      is_cursor_fixed = true
 | 
					      if e.end_row < cursor.row then
 | 
				
			||||||
    elseif e.end_row == cursor.row and e.end_col <= cursor.col then
 | 
					        cursor.row = cursor.row + (#e.text - row_count)
 | 
				
			||||||
      cursor.row = cursor.row + (#e.text - row_count)
 | 
					        is_cursor_fixed = true
 | 
				
			||||||
      cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
 | 
					      elseif e.end_row == cursor.row and e.end_col <= cursor.col then
 | 
				
			||||||
      if #e.text == 1 then
 | 
					        cursor.row = cursor.row + (#e.text - row_count)
 | 
				
			||||||
        cursor.col = cursor.col + e.start_col
 | 
					        cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
 | 
				
			||||||
 | 
					        if #e.text == 1 then
 | 
				
			||||||
 | 
					          cursor.col = cursor.col + e.start_col
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					        is_cursor_fixed = true
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
      is_cursor_fixed = true
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1719,6 +1719,46 @@ describe('LSP', function()
 | 
				
			|||||||
    end)
 | 
					    end)
 | 
				
			||||||
  end)
 | 
					  end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('apply_text_edits regression tests for #20116', function()
 | 
				
			||||||
 | 
					    before_each(function()
 | 
				
			||||||
 | 
					      insert(dedent([[
 | 
				
			||||||
 | 
					      Test line one
 | 
				
			||||||
 | 
					      Test line two 21 char]]))
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    describe('with LSP end column out of bounds and start column at 0', function()
 | 
				
			||||||
 | 
					      it('applies edits at the end of the buffer', function()
 | 
				
			||||||
 | 
					        local edits = {
 | 
				
			||||||
 | 
					          make_edit(0, 0, 1, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8")
 | 
				
			||||||
 | 
					        eq({'#include "whatever.h"', '#include <algorithm>'}, buf_lines(1))
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					      it('applies edits in the middle of the buffer', function()
 | 
				
			||||||
 | 
					        local edits = {
 | 
				
			||||||
 | 
					          make_edit(0, 0, 0, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8")
 | 
				
			||||||
 | 
					        eq({'#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char'}, buf_lines(1))
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    describe('with LSP end column out of bounds and start column NOT at 0', function()
 | 
				
			||||||
 | 
					      it('applies edits at the end of the buffer', function()
 | 
				
			||||||
 | 
					        local edits = {
 | 
				
			||||||
 | 
					          make_edit(0, 2, 1, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8")
 | 
				
			||||||
 | 
					        eq({'Te#include "whatever.h"', '#include <algorithm>'}, buf_lines(1))
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					      it('applies edits in the middle of the buffer', function()
 | 
				
			||||||
 | 
					        local edits = {
 | 
				
			||||||
 | 
					          make_edit(0, 2, 0, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8")
 | 
				
			||||||
 | 
					        eq({'Te#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char'}, buf_lines(1))
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					  end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('apply_text_document_edit', function()
 | 
					  describe('apply_text_document_edit', function()
 | 
				
			||||||
    local target_bufnr
 | 
					    local target_bufnr
 | 
				
			||||||
    local text_document_edit = function(editVersion)
 | 
					    local text_document_edit = function(editVersion)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user