Skip to content

Commit e246b60

Browse files
Copilotedvilme
andauthored
Restart server on config file change (#631)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
1 parent 534a438 commit e246b60

File tree

4 files changed

+176
-2
lines changed

4 files changed

+176
-2
lines changed

src/common/configWatcher.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Disposable, workspace } from 'vscode';
5+
import { BLACK_CONFIG_FILES } from './constants';
6+
import { traceLog } from './logging';
7+
8+
export function createConfigFileWatchers(onConfigChanged: () => Promise<void>): Disposable[] {
9+
return BLACK_CONFIG_FILES.map((pattern) => {
10+
const watcher = workspace.createFileSystemWatcher(`**/${pattern}`);
11+
const changeDisposable = watcher.onDidChange(async () => {
12+
traceLog(`Black config file changed: ${pattern}`);
13+
await onConfigChanged();
14+
});
15+
const createDisposable = watcher.onDidCreate(async () => {
16+
traceLog(`Black config file created: ${pattern}`);
17+
await onConfigChanged();
18+
});
19+
const deleteDisposable = watcher.onDidDelete(async () => {
20+
traceLog(`Black config file deleted: ${pattern}`);
21+
await onConfigChanged();
22+
});
23+
return Disposable.from(watcher, changeDisposable, createDisposable, deleteDisposable);
24+
});
25+
}

src/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export const PYTHON_MAJOR = 3;
1414
export const PYTHON_MINOR = 10;
1515
export const PYTHON_VERSION = `${PYTHON_MAJOR}.${PYTHON_MINOR}`;
1616
export const LS_SERVER_RESTART_DELAY = 1000;
17+
export const BLACK_CONFIG_FILES = ['pyproject.toml', '.black', 'setup.cfg', 'tox.ini'];

src/extension.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
import * as vscode from 'vscode';
55
import { LanguageClient } from 'vscode-languageclient/node';
6-
import { restartServer } from './common/server';
6+
import { createConfigFileWatchers } from './common/configWatcher';
7+
import { LS_SERVER_RESTART_DELAY, PYTHON_VERSION } from './common/constants';
78
import { registerLogger, traceError, traceLog, traceVerbose } from './common/logging';
89
import { initializePython, onDidChangePythonInterpreter } from './common/python';
10+
import { restartServer } from './common/server';
911
import {
1012
checkIfConfigurationChanged,
1113
getWorkspaceSettings,
@@ -17,7 +19,6 @@ import { getInterpreterFromSetting, getProjectRoot } from './common/utilities';
1719
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from './common/vscodeapi';
1820
import { registerEmptyFormatter } from './common/nullFormatter';
1921
import { registerLanguageStatusItem, updateStatus } from './common/status';
20-
import { LS_SERVER_RESTART_DELAY, PYTHON_VERSION } from './common/constants';
2122

2223
let lsClient: LanguageClient | undefined;
2324
export async function activate(context: vscode.ExtensionContext): Promise<void> {
@@ -66,6 +67,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
6667
};
6768

6869
context.subscriptions.push(
70+
// Create file watchers for Black configuration files
71+
...createConfigFileWatchers(runServer),
6972
onDidChangePythonInterpreter(async () => {
7073
await runServer();
7174
}),
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { assert } from 'chai';
5+
import * as sinon from 'sinon';
6+
import { Disposable, FileSystemWatcher, workspace } from 'vscode';
7+
import { createConfigFileWatchers } from '../../../../common/configWatcher';
8+
import { BLACK_CONFIG_FILES } from '../../../../common/constants';
9+
10+
interface MockFileSystemWatcher {
11+
watcher: FileSystemWatcher;
12+
fireDidCreate(): Promise<void>;
13+
fireDidChange(): Promise<void>;
14+
fireDidDelete(): Promise<void>;
15+
}
16+
17+
function createMockFileSystemWatcher(): MockFileSystemWatcher {
18+
let onDidChangeHandler: (() => Promise<void>) | undefined;
19+
let onDidCreateHandler: (() => Promise<void>) | undefined;
20+
let onDidDeleteHandler: (() => Promise<void>) | undefined;
21+
22+
const watcher = {
23+
onDidChange: (handler: () => Promise<void>): Disposable => {
24+
onDidChangeHandler = handler;
25+
return { dispose: () => {} };
26+
},
27+
onDidCreate: (handler: () => Promise<void>): Disposable => {
28+
onDidCreateHandler = handler;
29+
return { dispose: () => {} };
30+
},
31+
onDidDelete: (handler: () => Promise<void>): Disposable => {
32+
onDidDeleteHandler = handler;
33+
return { dispose: () => {} };
34+
},
35+
dispose: () => {},
36+
} as unknown as FileSystemWatcher;
37+
38+
return {
39+
watcher,
40+
fireDidCreate: async () => {
41+
if (onDidCreateHandler) {
42+
await onDidCreateHandler();
43+
}
44+
},
45+
fireDidChange: async () => {
46+
if (onDidChangeHandler) {
47+
await onDidChangeHandler();
48+
}
49+
},
50+
fireDidDelete: async () => {
51+
if (onDidDeleteHandler) {
52+
await onDidDeleteHandler();
53+
}
54+
},
55+
};
56+
}
57+
58+
suite('Config File Watcher Tests', () => {
59+
let sandbox: sinon.SinonSandbox;
60+
let createFileSystemWatcherStub: sinon.SinonStub;
61+
let mockWatchers: MockFileSystemWatcher[];
62+
63+
setup(() => {
64+
sandbox = sinon.createSandbox();
65+
mockWatchers = BLACK_CONFIG_FILES.map(() => createMockFileSystemWatcher());
66+
67+
let watcherIndex = 0;
68+
createFileSystemWatcherStub = sandbox.stub(workspace, 'createFileSystemWatcher').callsFake(() => {
69+
return mockWatchers[watcherIndex++].watcher;
70+
});
71+
});
72+
73+
teardown(() => {
74+
sandbox.restore();
75+
});
76+
77+
test('Creates a file watcher for each Black config file pattern', () => {
78+
const onConfigChanged = sandbox.stub().resolves();
79+
createConfigFileWatchers(onConfigChanged);
80+
81+
assert.strictEqual(createFileSystemWatcherStub.callCount, BLACK_CONFIG_FILES.length);
82+
for (let i = 0; i < BLACK_CONFIG_FILES.length; i++) {
83+
assert.isTrue(
84+
createFileSystemWatcherStub.getCall(i).calledWith(`**/${BLACK_CONFIG_FILES[i]}`),
85+
`Expected watcher for pattern **/${BLACK_CONFIG_FILES[i]}`,
86+
);
87+
}
88+
});
89+
90+
test('Server restarts when a config file is created', async () => {
91+
const onConfigChanged = sandbox.stub().resolves();
92+
createConfigFileWatchers(onConfigChanged);
93+
94+
// Simulate creating a pyproject.toml file
95+
await mockWatchers[0].fireDidCreate();
96+
97+
assert.isTrue(onConfigChanged.calledOnce, 'Expected onConfigChanged to be called when config file is created');
98+
});
99+
100+
test('Server restarts when a config file is changed', async () => {
101+
const onConfigChanged = sandbox.stub().resolves();
102+
createConfigFileWatchers(onConfigChanged);
103+
104+
// Simulate modifying .black
105+
await mockWatchers[1].fireDidChange();
106+
107+
assert.isTrue(onConfigChanged.calledOnce, 'Expected onConfigChanged to be called when config file is changed');
108+
});
109+
110+
test('Server restarts when a config file is deleted', async () => {
111+
const onConfigChanged = sandbox.stub().resolves();
112+
createConfigFileWatchers(onConfigChanged);
113+
114+
// Simulate deleting setup.cfg
115+
await mockWatchers[2].fireDidDelete();
116+
117+
assert.isTrue(onConfigChanged.calledOnce, 'Expected onConfigChanged to be called when config file is deleted');
118+
});
119+
120+
test('Server restarts for each config file type on create', async () => {
121+
const onConfigChanged = sandbox.stub().resolves();
122+
createConfigFileWatchers(onConfigChanged);
123+
124+
// Fire onDidCreate for every config file pattern
125+
for (const mock of mockWatchers) {
126+
await mock.fireDidCreate();
127+
}
128+
129+
assert.strictEqual(
130+
onConfigChanged.callCount,
131+
BLACK_CONFIG_FILES.length,
132+
`Expected onConfigChanged to be called once for each of the ${BLACK_CONFIG_FILES.length} config file patterns`,
133+
);
134+
});
135+
136+
test('Returns a disposable for each watcher', () => {
137+
const onConfigChanged = sandbox.stub().resolves();
138+
const disposables = createConfigFileWatchers(onConfigChanged);
139+
140+
assert.strictEqual(disposables.length, BLACK_CONFIG_FILES.length);
141+
for (const d of disposables) {
142+
assert.isFunction(d.dispose);
143+
}
144+
});
145+
});

0 commit comments

Comments
 (0)