Description
The Ruby LSP experiences unnecessary restarts when monitoring files for changes. This occurs because the file system watcher sometimes emits URIs with a git:
scheme instead of the expected file:
scheme, causing the extension to incorrectly calculate file content hashes and trigger restarts even when actual file contents haven't changed.
Problem
In extension/src/workspace.ts
, we monitor files like Gemfile.lock
for changes that would require restarting the language server. When these files change, debouncedRestartWithHashCheck
calculates a hash of the file contents using the fileContentsSha
function to determine if an actual content change occurred.
The current implementation:
const debouncedRestartWithHashCheck = async (uri: vscode.Uri) => {
const fsPath = uri.fsPath;
const currentSha = await this.fileContentsSha(uri);
const storedSha = this.restartDocumentShas.get(fsPath);
if (currentSha && storedSha !== currentSha) {
this.restartDocumentShas.set(fsPath, currentSha);
await this.debouncedRestart(`${fsPath} changed, matching ${pattern}`);
}
};
The issue occurs when the file system watcher emits URIs with a git:
scheme instead of the expected file:
scheme:
git:/workspace/project/Gemfile.lock?%7B%22path%22%3A%22%2Fworkspace%2Fproject%2FGemfile.lock%22%2C%22ref%22%3A%22~%22%7D
When a git:
URI is passed to fileContentsSha
, vscode.workspace.fs.readFile(uri)
resolves content based on a Git reference (ref: "~"
) rather than the file on disk. This leads to calculating a SHA hash for a Git-managed version of the file, not the current version on disk.
Consequences
This leads to unintended restarts when Git-referenced content differs from disk content.
Potential Solution
We are able to prevent the restart loop behaviour by converting git:
URIs to file:
URIs before hashing:
const debouncedRestartWithHashCheck = async (uri: vscode.Uri) => {
const fsPath = uri.fsPath;
// Convert git: URI scheme to file: before calculating the SHA
const fileUri = uri.scheme === "git" ? vscode.Uri.file(fsPath) : uri;
const currentSha = await this.fileContentsSha(fileUri);
const storedSha = this.restartDocumentShas.get(fsPath);
if (currentSha && storedSha !== currentSha) {
this.restartDocumentShas.set(fsPath, currentSha);
await this.debouncedRestart(`${fsPath} changed, matching ${pattern}`);
}
};
This ensures we hash the actual file content regardless of the URI scheme provided.
Further Investigation Needed
While this approach addresses the immediate symptom, I don't fully understand when or why the file system watcher emits URIs with a git:
scheme. I suspect the answer lies in how we're eagerly computing SHA's for watched files.
Further investigation is needed.
Reproduction
I have not been able to reproduce this issue consistently. I identified the problem when encountering a restart loop that produced this message in the logs:
Restarting the Ruby LSP because /workspace/project/Gemfile.lock changed, matching {Gemfile.lock,gems.locked}
These findings are based on debugging the restart loop. Notably, while no changes were being made to Gemfile.lock at the time the restart loop began, Gemfile.lock
did have uncommitted changes that had been made earlier.