Skip to content

Commit b60d9b6

Browse files
committed
feat(lsp): support configuration changes
The LSP server loads the quick-lint-js.tracing-directory configuration option on startup. Allow the user to change this option any time after startup.
1 parent b2db4bb commit b60d9b6

File tree

7 files changed

+330
-23
lines changed

7 files changed

+330
-23
lines changed

plugin/vscode-lsp/extension.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ async function startOrRestartServerAsync() {
2727
{ language: "javascriptreact" },
2828
{ language: "json" },
2929
],
30+
synchronize: {
31+
configurationSection: "quick-lint-js-lsp",
32+
},
3033
middleware: {
3134
workspace: {
3235
async configuration(params, _token, _next) {
@@ -35,6 +38,11 @@ async function startOrRestartServerAsync() {
3538
getLSPWorkspaceConfig(vscodeConfig, item)
3639
);
3740
},
41+
async didChangeConfiguration(sections) {
42+
return client.sendNotification("workspace/didChangeConfiguration", {
43+
settings: getLSPWorkspaceFullConfig(),
44+
});
45+
},
3846
},
3947
},
4048
}
@@ -57,6 +65,13 @@ async function startOrRestartServerAsync() {
5765
return null;
5866
}
5967
}
68+
69+
function getLSPWorkspaceFullConfig() {
70+
let config = getLatestWorkspaceConfig();
71+
return {
72+
"quick-lint-js.tracing-directory": config.get("tracing-directory"),
73+
};
74+
}
6075
}
6176

6277
async function stopServerIfStartedAsync() {

src/lsp-server.cpp

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,18 @@ linting_lsp_server_handler::linting_lsp_server_handler(
112112
this->workspace_configuration_.add_item(
113113
u8"quick-lint-js.tracing-directory"sv,
114114
[this](std::string_view new_value) {
115-
this->server_config_.tracing_directory = new_value;
115+
bool changed = this->server_config_.tracing_directory != new_value;
116+
if (changed) {
117+
this->server_config_.tracing_directory = new_value;
118+
if (this->tracer_) {
119+
if (this->server_config_.tracing_directory.empty()) {
120+
this->tracer_->disable();
121+
} else {
122+
this->tracer_->create_and_enable_in_child_directory(
123+
this->server_config_.tracing_directory);
124+
}
125+
}
126+
}
116127
});
117128
}
118129

@@ -160,6 +171,8 @@ void linting_lsp_server_handler::handle_notification(
160171
this->handle_text_document_did_open_notification(request);
161172
} else if (method == "textDocument/didClose") {
162173
this->handle_text_document_did_close_notification(request);
174+
} else if (method == "workspace/didChangeConfiguration") {
175+
this->handle_workspace_did_change_configuration_notification(request);
163176
} else if (method == "initialized") {
164177
this->handle_initialized_notification();
165178
} else if (method == "exit") {
@@ -239,11 +252,6 @@ void linting_lsp_server_handler::handle_workspace_configuration_response(
239252
// TODO(strager): Report an error.
240253
return;
241254
}
242-
243-
if (this->tracer_ && !this->server_config_.tracing_directory.empty()) {
244-
this->tracer_->create_and_enable_in_child_directory(
245-
this->server_config_.tracing_directory);
246-
}
247255
}
248256

249257
void linting_lsp_server_handler::handle_initialized_notification() {
@@ -435,6 +443,24 @@ void linting_lsp_server_handler::handle_text_document_did_open_notification(
435443
}
436444
}
437445

446+
void linting_lsp_server_handler::
447+
handle_workspace_did_change_configuration_notification(
448+
::simdjson::ondemand::object& request) {
449+
::simdjson::ondemand::object settings;
450+
if (request["params"]["settings"].get(settings) != ::simdjson::SUCCESS) {
451+
QLJS_DEBUG_LOG("failed to extract configuration notification settings\n");
452+
// TODO(strager): Report an error.
453+
return;
454+
}
455+
456+
bool ok = this->workspace_configuration_.process_notification(settings);
457+
if (!ok) {
458+
QLJS_DEBUG_LOG("failed to process configuration notification\n");
459+
// TODO(strager): Report an error.
460+
return;
461+
}
462+
}
463+
438464
void linting_lsp_server_handler::handle_config_file_changes(
439465
const std::vector<configuration_change>& config_changes) {
440466
for (auto& entry : this->documents_) {

src/lsp-workspace-configuration.cpp

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,70 @@ bool lsp_workspace_configuration::process_response(
7272
if ((*result_it).get(value) != ::simdjson::SUCCESS) {
7373
return false;
7474
}
75+
if (!this->set_item(*spec_it, value)) {
76+
return false;
77+
}
78+
}
79+
return spec_it == spec_end && result_it == result_end;
80+
}
7581

76-
::simdjson::ondemand::json_type type;
77-
if (value.type().get(type) != ::simdjson::SUCCESS) {
82+
bool lsp_workspace_configuration::process_notification(
83+
::simdjson::ondemand::object settings) {
84+
for (simdjson::simdjson_result< ::simdjson::ondemand::field> setting_field :
85+
settings) {
86+
std::string_view name;
87+
if (setting_field.unescaped_key().get(name) != ::simdjson::SUCCESS) {
7888
return false;
7989
}
80-
switch (type) {
81-
case ::simdjson::ondemand::json_type::string: {
82-
std::string_view string_value;
83-
if (value.get(string_value) != ::simdjson::SUCCESS) {
84-
return false;
85-
}
86-
spec_it->callback(string_value);
87-
break;
90+
::simdjson::ondemand::value value;
91+
if (setting_field.value().get(value) != ::simdjson::SUCCESS) {
92+
return false;
93+
}
94+
item* i = this->find_item(to_string8_view(name));
95+
if (!i) {
96+
// Ignore unknown settings.
97+
continue;
8898
}
99+
if (!this->set_item(*i, value)) {
100+
return false;
101+
}
102+
}
103+
return true;
104+
}
89105

90-
case ::simdjson::ondemand::json_type::null:
91-
spec_it->callback(std::string_view());
92-
break;
106+
lsp_workspace_configuration::item* lsp_workspace_configuration::find_item(
107+
string8_view name) {
108+
for (item& i : this->items_) {
109+
if (i.name == name) {
110+
return &i;
111+
}
112+
}
113+
return nullptr;
114+
}
93115

94-
default:
116+
bool lsp_workspace_configuration::set_item(const item& i,
117+
::simdjson::ondemand::value value) {
118+
::simdjson::ondemand::json_type type;
119+
if (value.type().get(type) != ::simdjson::SUCCESS) {
120+
return false;
121+
}
122+
switch (type) {
123+
case ::simdjson::ondemand::json_type::string: {
124+
std::string_view string_value;
125+
if (value.get(string_value) != ::simdjson::SUCCESS) {
95126
return false;
96127
}
128+
i.callback(string_value);
129+
return true;
130+
}
131+
132+
case ::simdjson::ondemand::json_type::null:
133+
i.callback(std::string_view());
134+
return true;
135+
136+
default:
137+
return false;
97138
}
98-
return spec_it == spec_end && result_it == result_end;
99139
}
100140
}
101141

src/quick-lint-js/lsp-server.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ class linting_lsp_server_handler final : public lsp_endpoint_handler {
147147
::simdjson::ondemand::object& request);
148148
void handle_text_document_did_open_notification(
149149
::simdjson::ondemand::object& request);
150+
void handle_workspace_did_change_configuration_notification(
151+
::simdjson::ondemand::object& request);
150152

151153
void handle_config_file_changes(
152154
const std::vector<configuration_change>& config_changes);

src/quick-lint-js/lsp-workspace-configuration.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class lsp_workspace_configuration {
2424
public:
2525
// Register a configuration setting.
2626
//
27-
// callback is later called by process_response.
27+
// callback is later called by process_response or process_notification.
2828
//
2929
// name must be have global lifetime (e.g. be a compile-time string).
3030
// name must be a JSON-encoded string (without surrounding quotation marks).
@@ -39,11 +39,19 @@ class lsp_workspace_configuration {
3939
// Handle a workspace/configuration JSON-RPC response sent by the LSP client.
4040
bool process_response(::simdjson::ondemand::value result);
4141

42+
// Handle a workspace/didChangeConfiguration JSON-RPC notification sent by the
43+
// LSP client.
44+
bool process_notification(::simdjson::ondemand::object settings);
45+
4246
private:
4347
struct item {
4448
string8_view name;
4549
std::function<void(std::string_view)> callback;
4650
};
51+
52+
item* find_item(string8_view name);
53+
bool set_item(const item&, ::simdjson::ondemand::value);
54+
4755
std::vector<item> items_;
4856
};
4957
}

test/test-lsp-server.cpp

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
#include <quick-lint-js/configuration.h>
1919
#include <quick-lint-js/fake-configuration-filesystem.h>
2020
#include <quick-lint-js/file-handle.h>
21+
#include <quick-lint-js/filesystem-test.h>
2122
#include <quick-lint-js/lsp-endpoint.h>
2223
#include <quick-lint-js/lsp-server.h>
2324
#include <quick-lint-js/padded-string.h>
2425
#include <quick-lint-js/spy-lsp-endpoint-remote.h>
26+
#include <quick-lint-js/trace-flusher.h>
2527
#include <quick-lint-js/version.h>
2628
#include <quick-lint-js/warning.h>
2729
#include <simdjson.h>
@@ -81,7 +83,7 @@ class mock_lsp_linter final : public lsp_linter {
8183
std::function<lint_and_get_diagnostics_notification_type> callback_;
8284
};
8385

84-
class test_linting_lsp_server : public ::testing::Test {
86+
class test_linting_lsp_server : public ::testing::Test, public filesystem_test {
8587
public:
8688
explicit test_linting_lsp_server() { this->reset(); }
8789

@@ -288,6 +290,149 @@ TEST_F(test_linting_lsp_server, stores_config_values_after_config_response) {
288290
EXPECT_EQ(config.tracing_directory, "/test/tracing/dir");
289291
}
290292

293+
TEST_F(test_linting_lsp_server, did_change_configuration_notification) {
294+
this->server->append(
295+
make_message(u8R"({
296+
"jsonrpc": "2.0",
297+
"method": "workspace/didChangeConfiguration",
298+
"params": {
299+
"settings": {
300+
"quick-lint-js.tracing-directory": "/test/tracing/dir"
301+
}
302+
}
303+
})"));
304+
this->handler->flush_pending_notifications(*this->client);
305+
306+
EXPECT_THAT(this->client->messages, IsEmpty());
307+
linting_lsp_server_config& config = this->handler->server_config();
308+
EXPECT_EQ(config.tracing_directory, "/test/tracing/dir");
309+
}
310+
311+
TEST_F(test_linting_lsp_server,
312+
changing_config_to_same_tracing_dir_does_not_reset_tracing) {
313+
std::string temp_dir = this->make_temporary_directory();
314+
trace_flusher tracer;
315+
fake_configuration_filesystem fs;
316+
mock_lsp_linter linter;
317+
linting_lsp_server_handler handler(&fs, &linter, &tracer);
318+
spy_lsp_endpoint_remote client;
319+
lsp_endpoint server(&handler, &client);
320+
321+
server.append(
322+
make_message(u8R"({
323+
"jsonrpc": "2.0",
324+
"method": "workspace/didChangeConfiguration",
325+
"params": {
326+
"settings": {
327+
"quick-lint-js.tracing-directory": )" +
328+
json_to_string8(::boost::json::string(temp_dir)) + u8R"(
329+
}
330+
}
331+
})"));
332+
333+
std::vector<std::string> original_children =
334+
list_files_in_directory(temp_dir);
335+
EXPECT_THAT(original_children, ElementsAre(::testing::_))
336+
<< "enabling tracing in " << temp_dir
337+
<< " should create a trace subdirectory";
338+
339+
server.append(
340+
make_message(u8R"({
341+
"jsonrpc": "2.0",
342+
"method": "workspace/didChangeConfiguration",
343+
"params": {
344+
"settings": {
345+
"quick-lint-js.tracing-directory": )" +
346+
json_to_string8(::boost::json::string(temp_dir)) + u8R"(
347+
}
348+
}
349+
})"));
350+
351+
std::vector<std::string> new_children = list_files_in_directory(temp_dir);
352+
EXPECT_THAT(new_children, original_children)
353+
<< "enabling tracing in " << temp_dir
354+
<< " should not create another trace subdirectory";
355+
}
356+
357+
TEST_F(test_linting_lsp_server,
358+
changing_config_to_different_tracing_dir_resets_tracing) {
359+
trace_flusher tracer;
360+
fake_configuration_filesystem fs;
361+
mock_lsp_linter linter;
362+
linting_lsp_server_handler handler(&fs, &linter, &tracer);
363+
spy_lsp_endpoint_remote client;
364+
lsp_endpoint server(&handler, &client);
365+
366+
std::string original_tracing_dir = this->make_temporary_directory();
367+
std::string new_tracing_dir = this->make_temporary_directory();
368+
369+
server.append(make_message(
370+
u8R"({
371+
"jsonrpc": "2.0",
372+
"method": "workspace/didChangeConfiguration",
373+
"params": {
374+
"settings": {
375+
"quick-lint-js.tracing-directory": )" +
376+
json_to_string8(::boost::json::string(original_tracing_dir)) +
377+
u8R"(
378+
}
379+
}
380+
})"));
381+
server.append(
382+
make_message(u8R"({
383+
"jsonrpc": "2.0",
384+
"method": "workspace/didChangeConfiguration",
385+
"params": {
386+
"settings": {
387+
"quick-lint-js.tracing-directory": )" +
388+
json_to_string8(::boost::json::string(new_tracing_dir)) +
389+
u8R"(
390+
}
391+
}
392+
})"));
393+
394+
std::vector<std::string> new_children =
395+
list_files_in_directory(new_tracing_dir);
396+
EXPECT_THAT(new_children, ElementsAre(::testing::_))
397+
<< "changing tracing dir from " << original_tracing_dir << " to "
398+
<< new_tracing_dir << " should create a new trace subdirectory";
399+
}
400+
401+
TEST_F(test_linting_lsp_server,
402+
changing_tracing_dir_config_to_empty_disables_tracing) {
403+
std::string temp_dir = this->make_temporary_directory();
404+
trace_flusher tracer;
405+
fake_configuration_filesystem fs;
406+
mock_lsp_linter linter;
407+
linting_lsp_server_handler handler(&fs, &linter, &tracer);
408+
spy_lsp_endpoint_remote client;
409+
lsp_endpoint server(&handler, &client);
410+
411+
server.append(
412+
make_message(u8R"({
413+
"jsonrpc": "2.0",
414+
"method": "workspace/didChangeConfiguration",
415+
"params": {
416+
"settings": {
417+
"quick-lint-js.tracing-directory": )" +
418+
json_to_string8(::boost::json::string(temp_dir)) + u8R"(
419+
}
420+
}
421+
})"));
422+
EXPECT_TRUE(tracer.is_enabled());
423+
server.append(
424+
make_message(u8R"({
425+
"jsonrpc": "2.0",
426+
"method": "workspace/didChangeConfiguration",
427+
"params": {
428+
"settings": {
429+
"quick-lint-js.tracing-directory": ""
430+
}
431+
}
432+
})"));
433+
EXPECT_FALSE(tracer.is_enabled());
434+
}
435+
291436
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#shutdown
292437
TEST_F(test_linting_lsp_server, shutdown) {
293438
this->server->append(

0 commit comments

Comments
 (0)