extension_host: Fix uploading dev extensions to the remote server (#21761)

Marshall Bowers and Conrad created

This PR fixes an issue where dev extensions were not working when
uploaded to the remote server.

The `extension.toml` for dev extensions may not contain all of the
information (such as the list of languages), as this is something that
we derive from the filesystem at packaging time. This meant that
uploading a dev extension that contained languages could have them
absent from the uploaded `extension.toml`.

For dev extensions we now upload a serialized version of the in-memory
extension manifest, which should have all of the information present.

Release Notes:

- SSH Remoting: Fixed an issue where some dev extensions would not work
after being uploaded to the remote server.

---------

Co-authored-by: Conrad <conrad@zed.dev>

Change summary

crates/extension_host/src/extension_host.rs | 52 +++++++++++++++++-----
1 file changed, 39 insertions(+), 13 deletions(-)

Detailed changes

crates/extension_host/src/extension_host.rs 🔗

@@ -32,7 +32,7 @@ use gpui::{
 };
 use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
 use language::{
-    LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage,
+    LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, Rope,
     QUERY_FILENAME_PREFIXES,
 };
 use node_runtime::NodeRuntime;
@@ -1387,6 +1387,7 @@ impl ExtensionStore {
     fn prepare_remote_extension(
         &mut self,
         extension_id: Arc<str>,
+        is_dev: bool,
         tmp_dir: PathBuf,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
@@ -1397,26 +1398,45 @@ impl ExtensionStore {
         };
         let fs = self.fs.clone();
         cx.background_executor().spawn(async move {
-            for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] {
-                if fs.is_file(&src_dir.join(well_known_path)).await {
-                    fs.copy_file(
-                        &src_dir.join(well_known_path),
-                        &tmp_dir.join(well_known_path),
-                        fs::CopyOptions::default(),
-                    )
-                    .await?
-                }
+            const EXTENSION_TOML: &str = "extension.toml";
+            const EXTENSION_WASM: &str = "extension.wasm";
+            const CONFIG_TOML: &str = "config.toml";
+
+            if is_dev {
+                let manifest_toml = toml::to_string(&loaded_extension.manifest)?;
+                fs.save(
+                    &tmp_dir.join(EXTENSION_TOML),
+                    &Rope::from(manifest_toml),
+                    language::LineEnding::Unix,
+                )
+                .await?;
+            } else {
+                fs.copy_file(
+                    &src_dir.join(EXTENSION_TOML),
+                    &tmp_dir.join(EXTENSION_TOML),
+                    fs::CopyOptions::default(),
+                )
+                .await?
+            }
+
+            if fs.is_file(&src_dir.join(EXTENSION_WASM)).await {
+                fs.copy_file(
+                    &src_dir.join(EXTENSION_WASM),
+                    &tmp_dir.join(EXTENSION_WASM),
+                    fs::CopyOptions::default(),
+                )
+                .await?
             }
 
             for language_path in loaded_extension.manifest.languages.iter() {
                 if fs
-                    .is_file(&src_dir.join(language_path).join("config.toml"))
+                    .is_file(&src_dir.join(language_path).join(CONFIG_TOML))
                     .await
                 {
                     fs.create_dir(&tmp_dir.join(language_path)).await?;
                     fs.copy_file(
-                        &src_dir.join(language_path).join("config.toml"),
-                        &tmp_dir.join(language_path).join("config.toml"),
+                        &src_dir.join(language_path).join(CONFIG_TOML),
+                        &tmp_dir.join(language_path).join(CONFIG_TOML),
                         fs::CopyOptions::default(),
                     )
                     .await?
@@ -1462,6 +1482,7 @@ impl ExtensionStore {
             this.update(cx, |this, cx| {
                 this.prepare_remote_extension(
                     missing_extension.id.clone().into(),
+                    missing_extension.dev,
                     tmp_dir.path().to_owned(),
                     cx,
                 )
@@ -1476,6 +1497,11 @@ impl ExtensionStore {
                 })?
                 .await?;
 
+            log::info!(
+                "Finished uploading extension {}",
+                missing_extension.clone().id
+            );
+
             client
                 .update(cx, |client, _cx| {
                     client.proto_client().request(proto::InstallExtension {