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

Eagerly compute SHA's for watched files #2861

Merged
merged 1 commit into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions vscode/src/test/suite/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ suite("Workspace", () => {

await workspace.activate();

for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 200));
for (let i = 0; i < 4; i++) {
await new Promise((resolve) => setTimeout(resolve, 50));
fs.writeFileSync(path.join(gitDir, "rebase-apply"), "1");
await new Promise((resolve) => setTimeout(resolve, 200));
await new Promise((resolve) => setTimeout(resolve, 50));
fs.rmSync(path.join(gitDir, "rebase-apply"));
}

// Give enough time for all watchers to fire and all debounces to run off
await new Promise((resolve) => setTimeout(resolve, 10000));
await new Promise((resolve) => setTimeout(resolve, 6000));

startStub.restore();
restartSpy.restore();
Expand All @@ -77,4 +77,40 @@ suite("Workspace", () => {
// The restart call only happens once because of the debounce
assert.strictEqual(restartSpy.callCount, 1);
}).timeout(60000);

test("does not restart when watched files are touched without modifying contents", async () => {
const lockfileUri = vscode.Uri.file(
path.join(workspacePath, "Gemfile.lock"),
);
const contents = Buffer.from("hello");
await vscode.workspace.fs.writeFile(lockfileUri, contents);

const workspace = new Workspace(
context,
workspaceFolder,
FAKE_TELEMETRY,
() => {},
new Map(),
);

await workspace.activate();

const startStub = sinon.stub(workspace, "start");
const restartSpy = sinon.spy(workspace, "restart");

for (let i = 0; i < 4; i++) {
const updateDate = new Date();
fs.utimesSync(lockfileUri.fsPath, updateDate, updateDate);
await new Promise((resolve) => setTimeout(resolve, 50));
}

// Give enough time for all watchers to fire and all debounces to run off
await new Promise((resolve) => setTimeout(resolve, 6000));

startStub.restore();
restartSpy.restore();

assert.strictEqual(startStub.callCount, 0);
assert.strictEqual(restartSpy.callCount, 0);
}).timeout(10000);
});
95 changes: 56 additions & 39 deletions vscode/src/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import {
} from "./common";
import { WorkspaceChannel } from "./workspaceChannel";

const WATCHED_FILES = [
"Gemfile.lock",
"gems.locked",
".rubocop.yml",
vinistock marked this conversation as resolved.
Show resolved Hide resolved
".rubocop",
];

export class Workspace implements WorkspaceInterface {
public lspClient?: Client;
public readonly ruby: Ruby;
Expand Down Expand Up @@ -58,8 +65,6 @@ export class Workspace implements WorkspaceInterface {
this.createTestItems = createTestItems;
this.isMainWorkspace = isMainWorkspace;
this.virtualDocuments = virtualDocuments;

this.registerRestarts(context);
}

// Activate this workspace. This method is intended to be invoked only once, unlikely `start` which may be invoked
Expand All @@ -82,6 +87,20 @@ export class Workspace implements WorkspaceInterface {
rootGitUri,
".git/{rebase-merge,rebase-apply,BISECT_START,CHERRY_PICK_HEAD}",
);

this.registerRestarts();

// Eagerly calculate SHAs for the watched files to avoid unnecessary restarts
for (const file of WATCHED_FILES) {
const uri = vscode.Uri.joinPath(this.workspaceFolder.uri, file);
const currentSha = await this.fileContentsSha(uri);

if (!currentSha) {
continue;
}

this.restartDocumentShas.set(uri.fsPath, currentSha);
}
}

async start(debugMode?: boolean) {
Expand Down Expand Up @@ -164,7 +183,9 @@ export class Workspace implements WorkspaceInterface {
// server can now handle shutdown requests
if (this.needsRestart) {
this.needsRestart = false;
await this.restart();
await this.debouncedRestart(
"a restart was requested while the server was still booting",
);
vinistock marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (error: any) {
this.error = true;
Expand Down Expand Up @@ -342,67 +363,49 @@ export class Workspace implements WorkspaceInterface {
return result;
}

private registerRestarts(context: vscode.ExtensionContext) {
this.createRestartWatcher(context, "Gemfile.lock");
this.createRestartWatcher(context, "gems.locked");
this.createRestartWatcher(context, "**/.rubocop.yml");
this.createRestartWatcher(context, ".rubocop");
private registerRestarts() {
this.createRestartWatcher(`{${WATCHED_FILES.join(",")}}`);

// If a configuration that affects the Ruby LSP has changed, update the client options using the latest
// configuration and restart the server
context.subscriptions.push(
this.context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(async (event) => {
if (event.affectsConfiguration("rubyLsp")) {
// Re-activate Ruby if the version manager changed
if (
event.affectsConfiguration("rubyLsp.rubyVersionManager") ||
event.affectsConfiguration("rubyLsp.bundleGemfile") ||
event.affectsConfiguration("rubyLsp.customRubyCommand")
) {
await this.ruby.activateRuby();
}
vinistock marked this conversation as resolved.
Show resolved Hide resolved

this.outputChannel.info(
"Restarting the Ruby LSP because configuration changed",
);
await this.restart();
await this.debouncedRestart("configuration changed");
vinistock marked this conversation as resolved.
Show resolved Hide resolved
}
}),
);
}

private createRestartWatcher(
context: vscode.ExtensionContext,
pattern: string,
) {
private createRestartWatcher(pattern: string) {
const watcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(this.workspaceFolder, pattern),
);

// Handler for only triggering restart if the contents of the file have been modified. If the file was just touched,
// but the contents are the same, we don't want to restart
const debouncedRestartWithHashCheck = async (uri: vscode.Uri) => {
const fileContents = await vscode.workspace.fs.readFile(uri);
const fsPath = uri.fsPath;
const currentSha = await this.fileContentsSha(uri);

const hash = createHash("sha256");
hash.update(fileContents.toString());
const currentSha = hash.digest("hex");

if (this.restartDocumentShas.get(fsPath) !== currentSha) {
if (currentSha && this.restartDocumentShas.get(fsPath) !== currentSha) {
this.restartDocumentShas.set(fsPath, currentSha);
await this.debouncedRestart(`${pattern} changed`);
await this.debouncedRestart(`${fsPath} changed, matching ${pattern}`);
}
};

const debouncedRestart = async () => {
await this.debouncedRestart(`${pattern} changed`);
const debouncedRestart = async (uri: vscode.Uri) => {
this.restartDocumentShas.delete(uri.fsPath);
await this.debouncedRestart(`${uri.fsPath} changed, matching ${pattern}`);
};
vinistock marked this conversation as resolved.
Show resolved Hide resolved

context.subscriptions.push(
this.context.subscriptions.push(
watcher,
watcher.onDidChange(debouncedRestartWithHashCheck),
watcher.onDidCreate(debouncedRestart),
// Interestingly, we are seeing create events being fired even when the file already exists. If a create event is
// fired for an update to an existing file, then we need to check if the contents still match to prevent unwanted
// restarts
watcher.onDidCreate(debouncedRestartWithHashCheck),
watcher.onDidDelete(debouncedRestart),
vinistock marked this conversation as resolved.
Show resolved Hide resolved
);
}
Expand All @@ -416,9 +419,9 @@ export class Workspace implements WorkspaceInterface {
this.#inhibitRestart = true;
};

const stop = async () => {
const stop = async (uri: vscode.Uri) => {
this.#inhibitRestart = false;
await this.debouncedRestart(`${glob} changed`);
await this.debouncedRestart(`${uri.fsPath} changed, matching ${glob}`);
};

this.context.subscriptions.push(
Expand All @@ -429,4 +432,18 @@ export class Workspace implements WorkspaceInterface {
workspaceWatcher.onDidDelete(stop),
);
}

private async fileContentsSha(uri: vscode.Uri): Promise<string | undefined> {
let fileContents;

try {
fileContents = await vscode.workspace.fs.readFile(uri);
} catch (error: any) {
vinistock marked this conversation as resolved.
Show resolved Hide resolved
return undefined;
}

const hash = createHash("sha256");
hash.update(fileContents.toString());
return hash.digest("hex");
}
}
Loading