Repository: gleam-lang/gleam (rust)

Issue:
Only offer "Generate JSON encoder" code action if `gleam_json` is a dependency
Someone mentioned [on discord](https://discord.com/channels/768594524158427167/768594524158427170/1352287162024067122) that it can be a little annoying to have the "Generate JSON encoder" code action when the `gleam_json` package is not installed.
>  I feel a little awkward that the lsp is suggesting code generation for a gleam package that I don't even have installed

So my proposal is to only offer the code action if `gleam_json` is a dependency or dev-dependency of the current project.

Pull request description:
Only offer "Generate JSON encoder" code action if `gleam_json` is a dependency
Closes #4376 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 783d8aeb5..a512b542f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -66,6 +66,9 @@
   project's `gleam.toml` as a JSON file.
   ([Rodrigo Álvarez](https://github.com/Papipo))
 
+- Improve error messages when encrypting and decrypting local Hex API key.
+  ([Samuel Cristobal](https://github.com/scristobal))
+
 ### Language server
 
 - The language server now allows renaming of functions, constants,
@@ -237,6 +240,10 @@
 
   ([Matias Carlander](https://github.com/matiascr))
 
+- The language server will now only offer the code action to generate a JSON
+  encoder if the `gleam_json` package is installed as a dependency.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ### Formatter
 
 ### Bug fixes
@@ -264,6 +271,14 @@
 - Fixed a bug where variant inference wouldn't work on `let assert` assignments.
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
 
+- Fixed a bug where the build tool could fail to lock the build directory but
+  not report an error.
+  ([Louis Pilfold](https://github.com/lpil))
+
+- Fixed a bug where the language server would be too eager to recompile modules
+  when it could use the cache from previous compilations.
+  ([Louis Pilfold](https://github.com/lpil))
+
 ## v1.9.1 - 2025-03-10
 
 ### Formatter
diff --git a/compiler-cli/src/build_lock.rs b/compiler-cli/src/build_lock.rs
index 2cb870788..e7c924a3e 100644
--- a/compiler-cli/src/build_lock.rs
+++ b/compiler-cli/src/build_lock.rs
@@ -88,7 +88,7 @@ pub(crate) struct Guard(
 fn locking_global() {
     let paths = crate::project_paths_at_current_directory_without_toml();
     let lock = BuildLock::new_packages(&paths).expect("make lock");
-    let _guard1 = lock.lock(&gleam_core::build::NullTelemetry);
+    let _guard1: Guard = lock.lock(&gleam_core::build::NullTelemetry).unwrap();
     println!("Locked!")
 }
 
@@ -96,7 +96,7 @@ fn locking_global() {
 fn locking_dev_erlang() {
     let paths = crate::project_paths_at_current_directory_without_toml();
     let lock = BuildLock::new_target(&paths, Mode::Dev, Target::Erlang).expect("make lock");
-    let _guard1 = lock.lock(&gleam_core::build::NullTelemetry);
+    let _guard1: Guard = lock.lock(&gleam_core::build::NullTelemetry).unwrap();
     println!("Locked!")
 }
 
@@ -104,7 +104,7 @@ fn locking_dev_erlang() {
 fn locking_prod_erlang() {
     let paths = crate::project_paths_at_current_directory_without_toml();
     let lock = BuildLock::new_target(&paths, Mode::Prod, Target::Erlang).expect("make lock");
-    let _guard1 = lock.lock(&gleam_core::build::NullTelemetry);
+    let _guard1: Guard = lock.lock(&gleam_core::build::NullTelemetry).unwrap();
     println!("Locked!")
 }
 
@@ -112,7 +112,7 @@ fn locking_prod_erlang() {
 fn locking_lsp_erlang() {
     let paths = crate::project_paths_at_current_directory_without_toml();
     let lock = BuildLock::new_target(&paths, Mode::Lsp, Target::Erlang).expect("make lock");
-    let _guard1 = lock.lock(&gleam_core::build::NullTelemetry);
+    let _guard1: Guard = lock.lock(&gleam_core::build::NullTelemetry).unwrap();
     println!("Locked!")
 }
 
@@ -120,7 +120,7 @@ fn locking_lsp_erlang() {
 fn locking_dev_javascript() {
     let paths = crate::project_paths_at_current_directory_without_toml();
     let lock = BuildLock::new_target(&paths, Mode::Dev, Target::JavaScript).expect("make lock");
-    let _guard1 = lock.lock(&gleam_core::build::NullTelemetry);
+    let _guard1: Guard = lock.lock(&gleam_core::build::NullTelemetry).unwrap();
     println!("Locked!")
 }
 
@@ -128,7 +128,7 @@ fn locking_dev_javascript() {
 fn locking_prod_javascript() {
     let paths = crate::project_paths_at_current_directory_without_toml();
     let lock = BuildLock::new_target(&paths, Mode::Prod, Target::JavaScript).expect("make lock");
-    let _guard1 = lock.lock(&gleam_core::build::NullTelemetry);
+    let _guard1: Guard = lock.lock(&gleam_core::build::NullTelemetry).unwrap();
     println!("Locked!")
 }
 
@@ -136,6 +136,6 @@ fn locking_prod_javascript() {
 fn locking_lsp_javascript() {
     let paths = crate::project_paths_at_current_directory_without_toml();
     let lock = BuildLock::new_target(&paths, Mode::Lsp, Target::JavaScript).expect("make lock");
-    let _guard1 = lock.lock(&gleam_core::build::NullTelemetry);
+    let _guard1: Guard = lock.lock(&gleam_core::build::NullTelemetry).unwrap();
     println!("Locked!")
 }
diff --git a/compiler-cli/src/dependencies.rs b/compiler-cli/src/dependencies.rs
index ac2ebd908..d8bb9d77c 100644
--- a/compiler-cli/src/dependencies.rs
+++ b/compiler-cli/src/dependencies.rs
@@ -30,7 +30,7 @@ mod tests;
 
 use crate::{
     TreeOptions,
-    build_lock::BuildLock,
+    build_lock::{BuildLock, Guard},
     cli,
     fs::{self, ProjectIO},
     http::HttpClient,
@@ -232,7 +232,7 @@ pub fn cleanup<Telem: Telemetry>(paths: &ProjectPaths, telemetry: Telem) -> Resu
     crate::config::ensure_config_exists(paths)?;
 
     let lock = BuildLock::new_packages(paths)?;
-    let _guard = lock.lock(&telemetry);
+    let _guard: Guard = lock.lock(&telemetry)?;
 
     // Read the project config
     let config = crate::root_config(paths)?;
@@ -363,7 +363,7 @@ pub fn download<Telem: Telemetry>(
     crate::config::ensure_config_exists(paths)?;
 
     let lock = BuildLock::new_packages(paths)?;
-    let _guard = lock.lock(&telemetry);
+    let _guard: Guard = lock.lock(&telemetry)?;
 
     let fs = ProjectIO::boxed();
 
diff --git a/compiler-cli/src/hex/auth.rs b/compiler-cli/src/hex/auth.rs
index 0405932d3..71da61ffa 100644
--- a/compiler-cli/src/hex/auth.rs
+++ b/compiler-cli/src/hex/auth.rs
@@ -62,7 +62,10 @@ encrypt your Hex API key.
             );
         }
         let password = self.ask_local_password()?;
-        let encrypted = encryption::encrypt_with_passphrase(api_key.as_bytes(), &password)?;
+        let encrypted = encryption::encrypt_with_passphrase(api_key.as_bytes(), &password)
+            .map_err(|e| Error::FailedToEncryptLocalHexApiKey {
+                detail: e.to_string(),
+            })?;
 
         crate::fs::write(&path, &format!("{name}\n{encrypted}"))?;
         println!("Encrypted Hex API key written to {path}");
@@ -110,8 +113,13 @@ encrypt your Hex API key.
         let Some(EncryptedApiKey { encrypted, .. }) = self.read_stored_api_key()? else {
             return Ok(None);
         };
+
         let password = self.ask_local_password()?;
-        let unencrypted = encryption::decrypt_with_passphrase(encrypted.as_bytes(), &password)?;
+        let unencrypted = encryption::decrypt_with_passphrase(encrypted.as_bytes(), &password)
+            .map_err(|e| Error::FailedToDecryptLocalHexApiKey {
+                detail: e.to_string(),
+            })?;
+
         Ok(Some(UnencryptedApiKey { unencrypted }))
     }
 
diff --git a/compiler-cli/src/lsp.rs b/compiler-cli/src/lsp.rs
index 1bfc2d96e..7d9939edb 100644
--- a/compiler-cli/src/lsp.rs
+++ b/compiler-cli/src/lsp.rs
@@ -1,4 +1,7 @@
-use crate::{build_lock::BuildLock, fs::ProjectIO};
+use crate::{
+    build_lock::{BuildLock, Guard},
+    fs::ProjectIO,
+};
 use gleam_core::{
     Result,
     build::{Mode, NullTelemetry, Target},
@@ -54,7 +57,8 @@ impl LspLocker {
 }
 
 impl Locker for LspLocker {
-    fn lock_for_build(&self) -> LockGuard {
-        LockGuard(Box::new(self.0.lock(&NullTelemetry)))
+    fn lock_for_build(&self) -> Result<LockGuard> {
+        let guard: Guard = self.0.lock(&NullTelemetry)?;
+        Ok(LockGuard(Box::new(guard)))
     }
 }
diff --git a/compiler-core/src/build/package_loader.rs b/compiler-core/src/build/package_loader.rs
index f82d0b35a..76aa262ce 100644
--- a/compiler-core/src/build/package_loader.rs
+++ b/compiler-core/src/build/package_loader.rs
@@ -1641,6 +1641,10 @@ impl StaleTracker {
     pub fn empty(&mut self) {
         let _ = self.0.drain(); // Clears the set but retains allocated memory
     }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
 }
 
 #[derive(Debug)]
diff --git a/compiler-core/src/build/project_compiler.rs b/compiler-core/src/build/project_compiler.rs
index b8da5f5d2..fcd3fdc86 100644
--- a/compiler-core/src/build/project_compiler.rs
+++ b/compiler-core/src/build/project_compiler.rs
@@ -170,14 +170,19 @@ where
         self.options.target.unwrap_or(self.config.target)
     }
 
-    /// Compiles all packages in the project and returns the compiled
-    /// information from the root package
-    pub fn compile(mut self) -> Result<Built> {
+    pub fn reset_state_for_new_compile_run(&mut self) {
         // We make sure the stale module tracker is empty before we start, to
         // avoid mistakenly thinking a module is stale due to outdated state
         // from a previous build. A ProjectCompiler instance is re-used by the
         // LSP engine so state could be reused if we don't reset it.
+
         self.stale_modules.empty();
+    }
+
+    /// Compiles all packages in the project and returns the compiled
+    /// information from the root package
+    pub fn compile(mut self) -> Result<Built> {
+        self.reset_state_for_new_compile_run();
 
         // Each package may specify a Gleam version that it supports, so we
         // verify that this version is appropriate.
@@ -257,6 +262,11 @@ where
     }
 
     pub fn compile_dependencies(&mut self) -> Result<Vec<Module>, Error> {
+        assert!(
+            self.stale_modules.is_empty(),
+            "The project compiler stale tracker was not emptied from the previous compilation"
+        );
+
         let sequence = order_packages(&self.packages)?;
         let mut modules = vec![];
 
diff --git a/compiler-core/src/encryption.rs b/compiler-core/src/encryption.rs
index 160adaf88..309a5d5be 100644
--- a/compiler-core/src/encryption.rs
+++ b/compiler-core/src/encryption.rs
@@ -1,24 +1,37 @@
-use crate::{Error, Result};
+use thiserror::Error;
 
-pub fn encrypt_with_passphrase(message: &[u8], passphrase: &str) -> Result<String> {
+pub fn encrypt_with_passphrase(
+    message: &[u8],
+    passphrase: &str,
+) -> Result<String, age::EncryptError> {
     let passphrase = age::secrecy::SecretString::from(passphrase);
     let recipient = age::scrypt::Recipient::new(passphrase.clone());
-    let encrypted =
-        age::encrypt_and_armor(&recipient, message).map_err(|e| Error::FailedToEncrypt {
-            detail: e.to_string(),
-        })?;
+
+    let encrypted = age::encrypt_and_armor(&recipient, message)?;
+
     Ok(encrypted)
 }
 
-pub fn decrypt_with_passphrase(encrypted_message: &[u8], passphrase: &str) -> Result<String> {
+// the function `decrypt_with_passphrase` has two possible failure cases:
+// - when decryption fails
+// - when the data was decrypted succesfully but the result is not UTF-8 valid
+#[derive(Error, Debug)]
+pub enum DecryptError {
+    #[error("unable to decrypt message: {0}")]
+    Decrypt(#[from] age::DecryptError),
+    #[error("decrypted message is not UTF-8 valid: {0}")]
+    Io(#[from] std::string::FromUtf8Error),
+}
+
+pub fn decrypt_with_passphrase(
+    encrypted_message: &[u8],
+    passphrase: &str,
+) -> Result<String, DecryptError> {
     let passphrase = age::secrecy::SecretString::from(passphrase);
     let identity = age::scrypt::Identity::new(passphrase);
-    let decrypted =
-        age::decrypt(&identity, encrypted_message).map_err(|e| Error::FailedToDecrypt {
-            detail: e.to_string(),
-        })?;
-    let decrypted = String::from_utf8(decrypted).map_err(|e| Error::FailedToDecrypt {
-        detail: e.to_string(),
-    })?;
+
+    let decrypted = age::decrypt(&identity, encrypted_message)?;
+    let decrypted = String::from_utf8(decrypted)?;
+
     Ok(decrypted)
 }
diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs
index 0f2a71f1f..cad477f6a 100644
--- a/compiler-core/src/error.rs
+++ b/compiler-core/src/error.rs
@@ -316,11 +316,11 @@ file_names.iter().map(|x| x.as_str()).join(", "))]
         wrongfully_allowed_version: SmallVersion,
     },
 
-    #[error("Failed to encrypt data")]
-    FailedToEncrypt { detail: String },
+    #[error("Failed to encrypt local Hex API key")]
+    FailedToEncryptLocalHexApiKey { detail: String },
 
-    #[error("Failed to decrypt data")]
-    FailedToDecrypt { detail: String },
+    #[error("Failed to decrypt local Hex API key")]
+    FailedToDecryptLocalHexApiKey { detail: String },
 }
 
 /// This is to make clippy happy and not make the error variant too big by
@@ -1457,8 +1457,8 @@ https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-fo
             }
 
 
-            Error::FailedToEncrypt { detail } => {
-                let text = wrap_format!("A problem was encountered encrypting data.
+            Error::FailedToEncryptLocalHexApiKey { detail } => {
+                let text = wrap_format!("A problem was encountered encrypting the local Hex API key with the given password.
 The error from the encryption library was:
 
     {detail}"
@@ -1472,14 +1472,14 @@ The error from the encryption library was:
                 }]
             }
 
-            Error::FailedToDecrypt { detail } => {
-                let text = wrap_format!("A problem was encountered decrypting data.
+            Error::FailedToDecryptLocalHexApiKey { detail }=> {
+                let text = wrap_format!("Unable to decrypt the local Hex API key with the given password.
 The error from the encryption library was:
 
     {detail}"
 );
                 vec![Diagnostic {
-                    title: "Failed to decrypt data".into(),
+                    title: "Failed to decrypt local Hex API key".into(),
                     text,
                     hint: None,
                     level: Level::Error,
diff --git a/compiler-core/src/language_server.rs b/compiler-core/src/language_server.rs
index 1f27d34e0..6b20578bf 100644
--- a/compiler-core/src/language_server.rs
+++ b/compiler-core/src/language_server.rs
@@ -30,7 +30,7 @@ use std::any::Any;
 pub struct LockGuard(pub Box<dyn Any>);
 
 pub trait Locker {
-    fn lock_for_build(&self) -> LockGuard;
+    fn lock_for_build(&self) -> Result<LockGuard>;
 }
 
 pub trait MakeLocker {
diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs
index a1242e141..99a3801dc 100644
--- a/compiler-core/src/language_server/code_action.rs
+++ b/compiler-core/src/language_server/code_action.rs
@@ -11,6 +11,7 @@ use crate::{
         visit::{Visit as _, visit_typed_call_arg, visit_typed_pattern_call_arg},
     },
     build::{Located, Module},
+    config::PackageConfig,
     io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter},
     line_numbers::LineNumbers,
     parse::{extra::ModuleExtra, lexer::str_to_keyword},
@@ -3779,6 +3780,7 @@ pub struct GenerateJsonEncoder<'a> {
     edits: TextEdits<'a>,
     printer: Printer<'a>,
     actions: &'a mut Vec<CodeAction>,
+    config: &'a PackageConfig,
 }
 
 const JSON_MODULE: &str = "gleam/json";
@@ -3810,6 +3812,7 @@ impl<'a> GenerateJsonEncoder<'a> {
         line_numbers: &'a LineNumbers,
         params: &'a CodeActionParams,
         actions: &'a mut Vec<CodeAction>,
+        config: &'a PackageConfig,
     ) -> Self {
         let printer = Printer::new(&module.ast.names);
         Self {
@@ -3818,11 +3821,16 @@ impl<'a> GenerateJsonEncoder<'a> {
             edits: TextEdits::new(line_numbers),
             printer,
             actions,
+            config,
         }
     }
 
     pub fn code_actions(&mut self) {
-        self.visit_typed_module(&self.module.ast);
+        if self.config.dependencies.contains_key(JSON_PACKAGE_NAME)
+            || self.config.dev_dependencies.contains_key(JSON_PACKAGE_NAME)
+        {
+            self.visit_typed_module(&self.module.ast);
+        }
     }
 
     fn custom_type_encoder_body(
diff --git a/compiler-core/src/language_server/compiler.rs b/compiler-core/src/language_server/compiler.rs
index 582c80604..4394728bb 100644
--- a/compiler-core/src/language_server/compiler.rs
+++ b/compiler-core/src/language_server/compiler.rs
@@ -19,6 +19,8 @@ use std::{collections::HashMap, rc::Rc};
 
 use camino::Utf8PathBuf;
 
+use super::LockGuard;
+
 /// A wrapper around the project compiler which makes it possible to repeatedly
 /// recompile the top level package, reusing the information about the already
 /// compiled dependency packages.
@@ -59,7 +61,7 @@ where
         // package before we run for the first time.
         // TODO: remove this once the caches have contain all the information
         {
-            let _guard = locker.lock_for_build();
+            let _guard: LockGuard = locker.lock_for_build()?;
             let path = paths.build_directory_for_package(Mode::Lsp, target, &name);
             io.delete_directory(&path)?;
         }
@@ -98,7 +100,10 @@ where
 
     pub fn compile(&mut self) -> Outcome<Vec<Utf8PathBuf>, Error> {
         // Lock the build directory to ensure to ensure we are the only one compiling
-        let _lock_guard = self.locker.lock_for_build();
+        let _lock_guard: LockGuard = match self.locker.lock_for_build() {
+            Ok(it) => it,
+            Err(err) => return err.into(),
+        };
 
         // Verify that the build directory was created using the same version of
         // Gleam as we are running. If it is not then we discard the build
@@ -107,6 +112,8 @@ where
             return e.into();
         }
 
+        self.project_compiler.reset_state_for_new_compile_run();
+
         let compiled_dependencies = match self.project_compiler.compile_dependencies() {
             Ok(it) => it,
             Err(err) => return err.into(),
diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs
index e3ad799d8..9077e0dd6 100644
--- a/compiler-core/src/language_server/engine.rs
+++ b/compiler-core/src/language_server/engine.rs
@@ -400,7 +400,14 @@ where
             );
             actions.extend(InlineVariable::new(module, &lines, &params).code_actions());
             GenerateDynamicDecoder::new(module, &lines, &params, &mut actions).code_actions();
-            GenerateJsonEncoder::new(module, &lines, &params, &mut actions).code_actions();
+            GenerateJsonEncoder::new(
+                module,
+                &lines,
+                &params,
+                &mut actions,
+                &this.compiler.project_compiler.config,
+            )
+            .code_actions();
             AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
             Ok(if actions.is_empty() {
                 None


Test patch:
diff --git a/compiler-core/src/language_server/tests.rs b/compiler-core/src/language_server/tests.rs
index d19cd0476..e8172df15 100644
--- a/compiler-core/src/language_server/tests.rs
+++ b/compiler-core/src/language_server/tests.rs
@@ -261,9 +261,9 @@ impl TestLocker {
 }
 
 impl Locker for TestLocker {
-    fn lock_for_build(&self) -> LockGuard {
+    fn lock_for_build(&self) -> Result<LockGuard> {
         self.record(Action::LockBuild);
-        LockGuard(Box::new(Guard(self.actions.clone())))
+        Ok(LockGuard(Box::new(Guard(self.actions.clone()))))
     }
 }
 
@@ -397,6 +397,7 @@ struct TestProject<'a> {
     hex_modules: Vec<(&'a str, &'a str)>,
     dev_hex_modules: Vec<(&'a str, &'a str)>,
     indirect_hex_modules: Vec<(&'a str, &'a str)>,
+    package_modules: HashMap<&'a str, Vec<(&'a str, &'a str)>>,
 }
 
 impl<'a> TestProject<'a> {
@@ -409,6 +410,7 @@ impl<'a> TestProject<'a> {
             hex_modules: vec![],
             dev_hex_modules: vec![],
             indirect_hex_modules: vec![],
+            package_modules: HashMap::new(),
         }
     }
 
@@ -475,6 +477,14 @@ impl<'a> TestProject<'a> {
         self
     }
 
+    pub fn add_package_module(mut self, package: &'a str, name: &'a str, src: &'a str) -> Self {
+        self.package_modules
+            .entry(package)
+            .or_default()
+            .push((name, src));
+        self
+    }
+
     pub fn build_engine(
         &self,
         io: &mut LanguageServerTestIO,
@@ -492,6 +502,13 @@ impl<'a> TestProject<'a> {
             _ = io.hex_dep_module("indirect_hex", name, code);
         });
 
+        for (package, modules) in self.package_modules.iter() {
+            io.add_hex_package(package);
+            for (module, code) in modules {
+                _ = io.hex_dep_module(package, module, code);
+            }
+        }
+
         let mut engine = setup_engine(io);
 
         // Add an external dependency and all its modules
diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs
index b60959c3b..9eaf5aaa2 100644
--- a/compiler-core/src/language_server/tests/action.rs
+++ b/compiler-core/src/language_server/tests/action.rs
@@ -6363,13 +6363,19 @@ fn map(list: List(a), fun: fn(a) -> b) -> List(b) { todo }
 
 #[test]
 fn generate_json_encoder() {
-    assert_code_action!(
-        GENERATE_JSON_ENCODER,
-        "
+    let src = "
 pub type Person {
   Person(name: String, age: Int, height: Float, is_cool: Bool)
 }
-",
+";
+
+    assert_code_action!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type").to_selection()
     );
 }
@@ -6411,7 +6417,8 @@ pub type Wibble(value) {
         GENERATE_JSON_ENCODER,
         TestProject::for_source(src)
             .add_module("gleam/option", "pub type Option(a)")
-            .add_module("gleam/dict", "pub type Dict(k, v)"),
+            .add_module("gleam/dict", "pub type Dict(k, v)")
+            .add_package_module("gleam_json", "gleam/json", "pub type Json"),
         find_position_of("type W").to_selection()
     );
 }
@@ -6443,7 +6450,11 @@ pub type Wibble {
 
     assert_code_action!(
         GENERATE_JSON_ENCODER,
-        TestProject::for_source(src).add_module("gleam/json", "pub type Json"),
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type W").to_selection()
     );
 }
@@ -6463,55 +6474,79 @@ pub fn main() {
 
 #[test]
 fn generate_json_encoder_tuple() {
-    assert_code_action!(
-        GENERATE_JSON_ENCODER,
-        "
+    let src = "
 pub type Wibble {
   Wibble(tuple: #(Int, Float, #(String, Bool)))
 }
-",
+";
+
+    assert_code_action!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type W").to_selection()
     );
 }
 
 #[test]
 fn generate_json_encoder_for_variant_with_no_fields() {
-    assert_code_action!(
-        GENERATE_JSON_ENCODER,
-        "
+    let src = "
 pub type Wibble {
   Wibble
 }
-",
+";
+
+    assert_code_action!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type W").to_selection()
     );
 }
 
 #[test]
 fn generate_json_encoder_for_type_with_multiple_variants_with_no_fields() {
-    assert_code_action!(
-        GENERATE_JSON_ENCODER,
-        "
+    let src = "
 pub type Wibble {
   Wibble
   Wobble
   Woo
 }
-",
+";
+
+    assert_code_action!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type W").to_selection()
     );
 }
 
 #[test]
 fn generate_json_encoder_for_variants_with_mixed_fields() {
-    assert_code_action!(
-        GENERATE_JSON_ENCODER,
-        "
+    let src = "
 pub type Wibble {
   Wibble
   Wobble(field: String, field1: Int)
 }
-",
+";
+
+    assert_code_action!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type W").to_selection()
     );
 }
@@ -6545,7 +6580,8 @@ pub type LinkedList {
     assert_code_action!(
         GENERATE_JSON_ENCODER,
         TestProject::for_source(src)
-            .add_module("gleam/option", "pub type Option(a) { Some(a) None }"),
+            .add_module("gleam/option", "pub type Option(a) { Some(a) None }")
+            .add_package_module("gleam_json", "gleam/json", "pub type Json"),
         find_position_of("type").to_selection()
     );
 }
@@ -6568,42 +6604,60 @@ fn map(list: List(a), fun: fn(a) -> b) -> List(b) { todo }
 
 #[test]
 fn generate_json_encoder_list_of_tuples() {
-    assert_code_action!(
-        GENERATE_JSON_ENCODER,
-        "
+    let src = "
 pub type Wibble {
   Wibble(values: List(#(Int, String)))
 }
-",
+";
+
+    assert_code_action!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type").to_selection()
     );
 }
 
 #[test]
 fn generate_json_encoder_for_multi_variant_type() {
-    assert_code_action!(
-        GENERATE_JSON_ENCODER,
-        "
+    let src = "
 pub type Wibble {
   Wibble(wibble: Int, next: Wibble)
   Wobble(wobble: Float, text: String, values: List(Bool))
 }
-",
+";
+
+    assert_code_action!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type").to_selection()
     );
 }
 
 #[test]
 fn generate_json_encoder_for_multi_variant_type_multi_word_name() {
-    assert_code_action!(
-        GENERATE_JSON_ENCODER,
-        "
+    let src = "
 pub type Wibble {
   OneTwoThree(wibble: Int, next: Wibble)
   FourFive(wobble: Float, text: String, values: List(Bool))
   SixSevenEight(one_two: Float)
 }
-",
+";
+
+    assert_code_action!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
         find_position_of("type").to_selection()
     );
 }
@@ -6715,11 +6769,30 @@ pub fn main() {
 
 #[test]
 fn no_code_action_to_generate_json_encoder_for_type_without_labels() {
+    let src = "
+pub type Wibble {
+  Wibble(Int, Int, String)
+}
+    ";
+
+    assert_no_code_actions!(
+        GENERATE_JSON_ENCODER,
+        TestProject::for_source(src).add_package_module(
+            "gleam_json",
+            "gleam/json",
+            "pub type Json"
+        ),
+        find_position_of("type").to_selection()
+    );
+}
+
+#[test]
+fn no_code_action_to_generate_json_encoder_without_gleam_json_dependency() {
     assert_no_code_actions!(
         GENERATE_JSON_ENCODER,
         "
 pub type Wibble {
-  Wibble(Int, Int, String)
+  Wibble(w: Int)
 }
 ",
         find_position_of("type").to_selection()