diff --git a/plugin/vscode/package.json b/plugin/vscode/package.json index b8740a7364..f485ec84bd 100644 --- a/plugin/vscode/package.json +++ b/plugin/vscode/package.json @@ -47,6 +47,12 @@ ], "default": "off", "description": "Log document changes. Useful for quick-lint-js contributors." + }, + "quick-lint-js.snarky": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "Add spice to your failures." } } }, diff --git a/plugin/vscode/quick-lint-js/vscode/qljs-document.cpp b/plugin/vscode/quick-lint-js/vscode/qljs-document.cpp index 94d0b4b7f9..e93fbbd927 100644 --- a/plugin/vscode/quick-lint-js/vscode/qljs-document.cpp +++ b/plugin/vscode/quick-lint-js/vscode/qljs-document.cpp @@ -22,7 +22,7 @@ void QLJS_Config_Document::after_modification(::Napi::Env env, void QLJS_Lintable_Document::after_modification( ::Napi::Env env, QLJS_Workspace& workspace, VSCode_Diagnostic_Collection diagnostic_collection) { - this->lint_javascript_and_publish_diagnostics(env, workspace.vscode_, + this->lint_javascript_and_publish_diagnostics(env, workspace, diagnostic_collection); } @@ -61,10 +61,10 @@ void QLJS_Config_Document::finish_init( auto loaded_config_result = workspace.config_loader_.watch_and_load_config_file(*file_path, this); if (loaded_config_result.ok()) { + this->loaded_config_ = *loaded_config_result; workspace.vscode_.load_non_persistent(env); - this->lint_config_and_publish_diagnostics(env, workspace.vscode_, - workspace.diagnostic_collection(), - *loaded_config_result); + this->lint_config_and_publish_diagnostics( + env, workspace, workspace.diagnostic_collection()); } else { QLJS_UNIMPLEMENTED(); } @@ -74,16 +74,32 @@ void QLJS_Config_Document::on_config_file_changed( ::Napi::Env env, QLJS_Workspace& workspace, VSCode_Diagnostic_Collection diagnostic_collection, Loaded_Config_File* config_file) { - this->lint_config_and_publish_diagnostics(env, workspace.vscode_, - diagnostic_collection, config_file); + this->loaded_config_ = config_file; + this->lint_config_and_publish_diagnostics(env, workspace, + diagnostic_collection); } +void QLJS_Config_Document::on_translator_changed( + ::Napi::Env env, QLJS_Workspace& workspace, + VSCode_Diagnostic_Collection diagnostic_collection) { + this->lint_config_and_publish_diagnostics(env, workspace, + diagnostic_collection); +} void QLJS_Config_Document::lint_config_and_publish_diagnostics( - ::Napi::Env env, VSCode_Module& vscode, - VSCode_Diagnostic_Collection diagnostic_collection, - Loaded_Config_File* loaded_config) { - diagnostic_collection.set(this->uri(), - this->lint_config(env, &vscode, loaded_config)); + ::Napi::Env env, QLJS_Workspace& workspace, + VSCode_Diagnostic_Collection diagnostic_collection) { + diagnostic_collection.set(this->uri(), this->lint_config(env, workspace)); +} + +::Napi::Array QLJS_Config_Document::lint_config(::Napi::Env env, + QLJS_Workspace& workspace) { + workspace.vscode_.load_non_persistent(env); + + LSP_Locator locator(&this->loaded_config_->file_content); + VSCode_Diag_Reporter diag_reporter(&workspace.vscode_, env, &locator, + this->uri(), workspace.translator_); + diag_reporter.report(this->loaded_config_->errors); + return std::move(diag_reporter).diagnostics(); } void QLJS_Lintable_Document::on_config_file_changed( @@ -92,14 +108,36 @@ void QLJS_Lintable_Document::on_config_file_changed( Loaded_Config_File* config_file) { this->config_ = config_file ? &config_file->config : &workspace.default_config_; - this->lint_javascript_and_publish_diagnostics(env, workspace.vscode_, + this->lint_javascript_and_publish_diagnostics(env, workspace, diagnostic_collection); } +void QLJS_Lintable_Document::on_translator_changed( + ::Napi::Env env, QLJS_Workspace& workspace, + VSCode_Diagnostic_Collection diagnostic_collection) { + this->lint_javascript_and_publish_diagnostics(env, workspace, + diagnostic_collection); +} void QLJS_Lintable_Document::lint_javascript_and_publish_diagnostics( - ::Napi::Env env, VSCode_Module& vscode, + ::Napi::Env env, QLJS_Workspace& workspace, VSCode_Diagnostic_Collection diagnostic_collection) { - diagnostic_collection.set(this->uri(), this->lint_javascript(env, &vscode)); + diagnostic_collection.set(this->uri(), this->lint_javascript(env, workspace)); +} + +::Napi::Array QLJS_Lintable_Document::lint_javascript( + ::Napi::Env env, QLJS_Workspace& workspace) { + VSCode_Module& vscode = workspace.vscode_; + vscode.load_non_persistent(env); + + VSCode_Diag_Reporter diag_reporter(&vscode, env, &this->document_.locator(), + this->uri(), workspace.translator_); + parse_and_lint(this->document_.string(), diag_reporter, + Linter_Options{ + .language = this->language_, + .configuration = this->config_, + }); + + return std::move(diag_reporter).diagnostics(); } } diff --git a/plugin/vscode/quick-lint-js/vscode/qljs-document.h b/plugin/vscode/quick-lint-js/vscode/qljs-document.h index 79ce2be877..d4a3019df1 100644 --- a/plugin/vscode/quick-lint-js/vscode/qljs-document.h +++ b/plugin/vscode/quick-lint-js/vscode/qljs-document.h @@ -94,6 +94,10 @@ class QLJS_Document_Base { VSCode_Diagnostic_Collection, Loaded_Config_File* config_file) = 0; + virtual void on_translator_changed( + ::Napi::Env env, QLJS_Workspace& workspace, + VSCode_Diagnostic_Collection diagnostic_collection) = 0; + protected: ::Napi::Value uri() { return this->vscode_document_.Value().uri(); } @@ -118,21 +122,17 @@ class QLJS_Config_Document : public QLJS_Document_Base { void on_config_file_changed(::Napi::Env, QLJS_Workspace&, VSCode_Diagnostic_Collection, Loaded_Config_File* config_file) override; + void on_translator_changed( + ::Napi::Env env, QLJS_Workspace& workspace, + VSCode_Diagnostic_Collection diagnostic_collection) override; private: - void lint_config_and_publish_diagnostics(::Napi::Env, VSCode_Module&, - VSCode_Diagnostic_Collection, - Loaded_Config_File* loaded_config); - - ::Napi::Array lint_config(::Napi::Env env, VSCode_Module* vscode, - Loaded_Config_File* loaded_config) { - vscode->load_non_persistent(env); - - LSP_Locator locator(&loaded_config->file_content); - VSCode_Diag_Reporter diag_reporter(vscode, env, &locator, this->uri()); - diag_reporter.report(loaded_config->errors); - return std::move(diag_reporter).diagnostics(); - } + void lint_config_and_publish_diagnostics(::Napi::Env, QLJS_Workspace&, + VSCode_Diagnostic_Collection); + + ::Napi::Array lint_config(::Napi::Env env, QLJS_Workspace& workspace); + + Loaded_Config_File* loaded_config_ = nullptr; }; class QLJS_Lintable_Document : public QLJS_Document_Base { @@ -149,24 +149,14 @@ class QLJS_Lintable_Document : public QLJS_Document_Base { void on_config_file_changed(::Napi::Env, QLJS_Workspace&, VSCode_Diagnostic_Collection, Loaded_Config_File* config_file) override; + void on_translator_changed(::Napi::Env, QLJS_Workspace&, + VSCode_Diagnostic_Collection) override; - void lint_javascript_and_publish_diagnostics(::Napi::Env, VSCode_Module&, + void lint_javascript_and_publish_diagnostics(::Napi::Env, QLJS_Workspace&, VSCode_Diagnostic_Collection); private: - ::Napi::Array lint_javascript(::Napi::Env env, VSCode_Module* vscode) { - vscode->load_non_persistent(env); - - VSCode_Diag_Reporter diag_reporter(vscode, env, &this->document_.locator(), - this->uri()); - parse_and_lint(this->document_.string(), diag_reporter, - Linter_Options{ - .language = this->language_, - .configuration = this->config_, - }); - - return std::move(diag_reporter).diagnostics(); - } + ::Napi::Array lint_javascript(::Napi::Env env, QLJS_Workspace& workspace); Configuration* config_; // Initialized by finish_init. diff --git a/plugin/vscode/quick-lint-js/vscode/qljs-workspace.cpp b/plugin/vscode/quick-lint-js/vscode/qljs-workspace.cpp index 62eadefbf2..522c9cedf1 100644 --- a/plugin/vscode/quick-lint-js/vscode/qljs-workspace.cpp +++ b/plugin/vscode/quick-lint-js/vscode/qljs-workspace.cpp @@ -86,6 +86,14 @@ class Extension_Configuration { } } + bool get_snarky(::Napi::Env env) { + ::Napi::Value value = this->get(env, "snarky"); + if (!value.IsBoolean()) { + return false; + } + return value.As<::Napi::Boolean>(); + } + ::Napi::Value get(::Napi::Env env, const char* section) { return this->config_ref_.Get("get").As<::Napi::Function>().Call( this->config_ref_.Value(), {::Napi::String::New(env, section)}); @@ -124,7 +132,8 @@ QLJS_Workspace::QLJS_Workspace(const Napi::CallbackInfo& info) ::Napi::Persistent(info[2].As<::Napi::Object>())), ui_(this) { QLJS_DEBUG_LOG("Workspace %p: created\n", this); - this->update_logging(info.Env()); + configuration_changed(info); + this->fs_change_detection_thread_ = Thread([this]() -> void { this->run_fs_change_detection_thread(); }); } @@ -234,10 +243,29 @@ ::Napi::Value QLJS_Workspace::configuration_changed( ::Napi::Env env = info.Env(); this->update_logging(env); + this->on_translator_changed(env); return env.Undefined(); } +void QLJS_Workspace::on_translator_changed(::Napi::Env env) { + Extension_Configuration config(env, this->vscode_); + bool is_snarky = config.get_snarky(env); + + this->is_snarky_enabled_ = is_snarky; + if (is_snarky) { + this->translator_.use_messages_from_locale("en_US@snarky"); + } else { + // TODO(#529): Use the locale from the VS Code configuration. + this->translator_.use_messages_from_source_code(); + } + this->qljs_documents_.for_each( + [this, is_snarky, env](::Napi::Value value) -> void { + QLJS_Document_Base* doc = QLJS_Document_Base::unwrap(value); + doc->on_translator_changed(env, *this, this->diagnostic_collection()); + }); +} + ::Napi::Value QLJS_Workspace::editor_visibility_changed( const Napi::CallbackInfo& info) { ::Napi::Env env = info.Env(); diff --git a/plugin/vscode/quick-lint-js/vscode/qljs-workspace.h b/plugin/vscode/quick-lint-js/vscode/qljs-workspace.h index 43f15ddd19..f49752ee99 100644 --- a/plugin/vscode/quick-lint-js/vscode/qljs-workspace.h +++ b/plugin/vscode/quick-lint-js/vscode/qljs-workspace.h @@ -73,6 +73,8 @@ class QLJS_Workspace : public ::Napi::ObjectWrap { // Disable logging if logging is enabled. void disable_logging(); + void on_translator_changed(::Napi::Env env); + ~QLJS_Workspace(); ::Napi::Value dispose(const ::Napi::CallbackInfo& info); @@ -248,6 +250,13 @@ class QLJS_Workspace : public ::Napi::ObjectWrap { QLJS_Workspace* workspace_; }; + public: + bool is_snarky_enabled() const; + + private: + bool is_snarky_enabled_ = false; + Translator translator_; + bool disposed_ = false; VSCode_Tracer tracer_; VSCode_Module vscode_; diff --git a/plugin/vscode/quick-lint-js/vscode/vscode-diag-reporter.h b/plugin/vscode/quick-lint-js/vscode/vscode-diag-reporter.h index fb32c15e8f..e9e9c3c2da 100644 --- a/plugin/vscode/quick-lint-js/vscode/vscode-diag-reporter.h +++ b/plugin/vscode/quick-lint-js/vscode/vscode-diag-reporter.h @@ -18,8 +18,9 @@ class VSCode_Diag_Formatter explicit VSCode_Diag_Formatter(VSCode_Module* vscode, ::Napi::Env env, ::Napi::Array diagnostics, const LSP_Locator* locator, - ::Napi::Value document_uri) - : Diagnostic_Formatter(qljs_messages), + ::Napi::Value document_uri, + Translator message_translator) + : Diagnostic_Formatter(message_translator), vscode_(vscode), env_(env), diagnostics_(diagnostics), @@ -129,12 +130,14 @@ class VSCode_Diag_Reporter final : public Diag_Reporter { public: explicit VSCode_Diag_Reporter(VSCode_Module* vscode, ::Napi::Env env, const LSP_Locator* locator, - ::Napi::Value document_uri) + ::Napi::Value document_uri, + Translator message_translator) : vscode_(vscode), env_(env), diagnostics_(::Napi::Array::New(env)), locator_(locator), - document_uri_(document_uri) {} + document_uri_(document_uri), + message_translator_(message_translator) {} ::Napi::Array diagnostics() const { return this->diagnostics_; } @@ -144,7 +147,8 @@ class VSCode_Diag_Reporter final : public Diag_Reporter { /*env=*/this->env_, /*diagnostics=*/this->diagnostics_, /*locator=*/this->locator_, - /*document_uri=*/this->document_uri_); + /*document_uri=*/this->document_uri_, + /*message_translator=*/this->message_translator_); formatter.format(get_diagnostic_info(type), diag); } @@ -154,6 +158,7 @@ class VSCode_Diag_Reporter final : public Diag_Reporter { ::Napi::Array diagnostics_; const LSP_Locator* locator_; ::Napi::Value document_uri_; + Translator message_translator_; }; } diff --git a/plugin/vscode/test/vscode-tests.js b/plugin/vscode/test/vscode-tests.js index e735a6b82d..40d2675b3f 100644 --- a/plugin/vscode/test/vscode-tests.js +++ b/plugin/vscode/test/vscode-tests.js @@ -74,6 +74,113 @@ for (let extension of [".js", ".mjs", ".cjs", ".jsx"]) { }; } +// SNARKY tests +for (let testCase of [ + { + fileName: "hello.js", + content: "undeclaredVariable", + englishMessage: "use of undeclared variable: undeclaredVariable", + snarkyEnglishMessage: "did you fail spelling class?", + }, + { + fileName: "quick-lint-js.config", + content: "{", + englishMessage: "JSON syntax error", + snarkyEnglishMessage: "yeah, JSON sucks; try quick-lint-json", + }, +]) { + tests = { + ...tests, + [`snarky enabled at start (${testCase.fileName})`]: async ({ + addCleanup, + }) => { + addCleanup(resetConfigurationAsync); + + await vscode.workspace + .getConfiguration("quick-lint-js") + .update("snarky", true, vscode.ConfigurationTarget.Workspace); + + let scratchDirectory = makeScratchDirectory({ addCleanup }); + let filePath = path.join(scratchDirectory, testCase.fileName); + fs.writeFileSync(filePath, testCase.content); + let helloURI = vscode.Uri.file(filePath); + let helloDocument = await vscode.workspace.openTextDocument(helloURI); + await loadExtensionAsync({ addCleanup }); + let helloEditor = await vscode.window.showTextDocument(helloDocument); + + await waitUntilAnyDiagnosticsAsync(helloURI); + let diags = normalizeDiagnostics(helloURI).map(({ message }) => message); + assert.deepStrictEqual(diags, [testCase.snarkyEnglishMessage]); + }, + + [`enabling snarky re-lints (${testCase.fileName})`]: async ({ + addCleanup, + }) => { + addCleanup(resetConfigurationAsync); + await loadExtensionAsync({ addCleanup }); + let scratchDirectory = makeScratchDirectory({ addCleanup }); + let filePath = path.join(scratchDirectory, testCase.fileName); + fs.writeFileSync(filePath, testCase.content); + let helloURI = vscode.Uri.file(filePath); + let helloDocument = await vscode.workspace.openTextDocument(helloURI); + let helloEditor = await vscode.window.showTextDocument(helloDocument); + + // 1. Make sure we're polite at the start + { + await waitUntilAnyDiagnosticsAsync(helloURI); + let diagMessages = normalizeDiagnostics(helloURI).map( + ({ message }) => message + ); + assert.deepStrictEqual(diagMessages, [testCase.englishMessage]); + } + + // 2. Enable snarky + await vscode.workspace + .getConfiguration("quick-lint-js") + .update("snarky", true, vscode.ConfigurationTarget.Workspace); + + // 3. Make sure we're snarky now + await pollAsync(() => { + let diagMessages = normalizeDiagnostics(helloURI).map( + ({ message }) => message + ); + let want = [testCase.snarkyEnglishMessage]; + assert.deepStrictEqual(diagMessages, want); + }); + }, + + [`disabling snarky re-lints (${testCase.fileName})`]: async ({ + addCleanup, + }) => { + addCleanup(resetConfigurationAsync); + await vscode.workspace + .getConfiguration("quick-lint-js") + .update("snarky", true, vscode.ConfigurationTarget.Workspace); + let scratchDirectory = makeScratchDirectory({ addCleanup }); + let filePath = path.join(scratchDirectory, testCase.fileName); + fs.writeFileSync(filePath, testCase.content); + let helloURI = vscode.Uri.file(filePath); + let helloDocument = await vscode.workspace.openTextDocument(helloURI); + await loadExtensionAsync({ addCleanup }); + let helloEditor = await vscode.window.showTextDocument(helloDocument); + + // 1. Disable snarky + await vscode.workspace + .getConfiguration("quick-lint-js") + .update("snarky", false, vscode.ConfigurationTarget.Workspace); + + // 2. Make sure we're polite now + await pollAsync(() => { + let diagMessages = normalizeDiagnostics(helloURI).map( + ({ message }) => message + ); + let want = [testCase.englishMessage]; + assert.deepStrictEqual(diagMessages, want); + }); + }, + }; +} + tests = { ...tests, @@ -1535,7 +1642,7 @@ async function pollAsync(callback) { } async function resetConfigurationAsync() { - for (let setting of ["logging"]) { + for (let setting of ["logging", "snarky"]) { await vscode.workspace .getConfiguration("quick-lint-js") .update(setting, undefined, vscode.ConfigurationTarget.Workspace); diff --git a/src/quick-lint-js/i18n/translation.cpp b/src/quick-lint-js/i18n/translation.cpp index d050377864..3b1649321a 100644 --- a/src/quick-lint-js/i18n/translation.cpp +++ b/src/quick-lint-js/i18n/translation.cpp @@ -63,6 +63,7 @@ Span get_user_locale_preferences( // TODO(strager): Determine the language using macOS' and Windows' native // APIs. See GNU gettext's _nl_language_preferences_default. + // TODO (#529): Also use VSCode's "Display Language" setting Vector locales("locales", allocator); locales.push_back(locale);