You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The didChange() method in FileCache.java throws a StringIndexOutOfBoundsException when processing multiple incremental character insertions sent as separate changes in a single event.
Steps to reproduce
Open an empty file in an LSP client that sends didChange events in a piecemeal fashion.
Type characters one by one (e.g., typing "proc")
Or alternatively, using the following python script:
#!/usr/bin/env python3
#### Please replace with the appropriate path to the LSP
LSP_PATH='/Users/ycc/.local/share/nvim/mason/bin/nextflow-language-server'
#### The content of my LSP executable (from `mason.nvim`)
# #!/usr/bin/env bash
#
# exec java -jar "$HOME/.local/share/nvim/mason/packages/nextflow-language-server/language-server-all.jar" "$@"
import json
import subprocess
import time
# Minimal JSON messages to reproduce the bug
messages = [
# Initialize request
{
"id": 1,
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"processId": None,
"rootUri": "file:///tmp/testlsp",
"capabilities": {},
"workspaceFolders": [{
"name": "testlsp",
"uri": "file:///tmp/testlsp"
}]
}
},
# Initialized notification
{
"jsonrpc": "2.0",
"method": "initialized",
"params": {}
},
# Open file
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///tmp/testlsp/test.nf",
"languageId": "nextflow",
"version": 0,
"text": "\n"
}
}
},
# First didChange - add 'p'
{
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file:///tmp/testlsp/test.nf",
"version": 1
},
"contentChanges": [{
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 0}
},
"rangeLength": 0,
"text": "p"
}]
}
},
# Second didChange - add 'r'
{
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file:///tmp/testlsp/test.nf",
"version": 2
},
"contentChanges": [{
"range": {
"start": {"line": 0, "character": 1},
"end": {"line": 0, "character": 1}
},
"rangeLength": 0,
"text": "r"
}]
}
},
# Delete 'r'
{
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file:///tmp/testlsp/test.nf",
"version": 3
},
"contentChanges": [{
"range": {
"start": {"line": 0, "character": 1},
"end": {"line": 0, "character": 2}
},
"rangeLength": 1,
"text": ""
}]
}
},
# Bug-triggering message - multiple changes in single request
{
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file:///tmp/testlsp/test.nf",
"version": 4
},
"contentChanges": [
{
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
},
"rangeLength": 1,
"text": ""
},
{
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 0}
},
"rangeLength": 0,
"text": "p"
},
{
"range": {
"start": {"line": 0, "character": 1},
"end": {"line": 0, "character": 1}
},
"rangeLength": 0,
"text": "r"
}
]
}
}
]
def send_message(proc, message):
"""Send a JSON-RPC message to the LSP server"""
content = json.dumps(message)
header = f"Content-Length: {len(content.encode('utf-8'))}\r\n\r\n"
proc.stdin.write(header.encode('utf-8'))
proc.stdin.write(content.encode('utf-8'))
proc.stdin.flush()
print(f"Sent: {json.dumps(message, indent=2)}")
def read_response(proc, timeout=0.5):
"""Read response from LSP server with timeout"""
import select
try:
# Use select to check if data is available
if not select.select([proc.stdout], [], [], timeout)[0]:
return None
# Read headers
headers = {}
while True:
line = proc.stdout.readline().decode('utf-8').strip()
if not line:
break
if ':' in line:
key, value = line.split(':', 1)
headers[key.strip()] = value.strip()
# Read content
if 'Content-Length' in headers:
content_length = int(headers['Content-Length'])
content = proc.stdout.read(content_length).decode('utf-8')
return json.loads(content)
except Exception as e:
print(f"Error reading response: {e}")
return None
def check_stderr(proc, nonblocking=True):
"""Check for errors in stderr"""
import select
if nonblocking:
if select.select([proc.stderr], [], [], 0)[0]:
stderr = proc.stderr.read1().decode('utf-8')
if stderr:
print(f"\nServer error: {stderr}")
if "StringIndexOutOfBoundsException" in stderr:
print("\n*** BUG REPRODUCED! ***")
print("The server crashed with StringIndexOutOfBoundsException")
print("This happens when multiple content changes are sent in a single didChange request")
print("where the first change removes content that subsequent changes try to modify.")
return True
return False
def reproduce_bug():
"""Reproduce the LSP bug by sending the minimal message sequence"""
print("Starting Nextflow Language Server...")
# Start the LSP server
proc = subprocess.Popen(
[LSP_PATH],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=0 # Unbuffered
)
try:
# Send messages to reproduce the bug
for i, message in enumerate(messages):
send_message(proc, message)
time.sleep(0.1) # Give server time to process
# For requests, wait for response
if 'id' in message:
response = read_response(proc)
if response:
print(f"Response: {json.dumps(response, indent=2)}")
else:
# For notifications, just check for any server messages
response = read_response(proc, timeout=0.1)
if response:
print(f"Server message: {json.dumps(response, indent=2)}")
# Check for errors in stderr
if check_stderr(proc):
break
# Also check if server has crashed
if proc.poll() is not None:
print("Server process terminated unexpectedly")
break
except Exception as e:
print(f"Error: {e}")
finally:
# Clean up
proc.terminate()
proc.wait()
# Read any remaining stderr
remaining_stderr = proc.stderr.read().decode('utf-8')
if remaining_stderr:
print(f"Remaining server errors: {remaining_stderr}")
if "StringIndexOutOfBoundsException" in remaining_stderr:
print("\n*** BUG REPRODUCED! ***")
if __name__ == "__main__":
reproduce_bug()
Error
# If with the python script above
java.lang.StringIndexOutOfBoundsException: Range [1, 0) out of bounds for length 2
at java.base/java.lang.String.substring(String.java:2925)
at nextflow.lsp.file.FileCache.didChange(FileCache.java:105)
Possible cause
The current implementation assumes change positions remain valid relative to the original text throughout processing, but this breaks when changes modify the text length (like single character insertions).
Note
This doesn't affect VSCode (maybe it aggregates changes?), but neovim (v0.11) using mason-provided LSP (v24.10.3) and mason-lspconfig or simulating LSP sessions could reproducibly trigger the error.
I will submit a pull request that seems not to trigger this error with the test script or my regular usage.
Thank you so much for all the awesome tool development. Please let me know if you have any questions.
The text was updated successfully, but these errors were encountered:
Description
The
didChange()
method inFileCache.java
throws aStringIndexOutOfBoundsException
when processing multiple incremental character insertions sent as separate changes in a single event.Steps to reproduce
didChange
events in a piecemeal fashion.Or alternatively, using the following python script:
Error
Possible cause
The current implementation assumes change positions remain valid relative to the original text throughout processing, but this breaks when changes modify the text length (like single character insertions).
Note
This doesn't affect VSCode (maybe it aggregates changes?), but neovim (v0.11) using
mason
-provided LSP (v24.10.3) andmason-lspconfig
or simulating LSP sessions could reproducibly trigger the error.I will submit a pull request that seems not to trigger this error with the test script or my regular usage.
Thank you so much for all the awesome tool development. Please let me know if you have any questions.
The text was updated successfully, but these errors were encountered: