Repository: dprint/dprint (language: rust)

Issue:
Future improvements to plugin and editor interfaces
This is a tracking issue for possible improvements to the editor and plugin interfaces.

**All**

- maybe - the plugin should specify if it can handle range formatting in the "can format" message (ex. 0 = no, 1 = yes, 2 = yes & can range format)
- maybe - should support formatting multiple ranges

**Wasm Plugins**

- must - range formatting similar to process plugins
- must - cancellation token similar to process plugins
- must - multiple configs loaded per plugin similar to process plugins
- maybe - async host formatting and therefore async formatting (doesn't seem necessary?)
- must - updates from https://github.com/dprint/dprint/issues/716
- must - updates from https://github.com/dprint/dprint/issues/714
- must - Get entire format result out of memory at once.

Additional:

- Send log level on initialize so plugins can output with verbose logging.
- Send past version with upgrade message (if not already done)
- Logging with log level in wasm plugins
- Ability for plugins to tell if no_color and if a tty. Probably send on init
- Maybe ability for Wasm plugin to import another wasm plugin via Wasm imports?
- Wasm schema should not require instantiating a wasm module (it should be figured out by just analyzing the import names)
- Ability for plugins to do comparisons with language aware algorithms (ex. for sorting imports).


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.generate.ts b/.github/workflows/ci.generate.ts
index 05aa260d3..8672fdbab 100644
--- a/.github/workflows/ci.generate.ts
+++ b/.github/workflows/ci.generate.ts
@@ -155,6 +155,11 @@ const ci = {
           if: "matrix.config.run_tests == 'true' && startsWith(github.ref, 'refs/tags/')",
           run: "cargo build -p test-process-plugin --locked --target ${{matrix.config.target}} --release",
         },
+        {
+          name: "Clippy",
+          if: "matrix.config.target == 'x86_64-unknown-linux-gnu' && !startsWith(github.ref, 'refs/tags/')",
+          run: "cargo clippy",
+        },
         {
           name: "Build (Debug)",
           if: "matrix.config.cross != 'true' && !startsWith(github.ref, 'refs/tags/')",
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3d66441ec..9dc664799 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -90,6 +90,9 @@ jobs:
       - name: Build test plugins (Release)
         if: "matrix.config.run_tests == 'true' && startsWith(github.ref, 'refs/tags/')"
         run: "cargo build -p test-process-plugin --locked --target ${{matrix.config.target}} --release"
+      - name: Clippy
+        if: "matrix.config.target == 'x86_64-unknown-linux-gnu' && !startsWith(github.ref, 'refs/tags/')"
+        run: cargo clippy
       - name: Build (Debug)
         if: "matrix.config.cross != 'true' && !startsWith(github.ref, 'refs/tags/')"
         env:
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 43bfd2131..3c4ce4277 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -16,7 +16,7 @@ async_runtime = ["async-trait", "futures", "parking_lot", "tokio", "tokio-util"]
 formatting = ["bumpalo", "hashbrown", "rustc-hash", "unicode-width"]
 communication = ["crossbeam-channel", "async_runtime"]
 process = ["communication", "serde_json", "libc", "winapi"]
-wasm = []
+wasm = ["serde_json"]
 tracing = ["formatting"]
 
 [dependencies]
diff --git a/crates/core/src/configuration.rs b/crates/core/src/configuration.rs
index d4b64682f..060874f22 100644
--- a/crates/core/src/configuration.rs
+++ b/crates/core/src/configuration.rs
@@ -375,6 +375,35 @@ where
   }
 }
 
+pub fn get_nullable_vec<T: std::str::FromStr>(
+  config: &mut ConfigKeyMap,
+  key: &str,
+  get_nullable_value: impl Fn(ConfigKeyValue, usize, &mut Vec<ConfigurationDiagnostic>) -> Option<T>,
+  diagnostics: &mut Vec<ConfigurationDiagnostic>,
+) -> Option<Vec<T>> {
+  match config.shift_remove(key) {
+    Some(value) => match value {
+      ConfigKeyValue::Array(values) => {
+        let mut result = Vec::with_capacity(values.len());
+        for (i, value) in values.into_iter().enumerate() {
+          if let Some(value) = get_nullable_value(value, i, diagnostics) {
+            result.push(value);
+          }
+        }
+        Some(result)
+      }
+      _ => {
+        diagnostics.push(ConfigurationDiagnostic {
+          property_name: key.to_string(),
+          message: "Expected an array.".to_string(),
+        });
+        None
+      }
+    },
+    None => None,
+  }
+}
+
 /// If it exists, moves over the configuration value over from the old key
 /// to the new key and adds a diagnostic.
 pub fn handle_renamed_config_property(config: &mut ConfigKeyMap, old_key: &str, new_key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) {
diff --git a/crates/core/src/formatting/infinite_reevaluation_protection.rs b/crates/core/src/formatting/infinite_reevaluation_protection.rs
index 0f0f621b5..994fe2150 100644
--- a/crates/core/src/formatting/infinite_reevaluation_protection.rs
+++ b/crates/core/src/formatting/infinite_reevaluation_protection.rs
@@ -34,9 +34,13 @@ impl InfiniteReevaluationProtector {
       if next_count == MAX_COUNT {
         // only ever log this once per execution
         if !LOGGED.swap(true, Ordering::SeqCst) {
-          eprintln!(
-            "[dprint-core] A file exceeded the re-evaluation count and formatting stabilized at a random condition value. Please report this as a bug."
-          );
+          // todo: use awasm logging here instead
+          #[allow(clippy::print_stderr)]
+          {
+            eprintln!(
+              "[dprint-core] A file exceeded the re-evaluation count and formatting stabilized at a random condition value. Please report this as a bug."
+            );
+          }
         }
         false
       } else {
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index f035f8bf3..ec57734d2 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -1,4 +1,7 @@
 #![allow(clippy::bool_to_int_with_if)]
+#![deny(clippy::print_stderr)]
+#![deny(clippy::print_stdout)]
+#![deny(clippy::unused_async)]
 
 #[cfg(feature = "communication")]
 pub mod communication;
diff --git a/crates/core/src/plugins/plugin_handler.rs b/crates/core/src/plugins/plugin_handler.rs
index d0bd9d11c..452345e6d 100644
--- a/crates/core/src/plugins/plugin_handler.rs
+++ b/crates/core/src/plugins/plugin_handler.rs
@@ -1,8 +1,6 @@
 use anyhow::Result;
 use serde::Deserialize;
 use serde::Serialize;
-use std::path::PathBuf;
-use std::sync::Arc;
 
 #[cfg(feature = "async_runtime")]
 use crate::async_runtime::FutureExt;
@@ -13,12 +11,11 @@ use crate::configuration::ConfigKeyMap;
 use crate::configuration::ConfigKeyValue;
 use crate::configuration::ConfigurationDiagnostic;
 use crate::configuration::GlobalConfiguration;
-use crate::configuration::ResolveConfigurationResult;
 use crate::plugins::PluginInfo;
 
 use super::FileMatchingInfo;
 
-pub trait CancellationToken: Send + Sync {
+pub trait CancellationToken: Send + Sync + std::fmt::Debug {
   fn is_cancelled(&self) -> bool;
   #[cfg(feature = "async_runtime")]
   fn wait_cancellation(&self) -> LocalBoxFuture<'static, ()>;
@@ -37,6 +34,7 @@ impl CancellationToken for tokio_util::sync::CancellationToken {
 }
 
 /// A cancellation token that always says it's not cancelled.
+#[derive(Debug)]
 pub struct NullCancellationToken;
 
 impl CancellationToken for NullCancellationToken {
@@ -72,14 +70,34 @@ impl std::error::Error for CriticalFormatError {
   }
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CheckConfigUpdatesMessage {
+  /// dprint versions < 0.47 won't have this set
+  #[serde(default)]
+  pub old_version: Option<String>,
+  pub config: ConfigKeyMap,
+}
+
 #[cfg(feature = "process")]
+#[derive(Debug)]
 pub struct HostFormatRequest {
-  pub file_path: PathBuf,
+  pub file_path: std::path::PathBuf,
   pub file_bytes: Vec<u8>,
   /// Range to format.
   pub range: FormatRange,
   pub override_config: ConfigKeyMap,
-  pub token: Arc<dyn CancellationToken>,
+  pub token: std::sync::Arc<dyn CancellationToken>,
+}
+
+#[cfg(feature = "wasm")]
+#[derive(Debug)]
+pub struct SyncHostFormatRequest<'a> {
+  pub file_path: &'a std::path::Path,
+  pub file_bytes: &'a [u8],
+  /// Range to format.
+  pub range: FormatRange,
+  pub override_config: &'a ConfigKeyMap,
 }
 
 /// `Ok(Some(text))` - Changes due to the format.
@@ -87,8 +105,14 @@ pub struct HostFormatRequest {
 /// `Err(err)` - Error formatting. Use a `CriticalError` to signal that the plugin can't recover.
 pub type FormatResult = Result<Option<Vec<u8>>>;
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RawFormatConfig {
+  pub plugin: ConfigKeyMap,
+  pub global: GlobalConfiguration,
+}
+
 /// A unique configuration id used for formatting.
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct FormatConfigId(u32);
 
 impl std::fmt::Display for FormatConfigId {
@@ -111,14 +135,26 @@ impl FormatConfigId {
   }
 }
 
+#[cfg(feature = "process")]
 pub struct FormatRequest<TConfiguration> {
-  pub file_path: PathBuf,
+  pub file_path: std::path::PathBuf,
   pub file_bytes: Vec<u8>,
   pub config_id: FormatConfigId,
-  pub config: Arc<TConfiguration>,
+  pub config: std::sync::Arc<TConfiguration>,
   /// Range to format.
   pub range: FormatRange,
-  pub token: Arc<dyn CancellationToken>,
+  pub token: std::sync::Arc<dyn CancellationToken>,
+}
+
+#[cfg(feature = "wasm")]
+pub struct SyncFormatRequest<'a, TConfiguration> {
+  pub file_path: &'a std::path::Path,
+  pub file_bytes: Vec<u8>,
+  pub config_id: FormatConfigId,
+  pub config: &'a TConfiguration,
+  /// Range to format.
+  pub range: FormatRange,
+  pub token: &'a dyn CancellationToken,
 }
 
 #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
@@ -193,7 +229,7 @@ pub trait AsyncPluginHandler: 'static {
   async fn resolve_config(&self, config: ConfigKeyMap, global_config: GlobalConfiguration) -> PluginResolveConfigurationResult<Self::Configuration>;
   /// Updates the config key map. This will be called after the CLI has upgraded the
   /// plugin in `dprint config update`.
-  async fn check_config_updates(&self, _plugin_config: ConfigKeyMap) -> Result<Vec<ConfigChange>> {
+  async fn check_config_updates(&self, _message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
     Ok(Vec::new())
   }
   /// Formats the provided file text based on the provided file path and configuration.
@@ -204,30 +240,18 @@ pub trait AsyncPluginHandler: 'static {
   ) -> FormatResult;
 }
 
-#[cfg(feature = "wasm")]
-#[derive(Clone, Serialize, serde::Deserialize, Debug, PartialEq, Eq)]
-pub struct SyncPluginInfo {
-  #[serde(flatten)]
-  pub info: PluginInfo,
-  #[serde(flatten)]
-  pub file_matching: FileMatchingInfo,
-}
-
 /// Trait for implementing a Wasm plugin.
 #[cfg(feature = "wasm")]
 pub trait SyncPluginHandler<TConfiguration: Clone + serde::Serialize> {
   /// Resolves configuration based on the provided config map and global configuration.
-  fn resolve_config(&mut self, config: ConfigKeyMap, global_config: &GlobalConfiguration) -> ResolveConfigurationResult<TConfiguration>;
+  fn resolve_config(&mut self, config: ConfigKeyMap, global_config: &GlobalConfiguration) -> PluginResolveConfigurationResult<TConfiguration>;
   /// Gets the plugin's plugin info.
-  fn plugin_info(&mut self) -> SyncPluginInfo;
+  fn plugin_info(&mut self) -> PluginInfo;
   /// Gets the plugin's license text.
   fn license_text(&mut self) -> String;
+  /// Updates the config key map. This will be called after the CLI has upgraded the
+  /// plugin in `dprint config update`.
+  fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>>;
   /// Formats the provided file text based on the provided file path and configuration.
-  fn format(
-    &mut self,
-    file_path: &std::path::Path,
-    file_bytes: Vec<u8>,
-    config: &TConfiguration,
-    format_with_host: impl FnMut(&std::path::Path, Vec<u8>, &ConfigKeyMap) -> FormatResult,
-  ) -> FormatResult;
+  fn format(&mut self, request: SyncFormatRequest<TConfiguration>, format_with_host: impl FnMut(SyncHostFormatRequest) -> FormatResult) -> FormatResult;
 }
diff --git a/crates/core/src/plugins/process/communicator.rs b/crates/core/src/plugins/process/communicator.rs
index bdcd566bc..6573e818b 100644
--- a/crates/core/src/plugins/process/communicator.rs
+++ b/crates/core/src/plugins/process/communicator.rs
@@ -280,8 +280,7 @@ impl ProcessPluginCommunicator {
     self.send_receiving_data(MessageBody::GetConfigDiagnostics(config_id)).await
   }
 
-  pub async fn check_config_updates(&self, plugin_config: ConfigKeyMap) -> Result<Vec<ConfigChange>> {
-    let message = CheckConfigUpdatesMessageBody { config: plugin_config };
+  pub async fn check_config_updates(&self, message: &CheckConfigUpdatesMessageBody) -> Result<Vec<ConfigChange>> {
     let bytes = serde_json::to_vec(&message)?;
     let response: CheckConfigUpdatesResponseBody = self.send_receiving_data(MessageBody::CheckConfigUpdates(bytes)).await?;
     Ok(response.changes)
diff --git a/crates/core/src/plugins/process/message_processor.rs b/crates/core/src/plugins/process/message_processor.rs
index c0913a7f1..c6bea78d2 100644
--- a/crates/core/src/plugins/process/message_processor.rs
+++ b/crates/core/src/plugins/process/message_processor.rs
@@ -159,7 +159,7 @@ pub async fn handle_process_stdio_messages<THandler: AsyncPluginHandler>(handler
             async {
               let message_body = serde_json::from_slice::<CheckConfigUpdatesMessageBody>(&body_bytes)
                 .with_context(|| "Could not deserialize the check config updates message body.".to_string())?;
-              let changes = handler.check_config_updates(message_body.config).await?;
+              let changes = handler.check_config_updates(message_body).await?;
               let response = CheckConfigUpdatesResponseBody { changes };
               let data = serde_json::to_vec(&response)?;
               Ok(MessageBody::DataResponse(ResponseBody { message_id: message.id, data }))
@@ -236,7 +236,10 @@ pub async fn handle_process_stdio_messages<THandler: AsyncPluginHandler>(handler
           if let Some(sender) = context.format_host_senders.take(body.message_id) {
             sender.send(Err(anyhow!("{}", text))).unwrap();
           } else {
-            eprintln!("Received error from CLI. {}", text);
+            #[allow(clippy::print_stderr)]
+            {
+              eprintln!("Received error from CLI. {}", text);
+            }
           }
         }
         MessageBody::FormatResponse(body) => {
diff --git a/crates/core/src/plugins/process/messages.rs b/crates/core/src/plugins/process/messages.rs
index b3497006d..80f292ffe 100644
--- a/crates/core/src/plugins/process/messages.rs
+++ b/crates/core/src/plugins/process/messages.rs
@@ -8,7 +8,6 @@ use serde::Deserialize;
 use serde::Serialize;
 
 use crate::communication::Message;
-use crate::configuration::ConfigKeyMap;
 use crate::plugins::ConfigChange;
 use crate::plugins::FormatConfigId;
 use crate::plugins::FormatRange;
@@ -294,11 +293,7 @@ pub struct RegisterConfigMessageBody {
   pub plugin_config: Vec<u8>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct CheckConfigUpdatesMessageBody {
-  pub config: ConfigKeyMap,
-}
+pub type CheckConfigUpdatesMessageBody = crate::plugins::CheckConfigUpdatesMessage;
 
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
diff --git a/crates/core/src/plugins/wasm/mod.rs b/crates/core/src/plugins/wasm/mod.rs
index c967c83b6..84a5a18ad 100644
--- a/crates/core/src/plugins/wasm/mod.rs
+++ b/crates/core/src/plugins/wasm/mod.rs
@@ -1,16 +1,98 @@
 /// The plugin system schema version that is incremented
 /// when there are any breaking changes.
-pub const PLUGIN_SYSTEM_SCHEMA_VERSION: u32 = 3;
+pub const PLUGIN_SYSTEM_SCHEMA_VERSION: u32 = 4;
+
+#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+extern "C" {
+  fn fd_write(fd: i32, iovs: *const crate::plugins::wasm::Iovec, iovs_len: i32, nwritten: *mut i32) -> i32;
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(tag = "kind", content = "data")]
+pub enum JsonResponse {
+  #[serde(rename = "ok")]
+  Ok(serde_json::Value),
+  #[serde(rename = "err")]
+  Err(String),
+}
+
+pub struct WasiPrintFd(pub i32);
+
+impl std::io::Write for WasiPrintFd {
+  fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+    {
+      let iovec = Iovec {
+        buf: buf.as_ptr(),
+        buf_len: buf.len() as u32,
+      };
+      let mut nwritten: i32 = 0;
+      let result = unsafe { fd_write(self.0, &iovec, 1, &mut nwritten) };
+      if result == 0 {
+        Ok(nwritten as usize)
+      } else {
+        Err(std::io::Error::from_raw_os_error(result))
+      }
+    }
+    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
+    {
+      let size = buf.len();
+      match self.0 {
+        0 => std::io::stdout().write_all(buf)?,
+        1 => std::io::stderr().write_all(buf)?,
+        _ => return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)),
+      }
+      Ok(size)
+    }
+  }
+
+  fn flush(&mut self) -> std::io::Result<()> {
+    Ok(())
+  }
+}
+
+#[repr(C)]
+pub struct Iovec {
+  pub buf: *const u8,
+  pub buf_len: u32,
+}
 
 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
 pub mod macros {
   #[macro_export]
   macro_rules! generate_plugin_code {
     ($wasm_plugin_struct:ident, $wasm_plugin_creation:expr) => {
+      struct RefStaticCell<T: Default>(std::cell::OnceCell<StaticCell<T>>);
+
+      impl<T: Default> RefStaticCell<T> {
+        pub const fn new() -> Self {
+          RefStaticCell(std::cell::OnceCell::new())
+        }
+
+        #[allow(clippy::mut_from_ref)]
+        unsafe fn get(&self) -> &mut T {
+          let inner = self.0.get_or_init(Default::default);
+          inner.get()
+        }
+
+        fn replace(&self, value: T) -> T {
+          let inner = self.0.get_or_init(Default::default);
+          inner.replace(value)
+        }
+      }
+
+      unsafe impl<T: Default> Sync for RefStaticCell<T> {}
+
       // This is ok to do because Wasm plugins are only ever executed on a single thread.
       // https://github.com/rust-lang/rust/issues/53639#issuecomment-790091647
       struct StaticCell<T>(std::cell::UnsafeCell<T>);
 
+      impl<T: Default> Default for StaticCell<T> {
+        fn default() -> Self {
+          StaticCell(std::cell::UnsafeCell::new(T::default()))
+        }
+      }
+
       impl<T> StaticCell<T> {
         const fn new(value: T) -> Self {
           StaticCell(std::cell::UnsafeCell::new(value))
@@ -32,37 +114,51 @@ pub mod macros {
 
       // HOST FORMATTING
 
-      fn format_with_host(
-        file_path: &std::path::Path,
-        file_bytes: Vec<u8>,
-        override_config: &dprint_core::configuration::ConfigKeyMap,
-      ) -> anyhow::Result<Option<Vec<u8>>> {
+      #[link(wasm_import_module = "dprint")]
+      extern "C" {
+        fn host_has_cancelled() -> i32;
+      }
+
+      fn format_with_host(request: dprint_core::plugins::SyncHostFormatRequest) -> anyhow::Result<Option<Vec<u8>>> {
+        use std::borrow::Cow;
+
         #[link(wasm_import_module = "dprint")]
         extern "C" {
-          fn host_clear_bytes(length: u32);
-          fn host_read_buffer(pointer: u32, length: u32);
-          fn host_write_buffer(pointer: u32, offset: u32, length: u32);
-          fn host_take_file_path();
-          fn host_take_override_config();
-          fn host_format() -> u8;
+          fn host_write_buffer(pointer: *const u8);
+          fn host_format(
+            file_path_ptr: *const u8,
+            file_path_len: u32,
+            start_range: u32,
+            end_range: u32,
+            override_config_ptr: *const u8,
+            override_config_len: u32,
+            file_text_ptr: *const u8,
+            file_text_len: u32,
+          ) -> u8;
           fn host_get_formatted_text() -> u32;
           fn host_get_error_text() -> u32;
         }
 
-        if !override_config.is_empty() {
-          send_string_to_host(serde_json::to_string(override_config).unwrap());
-          unsafe {
-            host_take_override_config();
-          }
-        }
-
-        send_string_to_host(file_path.to_string_lossy().to_string());
-        unsafe {
-          host_take_file_path();
-        }
-        send_bytes_to_host(file_bytes);
-
-        return match unsafe { host_format() } {
+        let file_path = request.file_path.to_string_lossy();
+        let override_config = if !request.override_config.is_empty() {
+          Cow::Owned(serde_json::to_string(request.override_config).unwrap())
+        } else {
+          Cow::Borrowed("")
+        };
+        let range = request.range.unwrap_or(0..request.file_bytes.len());
+
+        return match unsafe {
+          host_format(
+            file_path.as_ptr(),
+            file_path.len() as u32,
+            range.start as u32,
+            range.end as u32,
+            override_config.as_ptr(),
+            override_config.len() as u32,
+            request.file_bytes.as_ptr(),
+            request.file_bytes.len() as u32,
+          )
+        } {
           0 => {
             // no change
             Ok(None)
@@ -82,40 +178,15 @@ pub mod macros {
           value => panic!("unknown host format value: {}", value),
         };
 
-        fn send_string_to_host(text: String) {
-          send_bytes_to_host(text.into_bytes())
-        }
-
-        fn send_bytes_to_host(bytes: Vec<u8>) {
-          let mut index = 0;
-          let length = set_shared_bytes(bytes);
-          unsafe {
-            host_clear_bytes(length as u32);
-          }
-          while index < length {
-            let read_count = std::cmp::min(length - index, WASM_MEMORY_BUFFER_SIZE);
-            set_buffer_with_shared_bytes(index, read_count);
-            unsafe {
-              host_read_buffer(get_wasm_memory_buffer() as u32, read_count as u32);
-            }
-            index += read_count;
-          }
-        }
-
         fn get_string_from_host(length: u32) -> String {
           String::from_utf8(get_bytes_from_host(length)).unwrap()
         }
 
         fn get_bytes_from_host(length: u32) -> Vec<u8> {
           let mut index: u32 = 0;
-          clear_shared_bytes(length as usize);
-          while index < length {
-            let read_count = std::cmp::min(length - index, WASM_MEMORY_BUFFER_SIZE as u32);
-            unsafe {
-              host_write_buffer(get_wasm_memory_buffer() as u32, index, read_count);
-            }
-            add_to_shared_bytes_from_buffer(read_count as usize);
-            index += read_count;
+          let ptr = clear_shared_bytes(length as usize);
+          unsafe {
+            host_write_buffer(ptr);
           }
           take_from_shared_bytes()
         }
@@ -143,19 +214,46 @@ pub mod macros {
       }
 
       #[no_mangle]
-      pub fn format() -> u8 {
-        ensure_initialized();
+      pub fn format(config_id: u32) -> u8 {
+        format_inner(config_id, None)
+      }
+
+      #[no_mangle]
+      pub fn format_range(config_id: u32, range_start: u32, range_end: u32) -> u8 {
+        format_inner(config_id, Some(range_start as usize..range_end as usize))
+      }
+
+      fn format_inner(config_id: u32, range: dprint_core::plugins::FormatRange) -> u8 {
+        #[derive(Debug)]
+        struct HostCancellationToken;
+
+        impl dprint_core::plugins::CancellationToken for HostCancellationToken {
+          fn is_cancelled(&self) -> bool {
+            unsafe { host_has_cancelled() == 1 }
+          }
+        }
+
+        let config_id = dprint_core::plugins::FormatConfigId::from_raw(config_id);
+        ensure_initialized(config_id);
         let config = unsafe {
           if let Some(override_config) = OVERRIDE_CONFIG.get().take() {
-            std::borrow::Cow::Owned(create_resolved_config_result(override_config).config)
+            std::borrow::Cow::Owned(create_resolved_config_result(config_id, override_config).config)
           } else {
-            std::borrow::Cow::Borrowed(&get_resolved_config_result().config)
+            std::borrow::Cow::Borrowed(&get_resolved_config_result(config_id).config)
           }
         };
         let file_path = unsafe { FILE_PATH.get().take().expect("Expected the file path to be set.") };
         let file_bytes = take_from_shared_bytes();
 
-        let formatted_text = unsafe { WASM_PLUGIN.get().format(&file_path, file_bytes, &config, format_with_host) };
+        let request = dprint_core::plugins::SyncFormatRequest::<Configuration> {
+          file_path: &file_path,
+          file_bytes,
+          config: &config,
+          config_id,
+          range,
+          token: &HostCancellationToken,
+        };
+        let formatted_text = unsafe { WASM_PLUGIN.get().format(request, format_with_host) };
         match formatted_text {
           Ok(None) => {
             0 // no change
@@ -185,7 +283,9 @@ pub mod macros {
 
       // INFORMATION & CONFIGURATION
 
-      static RESOLVE_CONFIGURATION_RESULT: StaticCell<Option<dprint_core::configuration::ResolveConfigurationResult<Configuration>>> = StaticCell::new(None);
+      static RESOLVE_CONFIGURATION_RESULT: RefStaticCell<
+        std::collections::HashMap<dprint_core::plugins::FormatConfigId, dprint_core::plugins::PluginResolveConfigurationResult<Configuration>>,
+      > = RefStaticCell::new();
 
       #[no_mangle]
       pub fn get_plugin_info() -> usize {
@@ -201,121 +301,121 @@ pub mod macros {
       }
 
       #[no_mangle]
-      pub fn get_resolved_config() -> usize {
-        let bytes = serde_json::to_vec(&get_resolved_config_result().config).unwrap();
+      pub fn get_resolved_config(config_id: u32) -> usize {
+        let config_id = dprint_core::plugins::FormatConfigId::from_raw(config_id);
+        let bytes = serde_json::to_vec(&get_resolved_config_result(config_id).config).unwrap();
         set_shared_bytes(bytes)
       }
 
       #[no_mangle]
-      pub fn get_config_diagnostics() -> usize {
-        let bytes = serde_json::to_vec(&get_resolved_config_result().diagnostics).unwrap();
+      pub fn get_config_diagnostics(config_id: u32) -> usize {
+        let config_id = dprint_core::plugins::FormatConfigId::from_raw(config_id);
+        let bytes = serde_json::to_vec(&get_resolved_config_result(config_id).diagnostics).unwrap();
         set_shared_bytes(bytes)
       }
 
-      // for clearing the config in the playground
       #[no_mangle]
-      pub fn reset_config() {
-        unsafe {
-          RESOLVE_CONFIGURATION_RESULT.get().take();
-        }
+      pub fn get_config_file_matching(config_id: u32) -> usize {
+        let config_id = dprint_core::plugins::FormatConfigId::from_raw(config_id);
+        let bytes = serde_json::to_vec(&get_resolved_config_result(config_id).file_matching).unwrap();
+        set_shared_bytes(bytes)
       }
 
-      fn get_resolved_config_result<'a>() -> &'a dprint_core::configuration::ResolveConfigurationResult<Configuration> {
+      fn get_resolved_config_result<'a>(
+        config_id: dprint_core::plugins::FormatConfigId,
+      ) -> &'a dprint_core::plugins::PluginResolveConfigurationResult<Configuration> {
         unsafe {
-          ensure_initialized();
-          return RESOLVE_CONFIGURATION_RESULT.get().as_ref().unwrap();
+          ensure_initialized(config_id);
+          return RESOLVE_CONFIGURATION_RESULT.get().get(&config_id).unwrap();
         }
       }
 
-      fn ensure_initialized() {
+      fn ensure_initialized(config_id: dprint_core::plugins::FormatConfigId) {
         unsafe {
-          if RESOLVE_CONFIGURATION_RESULT.get().is_none() {
-            let config_result = create_resolved_config_result(dprint_core::configuration::ConfigKeyMap::new());
-            RESOLVE_CONFIGURATION_RESULT.get().replace(config_result);
+          if !RESOLVE_CONFIGURATION_RESULT.get().contains_key(&config_id) {
+            let config_result = create_resolved_config_result(config_id, dprint_core::configuration::ConfigKeyMap::new());
+            RESOLVE_CONFIGURATION_RESULT.get().insert(config_id, config_result);
           }
         }
       }
 
       fn create_resolved_config_result(
+        config_id: dprint_core::plugins::FormatConfigId,
         override_config: dprint_core::configuration::ConfigKeyMap,
-      ) -> dprint_core::configuration::ResolveConfigurationResult<Configuration> {
+      ) -> dprint_core::plugins::PluginResolveConfigurationResult<Configuration> {
         unsafe {
-          if let Some(global_config) = GLOBAL_CONFIG.get().as_ref() {
-            if let Some(plugin_config) = PLUGIN_CONFIG.get().as_ref() {
-              let mut plugin_config = plugin_config.clone();
-              for (key, value) in override_config {
-                plugin_config.insert(key, value);
-              }
-              return WASM_PLUGIN.get().resolve_config(plugin_config, global_config);
+          if let Some(config) = UNRESOLVED_CONFIG.get().get(&config_id) {
+            let mut plugin_config = config.plugin.clone();
+            for (key, value) in override_config {
+              plugin_config.insert(key, value);
             }
+            return WASM_PLUGIN.get().resolve_config(plugin_config, &config.global);
           }
         }
 
-        panic!("Plugin must have global config and plugin config set before use.");
+        panic!("Plugin must have config set before use (id: {:?}).", config_id);
       }
 
       // INITIALIZATION
 
-      static GLOBAL_CONFIG: StaticCell<Option<dprint_core::configuration::GlobalConfiguration>> = StaticCell::new(None);
-      static PLUGIN_CONFIG: StaticCell<Option<dprint_core::configuration::ConfigKeyMap>> = StaticCell::new(None);
+      static UNRESOLVED_CONFIG: RefStaticCell<std::collections::HashMap<dprint_core::plugins::FormatConfigId, dprint_core::plugins::RawFormatConfig>> =
+        RefStaticCell::new();
 
       #[no_mangle]
-      pub fn set_global_config() {
+      pub fn register_config(config_id: u32) {
+        let config_id = dprint_core::plugins::FormatConfigId::from_raw(config_id);
         let bytes = take_from_shared_bytes();
-        let global_config: dprint_core::configuration::GlobalConfiguration = serde_json::from_slice(&bytes).unwrap();
+        let config: dprint_core::plugins::RawFormatConfig = serde_json::from_slice(&bytes).unwrap();
         unsafe {
-          GLOBAL_CONFIG.get().replace(global_config);
-          RESOLVE_CONFIGURATION_RESULT.get().take(); // clear
+          UNRESOLVED_CONFIG.get().insert(config_id, config);
+          RESOLVE_CONFIGURATION_RESULT.get().remove(&config_id); // clear
         }
       }
 
       #[no_mangle]
-      pub fn set_plugin_config() {
-        let bytes = take_from_shared_bytes();
-        let plugin_config: dprint_core::configuration::ConfigKeyMap = serde_json::from_slice(&bytes).unwrap();
+      pub fn release_config(config_id: u32) {
+        let config_id = dprint_core::plugins::FormatConfigId::from_raw(config_id);
         unsafe {
-          PLUGIN_CONFIG.get().replace(plugin_config);
-          RESOLVE_CONFIGURATION_RESULT.get().take(); // clear
+          UNRESOLVED_CONFIG.get().remove(&config_id);
+          RESOLVE_CONFIGURATION_RESULT.get().remove(&config_id);
         }
       }
 
-      // LOW LEVEL SENDING AND RECEIVING
-
-      const WASM_MEMORY_BUFFER_SIZE: usize = 4 * 1024;
-      static mut WASM_MEMORY_BUFFER: [u8; WASM_MEMORY_BUFFER_SIZE] = [0; WASM_MEMORY_BUFFER_SIZE];
-      static SHARED_BYTES: StaticCell<Vec<u8>> = StaticCell::new(Vec::new());
-
       #[no_mangle]
-      pub fn get_plugin_schema_version() -> u32 {
-        dprint_core::plugins::wasm::PLUGIN_SYSTEM_SCHEMA_VERSION
-      }
+      pub fn check_config_updates() -> usize {
+        fn try_check_config_updates(bytes: &[u8]) -> anyhow::Result<serde_json::Value> {
+          let message: dprint_core::plugins::CheckConfigUpdatesMessage = serde_json::from_slice(&bytes)?;
+          let result = unsafe { WASM_PLUGIN.get().check_config_updates(message) }?;
+          Ok(serde_json::to_value(&result)?)
+        }
 
-      #[no_mangle]
-      pub fn get_wasm_memory_buffer() -> *const u8 {
-        unsafe { WASM_MEMORY_BUFFER.as_ptr() }
+        let bytes = take_from_shared_bytes();
+        let bytes = serde_json::to_vec(&match try_check_config_updates(&bytes) {
+          Ok(value) => dprint_core::plugins::wasm::JsonResponse::Ok(value),
+          Err(err) => dprint_core::plugins::wasm::JsonResponse::Err(err.to_string()),
+        })
+        .unwrap();
+        set_shared_bytes(bytes)
       }
 
-      #[no_mangle]
-      pub fn get_wasm_memory_buffer_size() -> usize {
-        WASM_MEMORY_BUFFER_SIZE
-      }
+      // LOW LEVEL SENDING AND RECEIVING
+
+      static SHARED_BYTES: StaticCell<Vec<u8>> = StaticCell::new(Vec::new());
 
       #[no_mangle]
-      pub fn add_to_shared_bytes_from_buffer(length: usize) {
-        unsafe { SHARED_BYTES.get().extend(&WASM_MEMORY_BUFFER[..length]) }
+      pub fn dprint_plugin_version_4() -> u32 {
+        dprint_core::plugins::wasm::PLUGIN_SYSTEM_SCHEMA_VERSION
       }
 
       #[no_mangle]
-      pub fn set_buffer_with_shared_bytes(offset: usize, length: usize) {
-        unsafe {
-          let bytes = &SHARED_BYTES.get()[offset..(offset + length)];
-          WASM_MEMORY_BUFFER[..length].copy_from_slice(bytes);
-        }
+      pub fn get_shared_bytes_ptr() -> *const u8 {
+        unsafe { SHARED_BYTES.get().as_ptr() }
       }
 
       #[no_mangle]
-      pub fn clear_shared_bytes(capacity: usize) {
-        SHARED_BYTES.replace(Vec::with_capacity(capacity));
+      pub fn clear_shared_bytes(size: usize) -> *const u8 {
+        SHARED_BYTES.replace(vec![0; size]);
+        unsafe { SHARED_BYTES.get().as_ptr() }
       }
 
       fn take_string_from_shared_bytes() -> String {
diff --git a/crates/dprint/src/commands/config.rs b/crates/dprint/src/commands/config.rs
index 5775c2be5..3f3f40042 100644
--- a/crates/dprint/src/commands/config.rs
+++ b/crates/dprint/src/commands/config.rs
@@ -4,6 +4,7 @@ use anyhow::Context;
 use anyhow::Error;
 use anyhow::Result;
 use dprint_core::async_runtime::future;
+use dprint_core::plugins;
 use std::collections::HashMap;
 use std::collections::HashSet;
 use std::path::PathBuf;
@@ -14,6 +15,7 @@ use crate::arg_parser::CliArgs;
 use crate::arg_parser::FilePatternArgs;
 use crate::configuration::get_init_config_file_text;
 use crate::configuration::*;
+use crate::environment::CanonicalizedPathBuf;
 use crate::environment::Environment;
 use crate::plugins::read_info_file;
 use crate::plugins::read_update_url;
@@ -104,7 +106,7 @@ pub async fn add_plugin_config_file<TEnvironment: Environment>(
                   let new_reference = plugin.as_source_reference()?;
                   let file_text = update_plugin_in_config(
                     &file_text,
-                    PluginUpdateInfo {
+                    &PluginUpdateInfo {
                       name: config_plugin.info().name.to_string(),
                       old_version: config_plugin.info().version.to_string(),
                       old_reference: config_plugin_reference,
@@ -190,6 +192,7 @@ pub async fn update_plugins_config_file<TEnvironment: Environment>(
   };
   let scopes = resolve_plugins_scope_and_paths(args, &file_pattern_args, environment, plugin_resolver).await?;
   let mut plugin_responses = HashMap::new();
+  let mut updates_per_scope = HashMap::with_capacity(scopes.len());
   for (i, scope) in scopes.into_iter().enumerate() {
     let is_main_config = i == 0;
     let Some(config) = &scope.scope.config else {
@@ -206,6 +209,7 @@ pub async fn update_plugins_config_file<TEnvironment: Environment>(
     let mut file_text = environment.read_file(config_path)?;
     let plugins_to_update = get_plugins_to_update(environment, plugin_resolver, config.plugins.clone()).await?;
 
+    let mut updated_plugins = Vec::with_capacity(plugins_to_update.len());
     for result in plugins_to_update {
       match result {
         Ok(info) => {
@@ -240,7 +244,8 @@ pub async fn update_plugins_config_file<TEnvironment: Environment>(
               },
               info.new_version
             );
-            file_text = update_plugin_in_config(&file_text, info);
+            file_text = update_plugin_in_config(&file_text, &info);
+            updated_plugins.push(info);
           }
         }
         Err(err_info) => {
@@ -249,12 +254,14 @@ pub async fn update_plugins_config_file<TEnvironment: Environment>(
       }
     }
 
+    updates_per_scope.insert(config_path.clone(), updated_plugins);
+
     environment.write_file(config_path, &file_text)?;
   }
 
   // now resolve the plugins again in every scope and run their config updates
 
-  run_plugin_config_updates(environment, args, &file_pattern_args, plugin_resolver)
+  run_plugin_config_updates(environment, args, &file_pattern_args, plugin_resolver, &updates_per_scope)
     .await
     .with_context(|| "Failed running plugin config updates.".to_string())?;
 
@@ -266,6 +273,7 @@ async fn run_plugin_config_updates<TEnvironment: Environment>(
   args: &CliArgs,
   file_pattern_args: &FilePatternArgs,
   plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
+  updates_per_scope: &HashMap<CanonicalizedPathBuf, Vec<PluginUpdateInfo>>,
 ) -> Result<()> {
   let scopes = resolve_plugins_scope_and_paths(args, file_pattern_args, environment, plugin_resolver).await?;
   for scope in scopes.into_iter() {
@@ -278,6 +286,15 @@ async fn run_plugin_config_updates<TEnvironment: Environment>(
         continue;
       }
     };
+    let updated_plugins = match updates_per_scope.get(config_path) {
+      Some(updates) => updates,
+      None => {
+        continue;
+      }
+    };
+    if updated_plugins.is_empty() {
+      continue;
+    }
     let mut file_text = environment.read_file(config_path)?;
     let config_map = match deserialize_config_raw(&file_text) {
       Ok(map) => map,
@@ -288,6 +305,12 @@ async fn run_plugin_config_updates<TEnvironment: Environment>(
     };
     let mut all_diagnostics = Vec::new();
     for plugin in scope.scope.plugins.values() {
+      let Some(update_info) = updated_plugins
+        .iter()
+        .find(|info| info.name == plugin.info().name && info.new_version == plugin.info().version)
+      else {
+        continue;
+      };
       log_debug!(environment, "Updating for {}", plugin.name());
       let config_key = &plugin.info().config_key;
       let Some(plugin_config) = config_map.get(config_key).and_then(|c| c.as_object()).cloned() else {
@@ -301,7 +324,13 @@ async fn run_plugin_config_updates<TEnvironment: Environment>(
         }
       };
 
-      let changes = match initialized_plugin.check_config_updates(plugin_config).await {
+      let changes = match initialized_plugin
+        .check_config_updates(plugins::CheckConfigUpdatesMessage {
+          old_version: Some(update_info.old_version.clone()),
+          config: plugin_config,
+        })
+        .await
+      {
         Ok(changes) => changes,
         Err(err) => {
           log_warn!(environment, "Failed updating {}. {:#}", plugin.name(), err);
@@ -473,7 +502,6 @@ mod test {
   use crate::environment::TestEnvironment;
   use crate::environment::TestEnvironmentBuilder;
   use crate::environment::TestInfoFilePlugin;
-  use crate::test_helpers;
   use crate::test_helpers::get_test_wasm_plugin_checksum;
   use crate::test_helpers::run_test_cli;
   use crate::test_helpers::TestProcessPluginFile;
@@ -596,8 +624,8 @@ mod test {
 
   #[test]
   fn config_add() {
-    let old_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
-    let new_wasm_url = "https://plugins.dprint.dev/test-plugin-2.wasm".to_string();
+    let old_wasm_url = "https://plugins.dprint.dev/test-plugin-0.1.0.wasm".to_string();
+    let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
     let old_ps_checksum = OLD_PROCESS_PLUGIN_FILE.checksum();
     let old_ps_url = format!("https://plugins.dprint.dev/test-process.json@{}", old_ps_checksum);
     let new_ps_url = "https://plugins.dprint.dev/test-plugin-3.json".to_string();
@@ -784,7 +812,7 @@ mod test {
 
   #[test]
   fn config_update_should_always_upgrade_to_latest_plugins() {
-    let new_wasm_url = "https://plugins.dprint.dev/test-plugin-2.wasm".to_string();
+    let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
     // test all the process plugin combinations
     let new_ps_url = "https://plugins.dprint.dev/test-plugin-3.json".to_string();
     let new_ps_url_with_checksum = format!("{}@{}", new_ps_url, NEW_PROCESS_PLUGIN_FILE.checksum());
@@ -846,7 +874,7 @@ mod test {
       expected_logs: vec![
         "Updating test-plugin 0.1.0 to 0.2.0...".to_string(),
         "Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
-        "Compiling https://plugins.dprint.dev/test-plugin-2.wasm".to_string(),
+        "Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
         "Extracting zip for test-process-plugin".to_string(),
       ],
       expected_urls: vec![new_wasm_url.clone(), new_ps_url_with_checksum.clone()],
@@ -858,10 +886,10 @@ mod test {
 
   #[test]
   fn config_update_should_upgrade_to_latest_plugins() {
-    let new_wasm_url = "https://plugins.dprint.dev/test-plugin-2.wasm".to_string();
+    let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
     let new_wasm_url_with_checksum = format!("{}@{}", new_wasm_url, get_test_wasm_plugin_checksum());
     let updating_message = "Updating test-plugin 0.1.0 to 0.2.0...".to_string();
-    let compiling_message = "Compiling https://plugins.dprint.dev/test-plugin-2.wasm".to_string();
+    let compiling_message = "Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string();
 
     // test all the wasm combinations
     test_update(TestUpdateOptions {
@@ -988,7 +1016,7 @@ mod test {
         "Updating test-plugin 0.1.0 to 0.2.0...".to_string(),
         format!("The process plugin test-process-plugin 0.1.0 has a new url: {}", new_ps_url_with_checksum),
         "Do you want to update it? N".to_string(),
-        "Compiling https://plugins.dprint.dev/test-plugin-2.wasm".to_string(),
+        "Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
       ],
       expected_urls: vec![new_wasm_url.clone(), old_ps_url.clone()],
       always_update: false,
@@ -1014,16 +1042,36 @@ mod test {
   },
   "should_set": "other",
   "should_remove": {},
+  "should_set_past_version": "",
+}"#,
+      );
+      config.add_config_section(
+        "test-plugin",
+        r#"{
+  "should_add": {
+  },
+  "should_set": "other",
+  "should_remove": {},
+  "should_set_past_version": "",
 }"#,
       );
     });
     builder.with_local_config("/sub_folder/dprint.json", |config| {
-      config.add_remote_process_plugin().add_config_section(
-        "testProcessPlugin",
-        r#"{
+      config
+        .add_remote_process_plugin()
+        .add_remote_wasm_plugin_0_1_0()
+        .add_config_section(
+          "testProcessPlugin",
+          r#"{
   "should_set": "asdf"
 }"#,
-      );
+        )
+        .add_config_section(
+          "test-plugin",
+          r#"{
+  "should_set": "asdf"
+}"#,
+        );
     });
     let environment = builder.initialize().build();
     run_test_cli(vec!["config", "update", "--yes"], &environment).unwrap();
@@ -1033,7 +1081,8 @@ mod test {
         "Updating test-plugin 0.1.0 to 0.2.0...".to_string(),
         "Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
         "Updating test-process-plugin 0.1.0 in /sub_folder/dprint.json to 0.3.0...".to_string(),
-        "Compiling https://plugins.dprint.dev/test-plugin-2.wasm".to_string(),
+        "Updating test-plugin 0.1.0 in /sub_folder/dprint.json to 0.2.0...".to_string(),
+        "Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
         "Extracting zip for test-process-plugin".to_string()
       ]
     );
@@ -1044,6 +1093,7 @@ mod test {
   "testProcessPlugin": {{
     "should_add": "new_value",
     "should_set": "new_value",
+    "should_set_past_version": "0.1.0",
     "new_prop1": [
       "new_value"
     ],
@@ -1051,8 +1101,19 @@ mod test {
       "new_prop": "new_value"
     }},
   }},
+  "test-plugin": {{
+    "should_add": "new_value_wasm",
+    "should_set": "new_value_wasm",
+    "should_set_past_version": "0.1.0",
+    "new_prop1": [
+      "new_value_wasm"
+    ],
+    "new_prop2": {{
+      "new_prop": "new_value_wasm"
+    }},
+  }},
   "plugins": [
-    "https://plugins.dprint.dev/test-plugin-2.wasm",
+    "https://plugins.dprint.dev/test-plugin.wasm",
     "https://plugins.dprint.dev/test-plugin-3.json@{}"
   ]
 }}"#,
@@ -1066,8 +1127,12 @@ mod test {
   "testProcessPlugin": {{
     "should_set": "new_value"
   }},
+  "test-plugin": {{
+    "should_set": "new_value_wasm"
+  }},
   "plugins": [
-    "https://plugins.dprint.dev/test-plugin-3.json@{}"
+    "https://plugins.dprint.dev/test-plugin-3.json@{}",
+    "https://plugins.dprint.dev/test-plugin.wasm"
   ]
 }}"#,
         NEW_PROCESS_PLUGIN_FILE.checksum()
@@ -1151,12 +1216,11 @@ mod test {
   }
 
   fn get_setup_builder(opts: SetupEnvOptions) -> TestEnvironmentBuilder {
-    let actual_wasm_plugin_checksum = test_helpers::get_test_wasm_plugin_checksum();
     let mut builder = TestEnvironmentBuilder::new();
 
     if opts.config_has_wasm {
       builder.add_remote_wasm_plugin();
-      builder.add_remote_wasm_plugin_at_url("https://plugins.dprint.dev/test-plugin-2.wasm");
+      builder.add_remote_wasm_0_1_0_plugin();
     }
     if opts.config_has_process {
       builder.add_remote_process_plugin();
@@ -1168,7 +1232,7 @@ mod test {
         info.add_plugin(TestInfoFilePlugin {
           name: "test-plugin".to_string(),
           version: "0.2.0".to_string(),
-          url: "https://plugins.dprint.dev/test-plugin-2.wasm".to_string(),
+          url: "https://plugins.dprint.dev/test-plugin.wasm".to_string(),
           config_key: Some("test-plugin".to_string()),
           checksum: if opts.remote_has_wasm_checksum {
             Some(get_test_wasm_plugin_checksum())
@@ -1195,9 +1259,9 @@ mod test {
         config.ensure_plugins_section();
         if opts.config_has_wasm {
           if opts.config_has_wasm_checksum {
-            config.add_remote_wasm_plugin_with_checksum(&actual_wasm_plugin_checksum);
+            config.add_remote_wasm_plugin_0_1_0_with_checksum();
           } else {
-            config.add_remote_wasm_plugin();
+            config.add_remote_wasm_plugin_0_1_0();
           }
         }
         if opts.config_has_process {
@@ -1210,7 +1274,7 @@ mod test {
         "https://plugins.dprint.dev/dprint/test-plugin/latest.json",
         &json!({
           "schemaVersion": 1,
-          "url": "https://plugins.dprint.dev/test-plugin-2.wasm",
+          "url": "https://plugins.dprint.dev/test-plugin.wasm",
           "version": "0.2.0",
           "checksum": if opts.remote_has_wasm_checksum { Some(get_test_wasm_plugin_checksum()) } else { None },
         })
@@ -1241,8 +1305,8 @@ mod test {
         "https://plugins.dprint.dev/dprint/test-plugin/latest.json",
         &json!({
           "schemaVersion": 1,
-          "url": "https://plugins.dprint.dev/test-plugin-2.wasm",
-          "version": "0.1.0"
+          "url": "https://plugins.dprint.dev/test-plugin.wasm",
+          "version": "0.2.0"
         })
         .to_string(),
       )
@@ -1257,9 +1321,10 @@ mod test {
   fn config_update_should_handle_wasm_to_process_plugin() {
     let environment = TestEnvironmentBuilder::new()
       .add_remote_wasm_plugin()
+      .add_remote_wasm_0_1_0_plugin()
       .with_info_file(|_| {})
       .with_default_config(|config| {
-        config.add_remote_wasm_plugin();
+        config.add_remote_wasm_plugin_0_1_0();
       })
       .add_remote_file(
         "https://plugins.dprint.dev/dprint/test-plugin/latest.json",
diff --git a/crates/dprint/src/commands/editor/mod.rs b/crates/dprint/src/commands/editor/mod.rs
index 842cbc07d..7f2f492ea 100644
--- a/crates/dprint/src/commands/editor/mod.rs
+++ b/crates/dprint/src/commands/editor/mod.rs
@@ -352,7 +352,7 @@ mod test {
     final_output.push_str(&environment.cli_version());
     final_output.push_str(r#"","configSchemaUrl":"https://dprint.dev/schemas/v0.json","plugins":["#);
     final_output
-      .push_str(r#"{"name":"test-plugin","version":"0.1.0","configKey":"test-plugin","fileExtensions":["txt"],"fileNames":[],"configSchemaUrl":"https://plugins.dprint.dev/test/schema.json","helpUrl":"https://dprint.dev/plugins/test"},"#);
+      .push_str(r#"{"name":"test-plugin","version":"0.2.0","configKey":"test-plugin","fileExtensions":["txt"],"fileNames":[],"configSchemaUrl":"https://plugins.dprint.dev/test/schema.json","helpUrl":"https://dprint.dev/plugins/test"},"#);
     final_output.push_str(r#"{"name":"test-process-plugin","version":"0.1.0","configKey":"testProcessPlugin","fileExtensions":["txt_ps"],"fileNames":["test-process-plugin-exact-file"],"helpUrl":"https://dprint.dev/plugins/test-process"}]}"#);
     assert_eq!(environment.take_stdout_messages(), vec![final_output]);
     let mut stderr_messages = environment.take_stderr_messages();
@@ -734,7 +734,7 @@ mod test {
             result.unwrap();
           }
 
-          // test range formatting
+          // test process range formatting
           assert_eq!(
             bytes_to_string(
               communicator
@@ -751,33 +751,88 @@ mod test {
             ),
             "t_formatted_process_sting_formatted_process"
           );
+          // test process plugin host formatting with range
+          assert_eq!(
+            bytes_to_string(
+              communicator
+                .format_text(
+                  &PathBuf::from("/file.txt_ps"),
+                  "plugin-range: testing".to_string().into_bytes(),
+                  Some(1..2),
+                  Default::default(),
+                  Default::default()
+                )
+                .await
+                .unwrap()
+                .unwrap()
+            ),
+            "plugin-range: t_formatted_sting_formatted_formatted_process"
+          );
 
-          // test cancellation
-          let token = CancellationToken::new();
-          let handle = dprint_core::async_runtime::spawn({
-            let communicator = communicator.clone();
-            let token = token.clone();
-            async move {
-              assert_eq!(
-                communicator
-                  .format_text(
-                    &PathBuf::from("/file.txt_ps"),
-                    "wait_cancellation".to_string().into_bytes(),
-                    None,
-                    Default::default(),
-                    token
-                  )
-                  .await
-                  .unwrap(),
-                None
-              )
-            }
-          });
+          // test wasm range formatting
+          assert_eq!(
+            bytes_to_string(
+              communicator
+                .format_text(
+                  &PathBuf::from("/file.txt"),
+                  "testing".to_string().into_bytes(),
+                  Some(1..2),
+                  Default::default(),
+                  Default::default()
+                )
+                .await
+                .unwrap()
+                .unwrap()
+            ),
+            "t_formatted_sting_formatted"
+          );
+
+          // test wasm plugin host formatting with range
+          assert_eq!(
+            bytes_to_string(
+              communicator
+                .format_text(
+                  &PathBuf::from("/file.txt"),
+                  "plugin-range: testing".to_string().into_bytes(),
+                  Some(1..2),
+                  Default::default(),
+                  Default::default()
+                )
+                .await
+                .unwrap()
+                .unwrap()
+            ),
+            "plugin-range: t_formatted_process_sting_formatted_process_formatted"
+          );
 
-          // give some time for the message to be sent
-          tokio::time::sleep(Duration::from_millis(50)).await;
-          token.cancel();
-          handle.await.unwrap();
+          // test process and wasm plugin cancellation
+          for file_name in ["/file.txt_ps", "/file.txt"] {
+            let token = CancellationToken::new();
+            let handle = dprint_core::async_runtime::spawn({
+              let communicator = communicator.clone();
+              let token = token.clone();
+              async move {
+                assert_eq!(
+                  communicator
+                    .format_text(
+                      &PathBuf::from(file_name),
+                      "wait_cancellation".to_string().into_bytes(),
+                      None,
+                      Default::default(),
+                      token
+                    )
+                    .await
+                    .unwrap(),
+                  None
+                )
+              }
+            });
+
+            // give some time for the message to be sent
+            tokio::time::sleep(Duration::from_millis(50)).await;
+            token.cancel();
+            handle.await.unwrap();
+          }
 
           // test override config
           assert_eq!(
diff --git a/crates/dprint/src/commands/formatting.rs b/crates/dprint/src/commands/formatting.rs
index e462c0b2b..d22469193 100644
--- a/crates/dprint/src/commands/formatting.rs
+++ b/crates/dprint/src/commands/formatting.rs
@@ -843,6 +843,52 @@ mod test {
     assert_eq!(environment.read_file(&file_path2).unwrap(), "text2"); // ignored
   }
 
+  #[test]
+  fn should_allow_using_config_to_get_file_matching_info() {
+    let file_path1 = "/file1.txt";
+    let file_path2 = "/file2.txt_ps";
+    let file_path3 = "/file1.other";
+    let file_path4 = "/file2.other_ps";
+    let file_path5 = "/format_wasm";
+    let file_path6 = "/format_ps";
+    let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_and_process_plugin()
+      .with_local_config("/config.json", |c| {
+        c.add_remote_wasm_plugin()
+          .add_remote_process_plugin()
+          .add_config_section(
+            "test-plugin",
+            r#"{
+              "file_extensions": ["other"],
+              "file_names": ["format_wasm"]
+            }"#,
+          )
+          .add_config_section(
+            "testProcessPlugin",
+            r#"{
+              "file_extensions": ["other_ps"],
+              "file_names": ["format_ps"]
+            }"#,
+          );
+      })
+      .write_file(&file_path1, "text")
+      .write_file(&file_path2, "text2")
+      .write_file(&file_path3, "text3")
+      .write_file(&file_path4, "text4")
+      .write_file(&file_path5, "text5")
+      .write_file(&file_path6, "text6")
+      .build();
+
+    run_test_cli(vec!["fmt", "--config", "/config.json"], &environment).unwrap();
+
+    assert_eq!(environment.take_stdout_messages(), vec![get_plural_formatted_text(4)]);
+    assert_eq!(environment.read_file(&file_path1).unwrap(), "text");
+    assert_eq!(environment.read_file(&file_path2).unwrap(), "text2");
+    assert_eq!(environment.read_file(&file_path3).unwrap(), "text3_formatted");
+    assert_eq!(environment.read_file(&file_path4).unwrap(), "text4_formatted_process");
+    assert_eq!(environment.read_file(&file_path5).unwrap(), "text5_formatted");
+    assert_eq!(environment.read_file(&file_path6).unwrap(), "text6_formatted_process");
+  }
+
   #[test]
   fn should_error_on_wasm_plugin_config_diagnostic() {
     let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_plugin()
@@ -2208,4 +2254,28 @@ mod test {
       .to_string()]
     );
   }
+
+  #[test]
+  fn wasm_log_stderr() {
+    let file_path1 = "/file1.txt";
+    let file_path2 = "/file2.txt";
+    let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_plugin()
+      // these will both go to stderr actually
+      .write_file(&file_path1, "stdout: hi stdout")
+      .write_file(&file_path2, "stderr: hi stderr")
+      .build();
+    run_test_cli(vec!["fmt", "*.txt"], &environment).unwrap();
+    assert_eq!(environment.take_stdout_messages(), vec![get_plural_formatted_text(2)]);
+    assert_eq!(environment.read_file(&file_path1).unwrap(), "stdout: hi stdout_formatted");
+    assert_eq!(environment.read_file(&file_path2).unwrap(), "stderr: hi stderr_formatted");
+    assert_eq!(
+      {
+        let mut messages = environment.take_stderr_messages();
+        messages.sort();
+        messages
+      },
+      // everything goes to stderr
+      vec![" hi stderr", " hi stderr_formatted", " hi stdout", " hi stdout_formatted"]
+    );
+  }
 }
diff --git a/crates/dprint/src/commands/lsp/mod.rs b/crates/dprint/src/commands/lsp/mod.rs
index e524f9091..148119202 100644
--- a/crates/dprint/src/commands/lsp/mod.rs
+++ b/crates/dprint/src/commands/lsp/mod.rs
@@ -505,6 +505,16 @@ mod test {
             };
           }
 
+          macro_rules! did_close {
+            ($uri: ident) => {
+              backend
+                .did_close(DidCloseTextDocumentParams {
+                  text_document: TextDocumentIdentifier { uri: $uri.clone() },
+                })
+                .await;
+            };
+          }
+
           macro_rules! assert_format {
             ($uri: ident, $expected: expr) => {
               let result = backend
@@ -811,6 +821,67 @@ mod test {
             }])
           );
 
+          // now ensure formatting works with a sub folder config file that has different config
+          {
+            let mut config_file = TestConfigFileBuilder::new(environment.clone());
+            config_file.add_remote_wasm_plugin().add_remote_process_plugin();
+            environment.write_file("/other_config/dprint.json", &config_file.to_string()).unwrap();
+            let mut config_file = TestConfigFileBuilder::new(environment.clone());
+            config_file
+              .add_remote_wasm_plugin()
+              .add_remote_process_plugin()
+              .add_config_section("test-plugin", r#"{"ending": "asdf"}"#)
+              .add_config_section("testProcessPlugin", r#"{"ending": "asdf_ps"}"#);
+            environment.write_file("/other_config/sub/dprint.json", &config_file.to_string()).unwrap();
+          }
+
+          for _ in 0..2 {
+            let file_uri = Url::parse("file:///other_config/file.txt").unwrap();
+            did_open!(file_uri, "text");
+            assert_format!(
+              file_uri,
+              Some(vec![TextEdit {
+                range: Range::new(Position::new(0, 4), Position::new(0, 4)),
+                new_text: "_formatted".to_string()
+              }])
+            );
+            did_close!(file_uri);
+            // switching to a different config should still work fine
+            let file_uri = Url::parse("file:///other_config/sub/file.txt").unwrap();
+            did_open!(file_uri, "text");
+            assert_format!(
+              file_uri,
+              Some(vec![TextEdit {
+                range: Range::new(Position::new(0, 4), Position::new(0, 4)),
+                new_text: "_asdf".to_string()
+              }])
+            );
+            did_close!(file_uri);
+
+            // now try with a process plugin
+            let file_uri = Url::parse("file:///other_config/file.txt_ps").unwrap();
+            did_open!(file_uri, "text");
+            assert_format!(
+              file_uri,
+              Some(vec![TextEdit {
+                range: Range::new(Position::new(0, 4), Position::new(0, 4)),
+                new_text: "_formatted_process".to_string()
+              }])
+            );
+            did_close!(file_uri);
+            // switching to a different config should work fine as well
+            let file_uri = Url::parse("file:///other_config/sub/file.txt_ps").unwrap();
+            did_open!(file_uri, "text");
+            assert_format!(
+              file_uri,
+              Some(vec![TextEdit {
+                range: Range::new(Position::new(0, 4), Position::new(0, 4)),
+                new_text: "_asdf_ps".to_string()
+              }])
+            );
+            did_close!(file_uri);
+          }
+
           backend.shutdown().await.unwrap();
         }
       });
diff --git a/crates/dprint/src/configuration/get_init_config_file_text.rs b/crates/dprint/src/configuration/get_init_config_file_text.rs
index eff680857..564e0fbe8 100644
--- a/crates/dprint/src/configuration/get_init_config_file_text.rs
+++ b/crates/dprint/src/configuration/get_init_config_file_text.rs
@@ -363,7 +363,7 @@ mod test {
   fn should_get_initialization_text_when_old_plugin_system() {
     let environment = TestEnvironmentBuilder::new()
       .with_info_file(|info| {
-        info.set_plugin_schema_version(9).add_plugin(TestInfoFilePlugin {
+        info.set_plugin_schema_version(999).add_plugin(TestInfoFilePlugin {
           name: "dprint-plugin-typescript".to_string(),
           version: "0.17.2".to_string(),
           url: "https://plugins.dprint.dev/typescript-0.17.2.wasm".to_string(),
@@ -393,7 +393,7 @@ mod test {
       expected_messages.push(concat!(
         "You are using an old version of dprint so the created config file may not be as helpful of a starting point. ",
         "Consider upgrading to support new plugins. ",
-        "Plugin system schema version is 3, latest is 9."
+        "Plugin system schema version is 4, latest is 999."
       ));
       assert_eq!(environment.take_stderr_messages(), expected_messages);
     });
diff --git a/crates/dprint/src/configuration/manipulation.rs b/crates/dprint/src/configuration/manipulation.rs
index 66f160118..5b42d5eed 100644
--- a/crates/dprint/src/configuration/manipulation.rs
+++ b/crates/dprint/src/configuration/manipulation.rs
@@ -19,6 +19,7 @@ use crate::utils::PluginKind;
 // This entire module is pretty bad. It would be better to add some manipulation
 // capabilities to jsonc-parser.
 
+#[derive(Debug)]
 pub struct PluginUpdateInfo {
   pub name: String,
   pub old_version: String,
@@ -43,7 +44,7 @@ impl PluginUpdateInfo {
   }
 }
 
-pub fn update_plugin_in_config(file_text: &str, info: PluginUpdateInfo) -> String {
+pub fn update_plugin_in_config(file_text: &str, info: &PluginUpdateInfo) -> String {
   let new_url = info.get_full_new_config_url();
   file_text.replace(&info.old_reference.to_string(), &new_url)
 }
diff --git a/crates/dprint/src/format.rs b/crates/dprint/src/format.rs
index 5a28d2d06..e1e5b6982 100644
--- a/crates/dprint/src/format.rs
+++ b/crates/dprint/src/format.rs
@@ -378,7 +378,7 @@ async fn run_cpu_throttling_task(environment: &impl Environment, number_threads:
     let cpu_usage = environment.cpu_usage().await;
     log_debug!(environment, "CPU usage: {}%", cpu_usage);
     if cpu_usage > decrease_bound {
-      if throttle_cpu(semaphores).await {
+      if throttle_cpu(semaphores) {
         log_debug!(environment, "High CPU. Reducing parallelism.");
         throttled_times += 1;
       }
@@ -403,7 +403,7 @@ async fn run_cpu_throttling_task(environment: &impl Environment, number_threads:
   }
 }
 
-async fn throttle_cpu(semaphores: &[Rc<Semaphore>]) -> bool {
+fn throttle_cpu(semaphores: &[Rc<Semaphore>]) -> bool {
   let mut best_match: Option<&Rc<Semaphore>> = None;
   let mut total_max_permits = 0;
   for semaphore in semaphores.iter() {
@@ -516,20 +516,20 @@ mod test {
     let permit2 = semaphore2.acquire().await;
     let permit3 = semaphore2.acquire().await;
     let semaphores = vec![semaphore1, semaphore2];
-    assert!(throttle_cpu(&semaphores).await);
+    assert!(throttle_cpu(&semaphores));
     assert_eq!(semaphores[1].max_permits(), 1);
     // still a pending removal
-    assert!(!throttle_cpu(&semaphores).await);
+    assert!(!throttle_cpu(&semaphores));
     drop(permit2);
-    assert!(throttle_cpu(&semaphores).await);
+    assert!(throttle_cpu(&semaphores));
     assert_eq!(semaphores[0].max_permits(), 0);
     assert_eq!(semaphores[1].max_permits(), 1);
     // still a pending removal
-    assert!(!throttle_cpu(&semaphores).await);
+    assert!(!throttle_cpu(&semaphores));
     drop(permit3);
     // only one permit remaining
-    assert!(!throttle_cpu(&semaphores).await);
+    assert!(!throttle_cpu(&semaphores));
     drop(permit1);
-    assert!(!throttle_cpu(&semaphores).await);
+    assert!(!throttle_cpu(&semaphores));
   }
 }
diff --git a/crates/dprint/src/main.rs b/crates/dprint/src/main.rs
index d0b393461..a8b595bc9 100644
--- a/crates/dprint/src/main.rs
+++ b/crates/dprint/src/main.rs
@@ -1,3 +1,7 @@
+#![deny(clippy::print_stderr)]
+#![deny(clippy::print_stdout)]
+#![deny(clippy::unused_async)]
+
 #[macro_use]
 mod environment;
 
@@ -34,6 +38,7 @@ fn main() {
       Err((err, log_level)) => {
         if log_level != LogLevel::Silent {
           let result = format!("{:#}", err.inner);
+          #[allow(clippy::print_stderr)]
           if !result.is_empty() {
             eprintln!("{}", result);
           }
diff --git a/crates/dprint/src/plugins/cache.rs b/crates/dprint/src/plugins/cache.rs
index 5f8c2c025..048d1ee4b 100644
--- a/crates/dprint/src/plugins/cache.rs
+++ b/crates/dprint/src/plugins/cache.rs
@@ -224,24 +224,23 @@ fn get_file_bytes<TEnvironment: Environment>(path_source: PathSource, environmen
 mod test {
   use super::*;
   use crate::environment::TestEnvironment;
-  use crate::plugins::CompilationResult;
   use crate::plugins::PluginSourceReference;
+  use crate::test_helpers::WASM_PLUGIN_0_1_0_BYTES;
+  use crate::test_helpers::WASM_PLUGIN_BYTES;
   use anyhow::Result;
-  use dprint_core::plugins::PluginInfo;
   use pretty_assertions::assert_eq;
   use std::path::PathBuf;
 
   #[tokio::test]
   async fn should_download_remote_file() -> Result<()> {
     let environment = TestEnvironment::new();
-    environment.add_remote_file("https://plugins.dprint.dev/test.wasm", "t".as_bytes());
-    environment.set_wasm_compile_result(create_compilation_result("t".as_bytes()));
+    environment.add_remote_file("https://plugins.dprint.dev/test.wasm", WASM_PLUGIN_BYTES);
     environment.set_cpu_arch("aarch64");
 
     let plugin_cache = PluginCache::new(environment.clone());
     let plugin_source = PluginSourceReference::new_remote_from_str("https://plugins.dprint.dev/test.wasm");
     let file_path = plugin_cache.get_plugin_cache_item(&plugin_source).await?.file_path;
-    let expected_file_path = PathBuf::from("/cache").join("plugins").join("test-plugin").join("0.1.0-4.2.5-aarch64");
+    let expected_file_path = PathBuf::from("/cache").join("plugins").join("test-plugin").join("0.2.0-4.2.5-aarch64");
 
     assert_eq!(file_path, expected_file_path);
     assert_eq!(environment.take_stderr_messages(), vec!["Compiling https://plugins.dprint.dev/test.wasm"]);
@@ -253,7 +252,7 @@ mod test {
     // should have saved the manifest
     assert_eq!(
       environment.read_file(&environment.get_cache_dir().join("plugin-cache-manifest.json")).unwrap(),
-      r#"{"schemaVersion":8,"wasmCacheVersion":"4.2.5","plugins":{"remote:https://plugins.dprint.dev/test.wasm":{"createdTime":123456,"info":{"name":"test-plugin","version":"0.1.0","configKey":"test-plugin","helpUrl":"test-url","configSchemaUrl":"schema-url","updateUrl":"update-url"}}}}"#,
+      r#"{"schemaVersion":8,"wasmCacheVersion":"4.2.5","plugins":{"remote:https://plugins.dprint.dev/test.wasm":{"createdTime":123456,"info":{"name":"test-plugin","version":"0.2.0","configKey":"test-plugin","helpUrl":"https://dprint.dev/plugins/test","configSchemaUrl":"https://plugins.dprint.dev/test/schema.json","updateUrl":"https://plugins.dprint.dev/dprint/test-plugin/latest.json"}}}}"#,
     );
 
     // should forget it afterwards
@@ -273,14 +272,12 @@ mod test {
   async fn should_cache_local_file() -> Result<()> {
     let environment = TestEnvironment::new();
     let original_file_path = PathBuf::from("/test.wasm");
-    let file_bytes = "t".as_bytes();
-    environment.write_file_bytes(&original_file_path, file_bytes).unwrap();
-    environment.set_wasm_compile_result(create_compilation_result("t".as_bytes()));
+    environment.write_file_bytes(&original_file_path, &WASM_PLUGIN_BYTES).unwrap();
 
     let plugin_cache = PluginCache::new(environment.clone());
     let plugin_source = PluginSourceReference::new_local(original_file_path.clone());
     let file_path = plugin_cache.get_plugin_cache_item(&plugin_source).await?.file_path;
-    let expected_file_path = PathBuf::from("/cache").join("plugins").join("test-plugin").join("0.1.0-4.2.5-x86_64");
+    let expected_file_path = PathBuf::from("/cache").join("plugins").join("test-plugin").join("0.2.0-4.2.5-x86_64");
 
     assert_eq!(file_path, expected_file_path);
 
@@ -291,35 +288,63 @@ mod test {
     assert_eq!(file_path, expected_file_path);
 
     // should have saved the manifest
+    let expected_text = serde_json::json!({
+      "schemaVersion": 8,
+      "wasmCacheVersion": "4.2.5",
+      "plugins": {
+        "local:/test.wasm": {
+          "createdTime": 123456,
+          "fileHash": get_bytes_hash(&WASM_PLUGIN_BYTES),
+          "info": {
+            "name": "test-plugin",
+            "version": "0.2.0",
+            "configKey": "test-plugin",
+            "helpUrl": "https://dprint.dev/plugins/test",
+            "configSchemaUrl": "https://plugins.dprint.dev/test/schema.json",
+            "updateUrl": "https://plugins.dprint.dev/dprint/test-plugin/latest.json"
+          }
+        }
+      }
+    });
     assert_eq!(
       environment.read_file(&environment.get_cache_dir().join("plugin-cache-manifest.json")).unwrap(),
-      concat!(
-        r#"{"schemaVersion":8,"wasmCacheVersion":"4.2.5","plugins":{"local:/test.wasm":{"createdTime":123456,"fileHash":10632242795325663332,"info":{"#,
-        r#""name":"test-plugin","version":"0.1.0","configKey":"test-plugin","#,
-        r#""helpUrl":"test-url","configSchemaUrl":"schema-url","updateUrl":"update-url"}}}}"#,
-      )
+      expected_text.to_string(),
     );
 
     assert_eq!(environment.take_stderr_messages().len(), 0); // no logs, nothing changed
 
     // update the file bytes
-    let file_bytes = "u".as_bytes();
-    environment.write_file_bytes(&original_file_path, file_bytes).unwrap();
+    environment.write_file_bytes(&original_file_path, &WASM_PLUGIN_0_1_0_BYTES).unwrap();
 
     // should update the cache with the new file
+    let expected_file_path = PathBuf::from("/cache").join("plugins").join("test-plugin").join("0.1.0-4.2.5-x86_64");
     let file_path = plugin_cache
       .get_plugin_cache_item(&PluginSourceReference::new_local(original_file_path.clone()))
       .await?
       .file_path;
     assert_eq!(file_path, expected_file_path);
 
+    let expected_text = serde_json::json!({
+      "schemaVersion": 8,
+      "wasmCacheVersion": "4.2.5",
+      "plugins": {
+        "local:/test.wasm": {
+          "createdTime": 123456,
+          "fileHash": get_bytes_hash(&WASM_PLUGIN_0_1_0_BYTES),
+          "info": {
+            "name": "test-plugin",
+            "version": "0.1.0",
+            "configKey": "test-plugin",
+            "helpUrl": "https://dprint.dev/plugins/test",
+            "configSchemaUrl": "https://plugins.dprint.dev/test/schema.json",
+            "updateUrl": "https://plugins.dprint.dev/dprint/test-plugin/latest.json"
+          }
+        }
+      }
+    });
     assert_eq!(
       environment.read_file(&environment.get_cache_dir().join("plugin-cache-manifest.json")).unwrap(),
-      concat!(
-        r#"{"schemaVersion":8,"wasmCacheVersion":"4.2.5","plugins":{"local:/test.wasm":{"createdTime":123456,"fileHash":6989588595861227504,"info":{"#,
-        r#""name":"test-plugin","version":"0.1.0","configKey":"test-plugin","#,
-        r#""helpUrl":"test-url","configSchemaUrl":"schema-url","updateUrl":"update-url"}}}}"#,
-      )
+      expected_text.to_string()
     );
 
     assert_eq!(environment.take_stderr_messages(), vec!["Compiling /test.wasm"]);
@@ -336,22 +361,4 @@ mod test {
 
     Ok(())
   }
-
-  fn create_compilation_result(bytes: &[u8]) -> CompilationResult {
-    CompilationResult {
-      bytes: bytes.to_vec(),
-      plugin_info: get_test_plugin_info(),
-    }
-  }
-
-  fn get_test_plugin_info() -> PluginInfo {
-    PluginInfo {
-      name: String::from("test-plugin"),
-      version: String::from("0.1.0"),
-      config_key: String::from("test-plugin"),
-      help_url: String::from("test-url"),
-      config_schema_url: String::from("schema-url"),
-      update_url: Some(String::from("update-url")),
-    }
-  }
 }
diff --git a/crates/dprint/src/plugins/implementations/mod.rs b/crates/dprint/src/plugins/implementations/mod.rs
index 5b80f4fa2..25dbcfb82 100644
--- a/crates/dprint/src/plugins/implementations/mod.rs
+++ b/crates/dprint/src/plugins/implementations/mod.rs
@@ -48,7 +48,7 @@ mod test {
           let format_config = Arc::new(FormatConfig {
             id: FormatConfigId::from_raw(1),
             global: Default::default(),
-            raw: Default::default(),
+            plugin: Default::default(),
           });
           let instance = plugin.initialize().await.unwrap();
           let file_matching_info = instance.file_matching_info(format_config.clone()).await.unwrap();
@@ -103,7 +103,7 @@ mod test {
           let format_config = Arc::new(FormatConfig {
             id: FormatConfigId::from_raw(1),
             global: Default::default(),
-            raw: Default::default(),
+            plugin: Default::default(),
           });
           let instance = plugin.initialize().await.unwrap();
           let file_matching_info = instance.file_matching_info(format_config.clone()).await.unwrap();
diff --git a/crates/dprint/src/plugins/implementations/process/communicator.rs b/crates/dprint/src/plugins/implementations/process/communicator.rs
index d035199b5..9f4905572 100644
--- a/crates/dprint/src/plugins/implementations/process/communicator.rs
+++ b/crates/dprint/src/plugins/implementations/process/communicator.rs
@@ -3,10 +3,10 @@ use crate::plugins::FormatConfig;
 use crate::plugins::InitializedPluginFormatRequest;
 use crate::utils::AsyncMutex;
 use anyhow::Result;
-use dprint_core::configuration::ConfigKeyMap;
 use dprint_core::configuration::ConfigurationDiagnostic;
 use dprint_core::plugins::process::ProcessPluginCommunicator;
 use dprint_core::plugins::process::ProcessPluginCommunicatorFormatRequest;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
 use dprint_core::plugins::ConfigChange;
 use dprint_core::plugins::FileMatchingInfo;
 use dprint_core::plugins::FormatConfigId;
@@ -84,8 +84,8 @@ impl<TEnvironment: Environment> InitializedProcessPluginCommunicator<TEnvironmen
     self.get_inner_ensure_config(config).await?.config_diagnostics(config.id).await
   }
 
-  pub async fn check_config_updates(&self, plugin_config: ConfigKeyMap) -> Result<Vec<ConfigChange>> {
-    self.get_inner().await.check_config_updates(plugin_config).await
+  pub async fn check_config_updates(&self, message: &CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
+    self.get_inner().await.check_config_updates(message).await
   }
 
   pub async fn format_text(&self, request: InitializedPluginFormatRequest) -> FormatResult {
@@ -128,7 +128,7 @@ impl<TEnvironment: Environment> InitializedProcessPluginCommunicator<TEnvironmen
     let inner = self.inner.lock().await;
     let has_config = inner.registered_configs.borrow_mut().contains(&config.id);
     if !has_config {
-      inner.communicator.register_config(config.id, &config.global, &config.raw).await?;
+      inner.communicator.register_config(config.id, &config.global, &config.plugin).await?;
       inner.registered_configs.borrow_mut().insert(config.id);
     }
     Ok(inner.communicator.clone())
@@ -170,12 +170,11 @@ mod test {
     environment.run_in_runtime({
       let environment = environment.clone();
       async move {
-        //debug_here::debug_here!();
         // ensure that the config gets recreated as well
         let communicator = Rc::new(InitializedProcessPluginCommunicator::new_test_plugin_communicator(environment.clone()).await);
         let format_config = Arc::new(FormatConfig {
           id: FormatConfigId::from_raw(1),
-          raw: {
+          plugin: {
             let mut config = ConfigKeyMap::new();
             config.insert("ending".to_string(), "custom".to_string().into());
             config
@@ -270,7 +269,7 @@ mod test {
         let communicator = InitializedProcessPluginCommunicator::new_test_plugin_communicator(environment.clone()).await;
         let format_config = Arc::new(FormatConfig {
           id: FormatConfigId::from_raw(1),
-          raw: Default::default(),
+          plugin: Default::default(),
           global: Default::default(),
         });
 
diff --git a/crates/dprint/src/plugins/implementations/process/plugin.rs b/crates/dprint/src/plugins/implementations/process/plugin.rs
index d6cab0875..634f581d2 100644
--- a/crates/dprint/src/plugins/implementations/process/plugin.rs
+++ b/crates/dprint/src/plugins/implementations/process/plugin.rs
@@ -1,7 +1,7 @@
 use anyhow::Result;
 use dprint_core::async_runtime::async_trait;
-use dprint_core::configuration::ConfigKeyMap;
 use dprint_core::configuration::ConfigurationDiagnostic;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
 use dprint_core::plugins::ConfigChange;
 use dprint_core::plugins::FileMatchingInfo;
 use dprint_core::plugins::FormatResult;
@@ -29,21 +29,36 @@ pub fn get_test_safe_executable_path(executable_file_path: PathBuf, environment:
     // do this so that we can launch the process in the tests
     let tmp_dir = PathBuf::from("temp");
     let temp_process_plugin_file = tmp_dir.join(if cfg!(target_os = "windows") { "temp-plugin.exe" } else { "temp-plugin" });
+    let temp_process_plugin_file_0_3_0 = tmp_dir.join(if cfg!(target_os = "windows") {
+      "temp-plugin-0.3.0.exe"
+    } else {
+      "temp-plugin-0.3.0"
+    });
     PLUGIN_FILE_INITIALIZE.call_once(|| {
+      fn create_for_file(file_path: &PathBuf, bytes: &[u8]) {
+        #[allow(clippy::disallowed_methods)]
+        let _ = std::fs::write(file_path, bytes);
+        if cfg!(unix) {
+          std::process::Command::new("sh")
+            .arg("-c")
+            .arg(format!("chmod +x {}", file_path.to_string_lossy()))
+            .status()
+            .unwrap();
+        }
+      }
       #[allow(clippy::disallowed_methods)]
       let _ = std::fs::create_dir(&tmp_dir);
-      #[allow(clippy::disallowed_methods)]
-      let _ = std::fs::write(&temp_process_plugin_file, environment.read_file_bytes(&executable_file_path).unwrap());
-      if cfg!(unix) {
-        std::process::Command::new("sh")
-          .arg("-c")
-          .arg(format!("chmod +x {}", temp_process_plugin_file.to_string_lossy()))
-          .status()
-          .unwrap();
-      }
+      let bytes = environment.read_file_bytes(&executable_file_path).unwrap();
+      create_for_file(&temp_process_plugin_file, &bytes);
+      create_for_file(&temp_process_plugin_file_0_3_0, &bytes);
     });
-    // ignore errors if path already exists
-    temp_process_plugin_file
+    if executable_file_path.to_string_lossy().contains("0.1.0") {
+      temp_process_plugin_file
+    } else if executable_file_path.to_string_lossy().contains("0.3.0") {
+      temp_process_plugin_file_0_3_0
+    } else {
+      unreachable!();
+    }
   }
 }
 
@@ -121,8 +136,8 @@ impl<TEnvironment: Environment> InitializedPlugin for InitializedProcessPlugin<T
     self.communicator.get_config_diagnostics(&config).await
   }
 
-  async fn check_config_updates(&self, plugin_config: ConfigKeyMap) -> Result<Vec<ConfigChange>> {
-    self.communicator.check_config_updates(plugin_config).await
+  async fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
+    self.communicator.check_config_updates(&message).await
   }
 
   async fn format_text(&self, request: InitializedPluginFormatRequest) -> FormatResult {
diff --git a/crates/dprint/src/plugins/implementations/wasm/compile.rs b/crates/dprint/src/plugins/implementations/wasm/compile.rs
index 453006329..46944df01 100644
--- a/crates/dprint/src/plugins/implementations/wasm/compile.rs
+++ b/crates/dprint/src/plugins/implementations/wasm/compile.rs
@@ -1,12 +1,28 @@
+use std::sync::Arc;
+
 use anyhow::bail;
 use anyhow::Result;
+use dprint_core::plugins::CancellationToken;
+use wasmer::ExportError;
+use wasmer::Instance;
+use wasmer::Store;
 
-use super::create_identity_import_object;
 use super::create_wasm_plugin_instance;
+use super::instance::create_identity_import_object;
 use super::load_instance::load_instance;
 use super::load_instance::WasmModuleCreator;
+use super::ImportObjectEnvironment;
 use crate::plugins::CompilationResult;
 
+struct CompileImportObjectEnvironment;
+
+impl ImportObjectEnvironment for CompileImportObjectEnvironment {
+  fn initialize(&self, _store: &mut Store, _instance: &Instance) -> Result<(), ExportError> {
+    Ok(())
+  }
+  fn set_token(&self, _store: &mut Store, _token: Arc<dyn CancellationToken>) {}
+}
+
 /// Compiles a Wasm module.
 pub fn compile(wasm_bytes: &[u8]) -> Result<CompilationResult> {
   let wasm_module_creator = WasmModuleCreator::default();
@@ -19,8 +35,8 @@ pub fn compile(wasm_bytes: &[u8]) -> Result<CompilationResult> {
 
   // load the plugin and get the info
   let mut store = wasmer::Store::default();
-  let imports = create_identity_import_object(&mut store);
-  let instance = load_instance(&mut store, &module, &imports)?;
+  let imports = create_identity_import_object(module.version(), &mut store);
+  let instance = load_instance(&mut store, &module, Box::new(CompileImportObjectEnvironment), &imports)?;
   let mut instance = create_wasm_plugin_instance(store, instance)?;
 
   Ok(CompilationResult {
diff --git a/crates/dprint/src/plugins/implementations/wasm/import_object.rs b/crates/dprint/src/plugins/implementations/wasm/import_object.rs
deleted file mode 100644
index ba2bcef45..000000000
--- a/crates/dprint/src/plugins/implementations/wasm/import_object.rs
+++ /dev/null
@@ -1,227 +0,0 @@
-use dprint_core::configuration::ConfigKeyMap;
-use dprint_core::plugins::FormatResult;
-use dprint_core::plugins::HostFormatRequest;
-use dprint_core::plugins::NullCancellationToken;
-use parking_lot::Mutex;
-use std::path::PathBuf;
-use std::sync::Arc;
-use wasmer::AsStoreRef;
-use wasmer::ExportError;
-use wasmer::Function;
-use wasmer::FunctionEnv;
-use wasmer::FunctionEnvMut;
-use wasmer::Instance;
-use wasmer::Memory;
-use wasmer::Store;
-
-pub type WasmHostFormatSender = tokio::sync::mpsc::UnboundedSender<(HostFormatRequest, std::sync::mpsc::Sender<FormatResult>)>;
-
-/// Use this when the plugins don't need to format via a plugin pool.
-pub fn create_identity_import_object(store: &mut Store) -> wasmer::Imports {
-  let host_clear_bytes = |_: u32| {};
-  let host_read_buffer = |_: u32, _: u32| {};
-  let host_write_buffer = |_: u32, _: u32, _: u32| {};
-  let host_take_override_config = || {};
-  let host_take_file_path = || {};
-  let host_format = || -> u32 { 0 }; // no change
-  let host_get_formatted_text = || -> u32 { 0 }; // zero length
-  let host_get_error_text = || -> u32 { 0 }; // zero length
-
-  wasmer::imports! {
-    "dprint" => {
-      "host_clear_bytes" => Function::new_typed(store, host_clear_bytes),
-      "host_read_buffer" => Function::new_typed(store, host_read_buffer),
-      "host_write_buffer" => Function::new_typed(store, host_write_buffer),
-      "host_take_override_config" => Function::new_typed(store, host_take_override_config),
-      "host_take_file_path" => Function::new_typed(store, host_take_file_path),
-      "host_format" => Function::new_typed(store, host_format),
-      "host_get_formatted_text" => Function::new_typed(store, host_get_formatted_text),
-      "host_get_error_text" => Function::new_typed(store, host_get_error_text),
-    }
-  }
-}
-
-/// Create an import object that formats text using plugins from the plugin pool
-pub fn create_pools_import_object(store: &mut Store, host_format_sender: WasmHostFormatSender) -> (wasmer::Imports, FunctionEnv<ImportObjectEnvironment>) {
-  let env = ImportObjectEnvironment::new(host_format_sender);
-  let env = FunctionEnv::new(store, env);
-
-  (
-    wasmer::imports! {
-      "dprint" => {
-        "host_clear_bytes" => Function::new_typed_with_env(store, &env, host_clear_bytes),
-        "host_read_buffer" => Function::new_typed_with_env(store, &env, host_read_buffer),
-        "host_write_buffer" => Function::new_typed_with_env(store, &env, host_write_buffer),
-        "host_take_override_config" => Function::new_typed_with_env(store, &env, host_take_override_config),
-        "host_take_file_path" => Function::new_typed_with_env(store, &env, host_take_file_path),
-        "host_format" => Function::new_typed_with_env(store, &env, host_format),
-        "host_get_formatted_text" => Function::new_typed_with_env(store, &env, host_get_formatted_text),
-        "host_get_error_text" => Function::new_typed_with_env(store, &env, host_get_error_text),
-      }
-    },
-    env,
-  )
-}
-
-#[derive(Default)]
-struct SharedBytes {
-  data: Vec<u8>,
-  index: usize,
-}
-
-impl SharedBytes {
-  pub fn with_size(size: usize) -> Self {
-    Self::from_bytes(vec![0; size])
-  }
-
-  pub fn from_bytes(data: Vec<u8>) -> Self {
-    Self { data, index: 0 }
-  }
-}
-
-pub struct ImportObjectEnvironment {
-  memory: Option<Memory>,
-  override_config: Option<ConfigKeyMap>,
-  file_path: Option<PathBuf>,
-  formatted_text_store: Vec<u8>,
-  shared_bytes: Mutex<SharedBytes>,
-  error_text_store: String,
-  host_format_sender: WasmHostFormatSender,
-}
-
-impl ImportObjectEnvironment {
-  pub fn new(host_format_sender: WasmHostFormatSender) -> Self {
-    ImportObjectEnvironment {
-      memory: None,
-      override_config: None,
-      file_path: None,
-      shared_bytes: Mutex::new(SharedBytes::default()),
-      formatted_text_store: Default::default(),
-      error_text_store: Default::default(),
-      host_format_sender,
-    }
-  }
-
-  pub fn initialize(&mut self, instance: &Instance) -> Result<(), ExportError> {
-    self.memory = Some(instance.exports.get_memory("memory")?.clone());
-    Ok(())
-  }
-
-  fn take_shared_bytes(&self) -> Vec<u8> {
-    let mut shared_bytes = self.shared_bytes.lock();
-    let data = std::mem::take(&mut shared_bytes.data);
-    shared_bytes.index = 0;
-    data
-  }
-}
-
-fn host_clear_bytes(env: FunctionEnvMut<ImportObjectEnvironment>, length: u32) {
-  let env = env.data();
-  *env.shared_bytes.lock() = SharedBytes::with_size(length as usize);
-}
-
-fn host_read_buffer(env: FunctionEnvMut<ImportObjectEnvironment>, buffer_pointer: u32, length: u32) {
-  let buffer_pointer: wasmer::WasmPtr<u32> = wasmer::WasmPtr::new(buffer_pointer);
-  let env_data = env.data();
-  let memory = env_data.memory.as_ref().unwrap();
-  let store_ref = env.as_store_ref();
-  let memory_view = memory.view(&store_ref);
-
-  let length = length as usize;
-  let mut shared_bytes = env_data.shared_bytes.lock();
-  let shared_bytes_index = shared_bytes.index;
-  memory_view
-    .read(
-      buffer_pointer.offset() as u64,
-      &mut shared_bytes.data[shared_bytes_index..shared_bytes_index + length],
-    )
-    .unwrap();
-  shared_bytes.index += length;
-}
-
-fn host_write_buffer(env: FunctionEnvMut<ImportObjectEnvironment>, buffer_pointer: u32, offset: u32, length: u32) {
-  let buffer_pointer: wasmer::WasmPtr<u32> = wasmer::WasmPtr::new(buffer_pointer);
-  let env_data = env.data();
-  let memory = env_data.memory.as_ref().unwrap();
-  let store_ref = env.as_store_ref();
-  let memory_view = memory.view(&store_ref);
-  let offset = offset as usize;
-  let length = length as usize;
-  let shared_bytes = env_data.shared_bytes.lock();
-  memory_view
-    .write(buffer_pointer.offset() as u64, &shared_bytes.data[offset..offset + length])
-    .unwrap();
-}
-
-fn host_take_override_config(mut env: FunctionEnvMut<ImportObjectEnvironment>) {
-  let env = env.data_mut();
-  let bytes = env.take_shared_bytes();
-  let config_key_map: ConfigKeyMap = serde_json::from_slice(&bytes).unwrap_or_default();
-  env.override_config.replace(config_key_map);
-}
-
-fn host_take_file_path(mut env: FunctionEnvMut<ImportObjectEnvironment>) {
-  let env = env.data_mut();
-  let bytes = env.take_shared_bytes();
-  let file_path_str = String::from_utf8(bytes).unwrap();
-  env.file_path.replace(PathBuf::from(file_path_str));
-}
-
-fn host_format(mut env: FunctionEnvMut<ImportObjectEnvironment>) -> u32 {
-  let env = env.data_mut();
-  let override_config = env.override_config.take().unwrap_or_default();
-  let file_path = env.file_path.take().expect("Expected to have file path.");
-  let file_bytes = env.take_shared_bytes();
-  let request = HostFormatRequest {
-    file_path,
-    file_bytes,
-    range: None,
-    override_config,
-    // Wasm plugins currently don't support cancellation
-    token: Arc::new(NullCancellationToken),
-  };
-  // todo: worth it to use a oneshot channel library here?
-  let (tx, rx) = std::sync::mpsc::channel();
-  let send_result = env.host_format_sender.send((request, tx));
-  let result = match send_result {
-    Ok(()) => match rx.recv() {
-      Ok(result) => result,
-      Err(_) => {
-        Ok(None) //receive error
-      }
-    },
-    Err(_) => Ok(None), // send error
-  };
-
-  match result {
-    Ok(Some(formatted_text)) => {
-      //let mut env = env.data_mut();
-      env.formatted_text_store = formatted_text;
-      1 // change
-    }
-    Ok(None) => {
-      0 // no change
-    }
-    // ignore critical error as we can just continue formatting
-    Err(err) => {
-      env.error_text_store = err.to_string();
-      2 // error
-    }
-  }
-}
-
-fn host_get_formatted_text(mut env: FunctionEnvMut<ImportObjectEnvironment>) -> u32 {
-  let env = env.data_mut();
-  let formatted_bytes = std::mem::take(&mut env.formatted_text_store);
-  let len = formatted_bytes.len();
-  *env.shared_bytes.lock() = SharedBytes::from_bytes(formatted_bytes);
-  len as u32
-}
-
-fn host_get_error_text(mut env: FunctionEnvMut<ImportObjectEnvironment>) -> u32 {
-  let env = env.data_mut();
-  let error_text = std::mem::take(&mut env.error_text_store);
-  let len = error_text.len();
-  *env.shared_bytes.lock() = SharedBytes::from_bytes(error_text.into_bytes());
-  len as u32
-}
diff --git a/crates/dprint/src/plugins/implementations/wasm/instance/mod.rs b/crates/dprint/src/plugins/implementations/wasm/instance/mod.rs
index af68220a7..f192e2e6d 100644
--- a/crates/dprint/src/plugins/implementations/wasm/instance/mod.rs
+++ b/crates/dprint/src/plugins/implementations/wasm/instance/mod.rs
@@ -1,40 +1,89 @@
 use std::path::Path;
+use std::sync::Arc;
 
 use anyhow::bail;
 use anyhow::Result;
 use dprint_core::configuration::ConfigKeyMap;
 use dprint_core::configuration::ConfigurationDiagnostic;
 use dprint_core::plugins::wasm::PLUGIN_SYSTEM_SCHEMA_VERSION;
+use dprint_core::plugins::CancellationToken;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
+use dprint_core::plugins::ConfigChange;
 use dprint_core::plugins::FileMatchingInfo;
+use dprint_core::plugins::FormatRange;
 use dprint_core::plugins::FormatResult;
+use dprint_core::plugins::HostFormatRequest;
 use dprint_core::plugins::PluginInfo;
-use v1::InitializedWasmPluginInstanceV1;
+use wasmer::ExportError;
+use wasmer::Instance;
 use wasmer::Store;
 
+use crate::environment::Environment;
 use crate::plugins::FormatConfig;
 
 use super::WasmInstance;
 
-mod v1;
+mod v3;
+mod v4;
+
+pub type WasmHostFormatSender = tokio::sync::mpsc::UnboundedSender<(HostFormatRequest, std::sync::mpsc::Sender<FormatResult>)>;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum PluginSchemaVersion {
+  V3,
+  V4,
+}
+
+pub trait ImportObjectEnvironment {
+  fn initialize(&self, store: &mut Store, instance: &Instance) -> Result<(), ExportError>;
+  fn set_token(&self, store: &mut Store, token: Arc<dyn CancellationToken>);
+}
 
 pub trait InitializedWasmPluginInstance {
   fn plugin_info(&mut self) -> Result<PluginInfo>;
   fn license_text(&mut self) -> Result<String>;
   fn resolved_config(&mut self, config: &FormatConfig) -> Result<String>;
   fn config_diagnostics(&mut self, config: &FormatConfig) -> Result<Vec<ConfigurationDiagnostic>>;
-  fn file_matching_info(&mut self, _config: &FormatConfig) -> Result<FileMatchingInfo>;
-  fn format_text(&mut self, file_path: &Path, file_bytes: &[u8], config: &FormatConfig, override_config: &ConfigKeyMap) -> FormatResult;
+  fn file_matching_info(&mut self, config: &FormatConfig) -> Result<FileMatchingInfo>;
+  fn check_config_updates(&mut self, message: &CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>>;
+  fn format_text(
+    &mut self,
+    file_path: &Path,
+    file_bytes: &[u8],
+    range: FormatRange,
+    config: &FormatConfig,
+    override_config: &ConfigKeyMap,
+    token: Arc<dyn CancellationToken>,
+  ) -> FormatResult;
 }
 
 pub fn create_wasm_plugin_instance(store: Store, instance: WasmInstance) -> Result<Box<dyn InitializedWasmPluginInstance>> {
   match instance.version() {
-    PluginSchemaVersion::V3 => Ok(Box::new(InitializedWasmPluginInstanceV1::new(store, instance)?)),
+    PluginSchemaVersion::V3 => Ok(Box::new(v3::InitializedWasmPluginInstanceV3::new(store, instance)?)),
+    PluginSchemaVersion::V4 => Ok(Box::new(v4::InitializedWasmPluginInstanceV4::new(store, instance)?)),
   }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum PluginSchemaVersion {
-  V3,
+/// Use this when the plugins don't need to format via a plugin pool.
+pub fn create_identity_import_object(version: PluginSchemaVersion, store: &mut Store) -> wasmer::Imports {
+  match version {
+    PluginSchemaVersion::V3 => v3::create_identity_import_object(store),
+    PluginSchemaVersion::V4 => v4::create_identity_import_object(store),
+  }
+}
+
+/// Create an import object that formats text using plugins from the plugin pool
+pub fn create_pools_import_object<TEnvironment: Environment>(
+  environment: TEnvironment,
+  plugin_name: &str,
+  version: PluginSchemaVersion,
+  store: &mut Store,
+  host_format_sender: WasmHostFormatSender,
+) -> (wasmer::Imports, Box<dyn ImportObjectEnvironment>) {
+  match version {
+    PluginSchemaVersion::V3 => v3::create_pools_import_object(store, host_format_sender),
+    PluginSchemaVersion::V4 => v4::create_pools_import_object(environment, plugin_name.to_string(), store, host_format_sender),
+  }
 }
 
 pub fn get_current_plugin_schema_version(module: &wasmer::Module) -> Result<PluginSchemaVersion> {
@@ -56,8 +105,9 @@ pub fn get_current_plugin_schema_version(module: &wasmer::Module) -> Result<Plug
 
   let plugin_schema_version = from_exports(module)?;
   match plugin_schema_version {
-    PLUGIN_SYSTEM_SCHEMA_VERSION => Ok(PluginSchemaVersion::V3),
-    version if version > PLUGIN_SYSTEM_SCHEMA_VERSION => {
+    3 => Ok(PluginSchemaVersion::V3),
+    4 => Ok(PluginSchemaVersion::V4),
+    version if version > 4 => {
       bail!(
         "Invalid schema version: {} -- Expected: {}. Upgrade your dprint CLI ({}).",
         plugin_schema_version,
diff --git a/crates/dprint/src/plugins/implementations/wasm/instance/v1.rs b/crates/dprint/src/plugins/implementations/wasm/instance/v3.rs
similarity index 53%
rename from crates/dprint/src/plugins/implementations/wasm/instance/v1.rs
rename to crates/dprint/src/plugins/implementations/wasm/instance/v3.rs
index 93eb50170..965d4382b 100644
--- a/crates/dprint/src/plugins/implementations/wasm/instance/v1.rs
+++ b/crates/dprint/src/plugins/implementations/wasm/instance/v3.rs
@@ -1,4 +1,6 @@
 use std::path::Path;
+use std::path::PathBuf;
+use std::sync::Arc;
 
 use anyhow::anyhow;
 use anyhow::bail;
@@ -6,13 +8,24 @@ use anyhow::Result;
 use dprint_core::configuration::ConfigKeyMap;
 use dprint_core::configuration::ConfigurationDiagnostic;
 use dprint_core::configuration::GlobalConfiguration;
+use dprint_core::plugins::CancellationToken;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
+use dprint_core::plugins::ConfigChange;
 use dprint_core::plugins::CriticalFormatError;
 use dprint_core::plugins::FileMatchingInfo;
 use dprint_core::plugins::FormatConfigId;
+use dprint_core::plugins::FormatRange;
 use dprint_core::plugins::FormatResult;
+use dprint_core::plugins::HostFormatRequest;
+use dprint_core::plugins::NullCancellationToken;
 use dprint_core::plugins::PluginInfo;
-use dprint_core::plugins::SyncPluginInfo;
-use wasmer::Engine;
+use parking_lot::Mutex;
+use serde::Serialize;
+use wasmer::AsStoreRef;
+use wasmer::ExportError;
+use wasmer::Function;
+use wasmer::FunctionEnv;
+use wasmer::FunctionEnvMut;
 use wasmer::Instance;
 use wasmer::Memory;
 use wasmer::MemoryView;
@@ -21,9 +34,11 @@ use wasmer::TypedFunction;
 use wasmer::WasmPtr;
 use wasmer::WasmTypeList;
 
+use crate::plugins::implementations::wasm::WasmHostFormatSender;
 use crate::plugins::implementations::wasm::WasmInstance;
 use crate::plugins::FormatConfig;
 
+use super::ImportObjectEnvironment;
 use super::InitializedWasmPluginInstance;
 
 enum WasmFormatResult {
@@ -32,13 +47,237 @@ enum WasmFormatResult {
   Error,
 }
 
-pub struct InitializedWasmPluginInstanceV1 {
+#[derive(Clone, Serialize, serde::Deserialize, Debug, PartialEq, Eq)]
+struct SyncPluginInfo {
+  #[serde(flatten)]
+  pub info: PluginInfo,
+  #[serde(flatten)]
+  pub file_matching: FileMatchingInfo,
+}
+
+pub fn create_identity_import_object(store: &mut Store) -> wasmer::Imports {
+  let host_clear_bytes = |_: u32| {};
+  let host_read_buffer = |_: u32, _: u32| {};
+  let host_write_buffer = |_: u32, _: u32, _: u32| {};
+  let host_take_override_config = || {};
+  let host_take_file_path = || {};
+  let host_format = || -> u32 { 0 }; // no change
+  let host_get_formatted_text = || -> u32 { 0 }; // zero length
+  let host_get_error_text = || -> u32 { 0 }; // zero length
+
+  wasmer::imports! {
+    "dprint" => {
+      "host_clear_bytes" => Function::new_typed(store, host_clear_bytes),
+      "host_read_buffer" => Function::new_typed(store, host_read_buffer),
+      "host_write_buffer" => Function::new_typed(store, host_write_buffer),
+      "host_take_override_config" => Function::new_typed(store, host_take_override_config),
+      "host_take_file_path" => Function::new_typed(store, host_take_file_path),
+      "host_format" => Function::new_typed(store, host_format),
+      "host_get_formatted_text" => Function::new_typed(store, host_get_formatted_text),
+      "host_get_error_text" => Function::new_typed(store, host_get_error_text),
+    }
+  }
+}
+
+pub fn create_pools_import_object(store: &mut Store, host_format_sender: WasmHostFormatSender) -> (wasmer::Imports, Box<dyn ImportObjectEnvironment>) {
+  #[derive(Default)]
+  struct SharedBytes {
+    data: Vec<u8>,
+    index: usize,
+  }
+
+  impl SharedBytes {
+    pub fn with_size(size: usize) -> Self {
+      Self::from_bytes(vec![0; size])
+    }
+
+    pub fn from_bytes(data: Vec<u8>) -> Self {
+      Self { data, index: 0 }
+    }
+  }
+
+  struct ImportObjectEnvironmentV3 {
+    memory: Option<Memory>,
+    override_config: Option<ConfigKeyMap>,
+    file_path: Option<PathBuf>,
+    formatted_text_store: Vec<u8>,
+    shared_bytes: Mutex<SharedBytes>,
+    error_text_store: String,
+    token: Arc<dyn CancellationToken>,
+    host_format_sender: WasmHostFormatSender,
+  }
+
+  impl ImportObjectEnvironmentV3 {
+    pub fn new(host_format_sender: WasmHostFormatSender) -> Self {
+      ImportObjectEnvironmentV3 {
+        memory: None,
+        override_config: None,
+        file_path: None,
+        shared_bytes: Mutex::new(SharedBytes::default()),
+        formatted_text_store: Default::default(),
+        error_text_store: Default::default(),
+        token: Arc::new(NullCancellationToken),
+        host_format_sender,
+      }
+    }
+
+    fn take_shared_bytes(&self) -> Vec<u8> {
+      let mut shared_bytes = self.shared_bytes.lock();
+      let data = std::mem::take(&mut shared_bytes.data);
+      shared_bytes.index = 0;
+      data
+    }
+  }
+
+  impl ImportObjectEnvironment for FunctionEnv<ImportObjectEnvironmentV3> {
+    fn initialize(&self, store: &mut Store, instance: &Instance) -> Result<(), ExportError> {
+      self.as_mut(store).memory = Some(instance.exports.get_memory("memory")?.clone());
+      Ok(())
+    }
+
+    fn set_token(&self, store: &mut Store, token: Arc<dyn CancellationToken>) {
+      // only used for host formatting in v3
+      self.as_mut(store).token = token;
+    }
+  }
+
+  fn host_clear_bytes(env: FunctionEnvMut<ImportObjectEnvironmentV3>, length: u32) {
+    let env = env.data();
+    *env.shared_bytes.lock() = SharedBytes::with_size(length as usize);
+  }
+
+  fn host_read_buffer(env: FunctionEnvMut<ImportObjectEnvironmentV3>, buffer_pointer: u32, length: u32) {
+    let buffer_pointer: wasmer::WasmPtr<u32> = wasmer::WasmPtr::new(buffer_pointer);
+    let env_data = env.data();
+    let memory = env_data.memory.as_ref().unwrap();
+    let store_ref = env.as_store_ref();
+    let memory_view = memory.view(&store_ref);
+
+    let length = length as usize;
+    let mut shared_bytes = env_data.shared_bytes.lock();
+    let shared_bytes_index = shared_bytes.index;
+    memory_view
+      .read(
+        buffer_pointer.offset() as u64,
+        &mut shared_bytes.data[shared_bytes_index..shared_bytes_index + length],
+      )
+      .unwrap();
+    shared_bytes.index += length;
+  }
+
+  fn host_write_buffer(env: FunctionEnvMut<ImportObjectEnvironmentV3>, buffer_pointer: u32, offset: u32, length: u32) {
+    let buffer_pointer: wasmer::WasmPtr<u32> = wasmer::WasmPtr::new(buffer_pointer);
+    let env_data = env.data();
+    let memory = env_data.memory.as_ref().unwrap();
+    let store_ref = env.as_store_ref();
+    let memory_view = memory.view(&store_ref);
+    let offset = offset as usize;
+    let length = length as usize;
+    let shared_bytes = env_data.shared_bytes.lock();
+    memory_view
+      .write(buffer_pointer.offset() as u64, &shared_bytes.data[offset..offset + length])
+      .unwrap();
+  }
+
+  fn host_take_override_config(mut env: FunctionEnvMut<ImportObjectEnvironmentV3>) {
+    let env = env.data_mut();
+    let bytes = env.take_shared_bytes();
+    let config_key_map: ConfigKeyMap = serde_json::from_slice(&bytes).unwrap_or_default();
+    env.override_config.replace(config_key_map);
+  }
+
+  fn host_take_file_path(mut env: FunctionEnvMut<ImportObjectEnvironmentV3>) {
+    let env = env.data_mut();
+    let bytes = env.take_shared_bytes();
+    let file_path_str = String::from_utf8(bytes).unwrap();
+    env.file_path.replace(PathBuf::from(file_path_str));
+  }
+
+  fn host_format(mut env: FunctionEnvMut<ImportObjectEnvironmentV3>) -> u32 {
+    let env = env.data_mut();
+    let override_config = env.override_config.take().unwrap_or_default();
+    let file_path = env.file_path.take().expect("Expected to have file path.");
+    let file_bytes = env.take_shared_bytes();
+    let request = HostFormatRequest {
+      file_path,
+      file_bytes,
+      range: None,
+      override_config,
+      token: env.token.clone(),
+    };
+    // todo: worth it to use a oneshot channel library here?
+    let (tx, rx) = std::sync::mpsc::channel();
+    let send_result = env.host_format_sender.send((request, tx));
+    let result = match send_result {
+      Ok(()) => match rx.recv() {
+        Ok(result) => result,
+        Err(_) => {
+          Ok(None) //receive error
+        }
+      },
+      Err(_) => Ok(None), // send error
+    };
+
+    match result {
+      Ok(Some(formatted_text)) => {
+        //let mut env = env.data_mut();
+        env.formatted_text_store = formatted_text;
+        1 // change
+      }
+      Ok(None) => {
+        0 // no change
+      }
+      // ignore critical error as we can just continue formatting
+      Err(err) => {
+        env.error_text_store = err.to_string();
+        2 // error
+      }
+    }
+  }
+
+  fn host_get_formatted_text(mut env: FunctionEnvMut<ImportObjectEnvironmentV3>) -> u32 {
+    let env = env.data_mut();
+    let formatted_bytes = std::mem::take(&mut env.formatted_text_store);
+    let len = formatted_bytes.len();
+    *env.shared_bytes.lock() = SharedBytes::from_bytes(formatted_bytes);
+    len as u32
+  }
+
+  fn host_get_error_text(mut env: FunctionEnvMut<ImportObjectEnvironmentV3>) -> u32 {
+    let env = env.data_mut();
+    let error_text = std::mem::take(&mut env.error_text_store);
+    let len = error_text.len();
+    *env.shared_bytes.lock() = SharedBytes::from_bytes(error_text.into_bytes());
+    len as u32
+  }
+
+  let env = ImportObjectEnvironmentV3::new(host_format_sender);
+  let env = FunctionEnv::new(store, env);
+
+  (
+    wasmer::imports! {
+      "dprint" => {
+        "host_clear_bytes" => Function::new_typed_with_env(store, &env, host_clear_bytes),
+        "host_read_buffer" => Function::new_typed_with_env(store, &env, host_read_buffer),
+        "host_write_buffer" => Function::new_typed_with_env(store, &env, host_write_buffer),
+        "host_take_override_config" => Function::new_typed_with_env(store, &env, host_take_override_config),
+        "host_take_file_path" => Function::new_typed_with_env(store, &env, host_take_file_path),
+        "host_format" => Function::new_typed_with_env(store, &env, host_format),
+        "host_get_formatted_text" => Function::new_typed_with_env(store, &env, host_get_formatted_text),
+        "host_get_error_text" => Function::new_typed_with_env(store, &env, host_get_error_text),
+      }
+    },
+    Box::new(env),
+  )
+}
+
+pub struct InitializedWasmPluginInstanceV3 {
   wasm_functions: WasmFunctions,
   buffer_size: usize,
   current_config_id: FormatConfigId,
 }
 
-impl InitializedWasmPluginInstanceV1 {
+impl InitializedWasmPluginInstanceV3 {
   pub fn new(store: Store, instance: WasmInstance) -> Result<Self> {
     let mut wasm_functions = WasmFunctions::new(store, instance)?;
     let buffer_size = wasm_functions.get_wasm_memory_buffer_size()?;
@@ -109,7 +348,7 @@ impl InitializedWasmPluginInstanceV1 {
       self.current_config_id = FormatConfigId::uninitialized();
       // update the plugin
       self.set_global_config(&config.global)?;
-      self.set_plugin_config(&config.raw)?;
+      self.set_plugin_config(&config.plugin)?;
       // now mark this as successfully set
       self.current_config_id = config.id;
     }
@@ -170,7 +409,7 @@ impl InitializedWasmPluginInstanceV1 {
   }
 }
 
-impl InitializedWasmPluginInstance for InitializedWasmPluginInstanceV1 {
+impl InitializedWasmPluginInstance for InitializedWasmPluginInstanceV3 {
   fn plugin_info(&mut self) -> Result<PluginInfo> {
     self.sync_plugin_info().map(|i| i.info)
   }
@@ -180,6 +419,10 @@ impl InitializedWasmPluginInstance for InitializedWasmPluginInstanceV1 {
     self.receive_string(len)
   }
 
+  fn check_config_updates(&mut self, _message: &CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
+    Ok(Vec::new())
+  }
+
   fn resolved_config(&mut self, config: &FormatConfig) -> Result<String> {
     self.ensure_config(config)?;
     let len = self.wasm_functions.get_resolved_config()?;
@@ -197,7 +440,19 @@ impl InitializedWasmPluginInstance for InitializedWasmPluginInstanceV1 {
     self.sync_plugin_info().map(|i| i.file_matching)
   }
 
-  fn format_text(&mut self, file_path: &Path, file_bytes: &[u8], config: &FormatConfig, override_config: &ConfigKeyMap) -> FormatResult {
+  fn format_text(
+    &mut self,
+    file_path: &Path,
+    file_bytes: &[u8],
+    range: FormatRange,
+    config: &FormatConfig,
+    override_config: &ConfigKeyMap,
+    token: Arc<dyn CancellationToken>,
+  ) -> FormatResult {
+    if range.is_some() && range != Some(0..file_bytes.len()) {
+      return Ok(None); // not supported for v3
+    }
+    self.wasm_functions.instance.set_token(&mut self.wasm_functions.store, token);
     self.ensure_config(config)?;
     match self.inner_format_text(file_path, file_bytes, override_config) {
       Ok(inner) => inner,
@@ -208,23 +463,15 @@ impl InitializedWasmPluginInstance for InitializedWasmPluginInstanceV1 {
 
 struct WasmFunctions {
   store: Store,
-  instance: Instance,
+  instance: WasmInstance,
   memory: Memory,
-  // keep this alive for the duration of the engine otherwise it
-  // could be cleaned up before the instance is dropped
-  _engine: Engine,
 }
 
 impl WasmFunctions {
   pub fn new(store: Store, instance: WasmInstance) -> Result<Self> {
-    let memory = instance.inner.exports.get_memory("memory")?.clone();
+    let memory = instance.get_memory("memory")?.clone();
 
-    Ok(WasmFunctions {
-      instance: instance.inner,
-      memory,
-      store,
-      _engine: instance.engine,
-    })
+    Ok(WasmFunctions { instance, memory, store })
   }
 
   #[inline]
@@ -334,7 +581,7 @@ impl WasmFunctions {
     Args: WasmTypeList,
     Rets: WasmTypeList,
   {
-    match self.instance.exports.get_function(name) {
+    match self.instance.get_function(name) {
       Ok(func) => match func.typed::<Args, Rets>(&self.store) {
         Ok(native_func) => Ok(native_func),
         Err(err) => bail!("Error creating function '{}'. Message: {:#}", name, err),
diff --git a/crates/dprint/src/plugins/implementations/wasm/instance/v4.rs b/crates/dprint/src/plugins/implementations/wasm/instance/v4.rs
new file mode 100644
index 000000000..a555b4e5d
--- /dev/null
+++ b/crates/dprint/src/plugins/implementations/wasm/instance/v4.rs
@@ -0,0 +1,651 @@
+use std::collections::HashSet;
+use std::path::Path;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use anyhow::anyhow;
+use anyhow::bail;
+use anyhow::Result;
+use dprint_core::configuration::ConfigKeyMap;
+use dprint_core::configuration::ConfigurationDiagnostic;
+use dprint_core::configuration::GlobalConfiguration;
+use dprint_core::plugins::wasm::JsonResponse;
+use dprint_core::plugins::CancellationToken;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
+use dprint_core::plugins::ConfigChange;
+use dprint_core::plugins::CriticalFormatError;
+use dprint_core::plugins::FileMatchingInfo;
+use dprint_core::plugins::FormatConfigId;
+use dprint_core::plugins::FormatRange;
+use dprint_core::plugins::FormatResult;
+use dprint_core::plugins::HostFormatRequest;
+use dprint_core::plugins::NullCancellationToken;
+use dprint_core::plugins::PluginInfo;
+use parking_lot::Mutex;
+use wasmer::AsStoreRef;
+use wasmer::ExportError;
+use wasmer::Function;
+use wasmer::FunctionEnv;
+use wasmer::FunctionEnvMut;
+use wasmer::Instance;
+use wasmer::Memory;
+use wasmer::MemoryView;
+use wasmer::Store;
+use wasmer::TypedFunction;
+use wasmer::WasmPtr;
+use wasmer::WasmTypeList;
+
+use crate::environment::Environment;
+use crate::plugins::implementations::wasm::ImportObjectEnvironment;
+use crate::plugins::implementations::wasm::WasmHostFormatSender;
+use crate::plugins::implementations::wasm::WasmInstance;
+use crate::plugins::FormatConfig;
+
+use super::InitializedWasmPluginInstance;
+
+enum WasmFormatResult {
+  NoChange,
+  Change,
+  Error,
+}
+
+pub fn create_identity_import_object(store: &mut Store) -> wasmer::Imports {
+  let host_write_buffer = |_: u32| {};
+  let host_format = |_: u32, _: u32, _: u32, _: u32, _: u32, _: u32, _: u32, _: u32| -> u32 { 0 }; // no change
+  let host_get_formatted_text = || -> u32 { 0 }; // zero length
+  let host_get_error_text = || -> u32 { 0 }; // zero length
+  let host_has_cancelled = || -> u32 { 0 }; // false
+  let fd_write = |_: u32, _: u32, _: u32, _: u32| 0; // ignore
+
+  wasmer::imports! {
+    "env" => {
+      "fd_write" => Function::new_typed(store, fd_write),
+    },
+    "dprint" => {
+      "host_write_buffer" => Function::new_typed(store, host_write_buffer),
+      "host_format" => Function::new_typed(store, host_format),
+      "host_get_formatted_text" => Function::new_typed(store, host_get_formatted_text),
+      "host_get_error_text" => Function::new_typed(store, host_get_error_text),
+      "host_has_cancelled" => Function::new_typed(store, host_has_cancelled),
+    }
+  }
+}
+
+pub fn create_pools_import_object<TEnvironment: Environment>(
+  environment: TEnvironment,
+  plugin_name: String,
+  store: &mut Store,
+  host_format_sender: WasmHostFormatSender,
+) -> (wasmer::Imports, Box<dyn ImportObjectEnvironment>) {
+  struct ImportObjectEnvironmentV4<TEnvironment: Environment> {
+    environment: TEnvironment,
+    plugin_name: String,
+    memory: Option<Memory>,
+    formatted_text_store: Vec<u8>,
+    shared_bytes: Mutex<Vec<u8>>,
+    error_text_store: String,
+    token: Arc<dyn CancellationToken>,
+    host_format_sender: WasmHostFormatSender,
+  }
+
+  impl<TEnvironment: Environment> ImportObjectEnvironment for FunctionEnv<ImportObjectEnvironmentV4<TEnvironment>> {
+    fn initialize(&self, store: &mut Store, instance: &Instance) -> Result<(), ExportError> {
+      self.as_mut(store).memory = Some(instance.exports.get_memory("memory")?.clone());
+      Ok(())
+    }
+
+    fn set_token(&self, store: &mut Store, token: Arc<dyn CancellationToken>) {
+      self.as_mut(store).token = token;
+    }
+  }
+
+  fn fd_write<TEnvironment: Environment>(
+    env: FunctionEnvMut<ImportObjectEnvironmentV4<TEnvironment>>,
+    fd: u32,
+    iovs_ptr: u32,
+    iovs_len: u32,
+    nwritten: WasmPtr<u32>,
+  ) -> u32 {
+    #[derive(Copy, Clone)]
+    #[repr(C)]
+    struct Iovec {
+      buf: u32,
+      buf_len: u32,
+    }
+
+    unsafe impl wasmer::ValueType for Iovec {
+      fn zero_padding_bytes(&self, _bytes: &mut [std::mem::MaybeUninit<u8>]) {}
+    }
+
+    let env_data = env.data();
+    let memory = env_data.memory.as_ref().unwrap();
+    let store_ref = env.as_store_ref();
+    let memory_view = memory.view(&store_ref);
+
+    let mut total_written = 0;
+
+    for i in 0..iovs_len {
+      let iovec_ptr = WasmPtr::<Iovec>::new(iovs_ptr + i * std::mem::size_of::<Iovec>() as u32);
+      let Ok(iovec) = iovec_ptr.deref(&memory_view).read() else {
+        return 1;
+      };
+
+      let buf_addr = iovec.buf;
+      let buf_len = iovec.buf_len;
+
+      let mut bytes = vec![0; buf_len as usize];
+      let success = memory_view.read(buf_addr as u64, &mut bytes).is_ok();
+      if !success {
+        return 1;
+      }
+
+      if matches!(fd, 1 | 2) {
+        let text = String::from_utf8_lossy(&bytes);
+        env_data.environment.log_stderr_with_context(&text, &env_data.plugin_name);
+      } else {
+        return 1; // Indicate error for unsupported fd
+      }
+
+      total_written += buf_len;
+    }
+
+    let nwritten = nwritten.deref(&memory_view);
+    let success = nwritten.write(total_written).is_ok();
+    if !success {
+      return 1;
+    }
+
+    0
+  }
+
+  fn host_write_buffer<TEnvironment: Environment>(env: FunctionEnvMut<ImportObjectEnvironmentV4<TEnvironment>>, buffer_pointer: u32) {
+    let buffer_pointer: wasmer::WasmPtr<u32> = wasmer::WasmPtr::new(buffer_pointer);
+    let env_data = env.data();
+    let memory = env_data.memory.as_ref().unwrap();
+    let store_ref = env.as_store_ref();
+    let memory_view = memory.view(&store_ref);
+    let shared_bytes = env_data.shared_bytes.lock();
+    memory_view.write(buffer_pointer.offset() as u64, &shared_bytes).unwrap();
+  }
+
+  #[allow(clippy::too_many_arguments)]
+  fn host_format<TEnvironment: Environment>(
+    mut env: FunctionEnvMut<ImportObjectEnvironmentV4<TEnvironment>>,
+    file_path_ptr: u32,
+    file_path_len: u32,
+    range_start: u32,
+    range_end: u32,
+    override_cfg_ptr: u32,
+    override_cfg_len: u32,
+    file_bytes_ptr: u32,
+    file_bytes_len: u32,
+  ) -> u32 {
+    let env_data = env.data();
+    let memory = env_data.memory.as_ref().unwrap();
+    let store_ref = env.as_store_ref();
+    let memory_view = memory.view(&store_ref);
+    let override_config = {
+      if override_cfg_len == 0 {
+        Default::default()
+      } else {
+        let mut buf = vec![0; override_cfg_len as usize];
+        memory_view.read(override_cfg_ptr as u64, &mut buf).unwrap();
+        serde_json::from_slice::<ConfigKeyMap>(&buf).unwrap()
+      }
+    };
+    let file_path = {
+      let mut buf = vec![0; file_path_len as usize];
+      memory_view.read(file_path_ptr as u64, &mut buf).unwrap();
+      PathBuf::from(String::from_utf8(buf).unwrap())
+    };
+    let file_bytes = {
+      let mut buf = vec![0; file_bytes_len as usize];
+      memory_view.read(file_bytes_ptr as u64, &mut buf).unwrap();
+      buf
+    };
+    let range = if range_start == 0 && range_end == file_bytes_len {
+      None
+    } else {
+      Some(range_start as usize..range_end as usize)
+    };
+    let env = env.data_mut();
+    let request = HostFormatRequest {
+      file_path,
+      file_bytes,
+      range,
+      override_config,
+      token: env.token.clone(),
+    };
+    // todo: worth it to use a oneshot channel library here?
+    let (tx, rx) = std::sync::mpsc::channel();
+    let send_result = env.host_format_sender.send((request, tx));
+    let result = match send_result {
+      Ok(()) => match rx.recv() {
+        Ok(result) => result,
+        Err(_) => {
+          Ok(None) // receive error
+        }
+      },
+      Err(_) => Ok(None), // send error
+    };
+
+    match result {
+      Ok(Some(formatted_text)) => {
+        //let mut env = env.data_mut();
+        env.formatted_text_store = formatted_text;
+        1 // change
+      }
+      Ok(None) => {
+        0 // no change
+      }
+      // ignore critical error as we can just continue formatting
+      Err(err) => {
+        env.error_text_store = err.to_string();
+        2 // error
+      }
+    }
+  }
+
+  fn host_get_formatted_text<TEnvironment: Environment>(mut env: FunctionEnvMut<ImportObjectEnvironmentV4<TEnvironment>>) -> u32 {
+    let env = env.data_mut();
+    let formatted_bytes = std::mem::take(&mut env.formatted_text_store);
+    let len = formatted_bytes.len();
+    *env.shared_bytes.lock() = formatted_bytes;
+    len as u32
+  }
+
+  fn host_get_error_text<TEnvironment: Environment>(mut env: FunctionEnvMut<ImportObjectEnvironmentV4<TEnvironment>>) -> u32 {
+    let env = env.data_mut();
+    let error_text = std::mem::take(&mut env.error_text_store);
+    let len = error_text.len();
+    *env.shared_bytes.lock() = error_text.into_bytes();
+    len as u32
+  }
+
+  fn host_has_cancelled<TEnvironment: Environment>(env: FunctionEnvMut<ImportObjectEnvironmentV4<TEnvironment>>) -> i32 {
+    if env.data().token.as_ref().is_cancelled() {
+      1
+    } else {
+      0
+    }
+  }
+
+  let env = ImportObjectEnvironmentV4 {
+    environment,
+    plugin_name,
+    memory: None,
+    shared_bytes: Default::default(),
+    formatted_text_store: Default::default(),
+    error_text_store: Default::default(),
+    token: Arc::new(NullCancellationToken),
+    host_format_sender,
+  };
+  let env = FunctionEnv::new(store, env);
+
+  (
+    wasmer::imports! {
+      "env" => {
+        "fd_write" => Function::new_typed_with_env(store, &env, fd_write),
+      },
+      "dprint" => {
+        "host_write_buffer" => Function::new_typed_with_env(store, &env, host_write_buffer),
+        "host_format" => Function::new_typed_with_env(store, &env, host_format),
+        "host_get_formatted_text" => Function::new_typed_with_env(store, &env, host_get_formatted_text),
+        "host_get_error_text" => Function::new_typed_with_env(store, &env, host_get_error_text),
+        "host_has_cancelled" => Function::new_typed_with_env(store, &env, host_has_cancelled),
+      }
+    },
+    Box::new(env),
+  )
+}
+
+pub struct InitializedWasmPluginInstanceV4 {
+  wasm_functions: WasmFunctions,
+  registered_config_ids: HashSet<FormatConfigId>,
+}
+
+impl InitializedWasmPluginInstanceV4 {
+  pub fn new(store: Store, instance: WasmInstance) -> Result<Self> {
+    let wasm_functions = WasmFunctions::new(store, instance)?;
+    Ok(Self {
+      wasm_functions,
+      registered_config_ids: HashSet::new(),
+    })
+  }
+
+  fn register_config(&mut self, config: &FormatConfig) -> Result<()> {
+    #[derive(serde::Serialize)]
+    struct RawFormatConfig<'a> {
+      pub plugin: &'a ConfigKeyMap,
+      pub global: &'a GlobalConfiguration,
+    }
+
+    let json = serde_json::to_string(&RawFormatConfig {
+      plugin: &config.plugin,
+      global: &config.global,
+    })?;
+    self.send_string(&json)?;
+    self.wasm_functions.register_config(config.id)?;
+    Ok(())
+  }
+
+  fn inner_format_text(
+    &mut self,
+    file_path: &Path,
+    file_bytes: &[u8],
+    range: FormatRange,
+    config: &FormatConfig,
+    override_config: Option<&str>,
+  ) -> Result<FormatResult> {
+    self.inner_setup_formatting(file_path, file_bytes, override_config)?;
+    let response_code = match range {
+      Some(range) => self.wasm_functions.format_range(config.id, range)?,
+      None => self.wasm_functions.format(config.id)?,
+    };
+    self.inner_handle_response(response_code)
+  }
+
+  fn inner_setup_formatting(&mut self, file_path: &Path, file_bytes: &[u8], override_config: Option<&str>) -> Result<()> {
+    // send override config if necessary
+    if let Some(override_config) = override_config {
+      self.send_string(override_config)?;
+      self.wasm_functions.set_override_config()?;
+    }
+
+    // send file path
+    self.send_string(&file_path.to_string_lossy())?;
+    self.wasm_functions.set_file_path()?;
+
+    // send file text
+    self.send_bytes(file_bytes)
+  }
+
+  fn inner_handle_response(&mut self, response_code: WasmFormatResult) -> Result<FormatResult> {
+    match response_code {
+      WasmFormatResult::NoChange => Ok(Ok(None)),
+      WasmFormatResult::Change => {
+        let len = self.wasm_functions.get_formatted_text()?;
+        let text_bytes = self.receive_bytes(len)?;
+        Ok(Ok(Some(text_bytes)))
+      }
+      WasmFormatResult::Error => {
+        let len = self.wasm_functions.get_error_text()?;
+        let text = self.receive_string(len)?;
+        Ok(Err(anyhow!("{}", text)))
+      }
+    }
+  }
+
+  fn ensure_config(&mut self, config: &FormatConfig) -> Result<()> {
+    if !self.registered_config_ids.contains(&config.id) {
+      // update the plugin
+      self.register_config(config)?;
+      // now mark this as successfully set
+      self.registered_config_ids.insert(config.id);
+    }
+    Ok(())
+  }
+
+  /* LOW LEVEL SENDING AND RECEIVING */
+
+  // These methods should panic when failing because that may indicate
+  // a major problem where the CLI is out of sync with the plugin.
+
+  fn send_string(&mut self, text: &str) -> Result<()> {
+    self.send_bytes(text.as_bytes())
+  }
+
+  fn send_bytes(&mut self, bytes: &[u8]) -> Result<()> {
+    let shared_bytes_ptr = self.wasm_functions.clear_shared_bytes(bytes.len())?;
+    let memory_view = self.wasm_functions.get_memory_view();
+    memory_view.write(shared_bytes_ptr.offset() as u64, bytes)?;
+    Ok(())
+  }
+
+  fn receive_string(&mut self, len: usize) -> Result<String> {
+    let bytes = self.receive_bytes(len)?;
+    Ok(String::from_utf8(bytes)?)
+  }
+
+  fn receive_bytes(&mut self, len: usize) -> Result<Vec<u8>> {
+    let mut bytes: Vec<u8> = vec![0; len];
+    self.read_bytes_from_shared_bytes(&mut bytes)?;
+    Ok(bytes)
+  }
+
+  fn read_bytes_from_shared_bytes(&mut self, bytes: &mut [u8]) -> Result<()> {
+    let wasm_buffer_pointer = self.wasm_functions.get_shared_bytes_ptr()?;
+    let memory_view = self.wasm_functions.get_memory_view();
+    memory_view.read(wasm_buffer_pointer.offset() as u64, bytes)?;
+    Ok(())
+  }
+}
+
+impl InitializedWasmPluginInstance for InitializedWasmPluginInstanceV4 {
+  fn plugin_info(&mut self) -> Result<PluginInfo> {
+    let len = self.wasm_functions.get_plugin_info()?;
+    let json_bytes = self.receive_bytes(len)?;
+    Ok(serde_json::from_slice(&json_bytes)?)
+  }
+
+  fn license_text(&mut self) -> Result<String> {
+    let len = self.wasm_functions.get_license_text()?;
+    self.receive_string(len)
+  }
+
+  fn check_config_updates(&mut self, message: &CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
+    let bytes = serde_json::to_vec(&message)?;
+    self.send_bytes(&bytes)?;
+    let len = self.wasm_functions.check_config_updates()?;
+    let bytes = self.receive_bytes(len)?;
+    let result: JsonResponse = serde_json::from_slice(&bytes)?;
+    match result {
+      JsonResponse::Ok(value) => Ok(serde_json::from_value(value)?),
+      JsonResponse::Err(err) => Err(anyhow!("{}", err)),
+    }
+  }
+
+  fn resolved_config(&mut self, config: &FormatConfig) -> Result<String> {
+    self.ensure_config(config)?;
+    let len = self.wasm_functions.get_resolved_config(config.id)?;
+    self.receive_string(len)
+  }
+
+  fn config_diagnostics(&mut self, config: &FormatConfig) -> Result<Vec<ConfigurationDiagnostic>> {
+    self.ensure_config(config)?;
+    let len = self.wasm_functions.get_config_diagnostics(config.id)?;
+    let json_text = self.receive_string(len)?;
+    Ok(serde_json::from_str(&json_text)?)
+  }
+
+  fn file_matching_info(&mut self, config: &FormatConfig) -> Result<FileMatchingInfo> {
+    self.ensure_config(config)?;
+    let len = self.wasm_functions.get_config_file_matching(config.id)?;
+    let json_text = self.receive_string(len)?;
+    Ok(serde_json::from_str(&json_text)?)
+  }
+
+  fn format_text(
+    &mut self,
+    file_path: &Path,
+    file_bytes: &[u8],
+    range: FormatRange,
+    config: &FormatConfig,
+    override_config: &ConfigKeyMap,
+    token: Arc<dyn CancellationToken>,
+  ) -> FormatResult {
+    let override_config = if !override_config.is_empty() {
+      Some(serde_json::to_string(override_config)?)
+    } else {
+      None
+    };
+    self.wasm_functions.instance.set_token(&mut self.wasm_functions.store, token);
+    self.ensure_config(config)?;
+    match self.inner_format_text(file_path, file_bytes, range, config, override_config.as_deref()) {
+      Ok(inner) => inner,
+      Err(err) => Err(CriticalFormatError(err).into()),
+    }
+  }
+}
+
+struct WasmFunctions {
+  store: Store,
+  instance: WasmInstance,
+  memory: Memory,
+}
+
+impl WasmFunctions {
+  pub fn new(store: Store, instance: WasmInstance) -> Result<Self> {
+    let memory = instance.get_memory("memory")?.clone();
+
+    Ok(WasmFunctions { instance, memory, store })
+  }
+
+  #[inline]
+  pub fn register_config(&mut self, config_id: FormatConfigId) -> Result<()> {
+    let func = self.get_export::<u32, ()>("register_config")?;
+    Ok(func.call(&mut self.store, config_id.as_raw())?)
+  }
+
+  #[inline]
+  pub fn get_plugin_info(&mut self) -> Result<usize> {
+    let func = self.get_export::<(), u32>("get_plugin_info")?;
+    Ok(func.call(&mut self.store).map(|value| value as usize)?)
+  }
+
+  #[inline]
+  pub fn get_license_text(&mut self) -> Result<usize> {
+    let func = self.get_export::<(), u32>("get_license_text")?;
+    Ok(func.call(&mut self.store).map(|value| value as usize)?)
+  }
+
+  #[inline]
+  pub fn check_config_updates(&mut self) -> Result<usize> {
+    let maybe_func = self.get_maybe_export::<(), u32>("check_config_updates")?;
+    match maybe_func {
+      Some(func) => Ok(func.call(&mut self.store).map(|value| value as usize)?),
+      None => Ok(0), // ignore, the plugin doesn't have this defined
+    }
+  }
+
+  #[inline]
+  pub fn get_resolved_config(&mut self, config_id: FormatConfigId) -> Result<usize> {
+    let func = self.get_export::<u32, u32>("get_resolved_config")?;
+    Ok(func.call(&mut self.store, config_id.as_raw()).map(|value| value as usize)?)
+  }
+
+  #[inline]
+  pub fn get_config_diagnostics(&mut self, config_id: FormatConfigId) -> Result<usize> {
+    let func = self.get_export::<u32, u32>("get_config_diagnostics")?;
+    Ok(func.call(&mut self.store, config_id.as_raw()).map(|value| value as usize)?)
+  }
+
+  #[inline]
+  pub fn get_config_file_matching(&mut self, config_id: FormatConfigId) -> Result<usize> {
+    let func = self.get_export::<u32, u32>("get_config_file_matching")?;
+    Ok(func.call(&mut self.store, config_id.as_raw()).map(|value| value as usize)?)
+  }
+
+  #[inline]
+  pub fn set_override_config(&mut self) -> Result<()> {
+    let set_override_config_func = self.get_export::<(), ()>("set_override_config")?;
+    Ok(set_override_config_func.call(&mut self.store)?)
+  }
+
+  #[inline]
+  pub fn set_file_path(&mut self) -> Result<()> {
+    let func = self.get_export::<(), ()>("set_file_path")?;
+    Ok(func.call(&mut self.store)?)
+  }
+
+  #[inline]
+  pub fn format(&mut self, config_id: FormatConfigId) -> Result<WasmFormatResult> {
+    let func = self.get_export::<u32, u8>("format")?;
+    Ok(func.call(&mut self.store, config_id.as_raw()).map(u8_to_format_result)?)
+  }
+
+  #[inline]
+  pub fn format_range(&mut self, config_id: FormatConfigId, range: std::ops::Range<usize>) -> Result<WasmFormatResult> {
+    let maybe_func = self.get_maybe_export::<(u32, u32, u32), u8>("format_range")?;
+    match maybe_func {
+      Some(func) => Ok(
+        func
+          .call(&mut self.store, config_id.as_raw(), range.start as u32, range.end as u32)
+          .map(u8_to_format_result)?,
+      ),
+      None => {
+        // not supported
+        Ok(WasmFormatResult::NoChange)
+      }
+    }
+  }
+
+  #[inline]
+  pub fn get_formatted_text(&mut self) -> Result<usize> {
+    let func = self.get_export::<(), u32>("get_formatted_text")?;
+    Ok(func.call(&mut self.store).map(|value| value as usize)?)
+  }
+
+  #[inline]
+  pub fn get_error_text(&mut self) -> Result<usize> {
+    let func = self.get_export::<(), u32>("get_error_text")?;
+    Ok(func.call(&mut self.store).map(|value| value as usize)?)
+  }
+
+  #[inline]
+  pub fn get_memory_view(&self) -> MemoryView {
+    self.memory.view(&self.store)
+  }
+
+  #[inline]
+  pub fn clear_shared_bytes(&mut self, capacity: usize) -> Result<WasmPtr<u32>> {
+    let clear_shared_bytes_func = self.get_export::<u32, WasmPtr<u32>>("clear_shared_bytes")?;
+    Ok(clear_shared_bytes_func.call(&mut self.store, capacity as u32)?)
+  }
+
+  #[inline]
+  pub fn get_shared_bytes_ptr(&mut self) -> Result<WasmPtr<u32>> {
+    let func = self.get_export::<(), WasmPtr<u32>>("get_shared_bytes_ptr")?;
+    Ok(func.call(&mut self.store)?)
+  }
+
+  fn get_export<Args, Rets>(&mut self, name: &str) -> Result<TypedFunction<Args, Rets>>
+  where
+    Args: WasmTypeList,
+    Rets: WasmTypeList,
+  {
+    let maybe_export = self.get_maybe_export(name)?;
+    match maybe_export {
+      Some(export) => Ok(export),
+      None => bail!("Could not find export '{}' in plugin.", name),
+    }
+  }
+
+  fn get_maybe_export<Args, Rets>(&mut self, name: &str) -> Result<Option<TypedFunction<Args, Rets>>>
+  where
+    Args: WasmTypeList,
+    Rets: WasmTypeList,
+  {
+    match self.instance.get_function(name) {
+      Ok(func) => match func.typed::<Args, Rets>(&self.store) {
+        Ok(native_func) => Ok(Some(native_func)),
+        Err(err) => bail!("Error creating function '{}'. Message: {:#}", name, err),
+      },
+      Err(err) => match err {
+        ExportError::IncompatibleType => {
+          bail!("Export '{}'. {:#}", name, err)
+        }
+        ExportError::Missing(_) => Ok(None),
+      },
+    }
+  }
+}
+
+fn u8_to_format_result(orig: u8) -> WasmFormatResult {
+  match orig {
+    0 => WasmFormatResult::NoChange,
+    1 => WasmFormatResult::Change,
+    2 => WasmFormatResult::Error,
+    _ => unreachable!(),
+  }
+}
diff --git a/crates/dprint/src/plugins/implementations/wasm/load_instance.rs b/crates/dprint/src/plugins/implementations/wasm/load_instance.rs
index 6384213e4..df113e361 100644
--- a/crates/dprint/src/plugins/implementations/wasm/load_instance.rs
+++ b/crates/dprint/src/plugins/implementations/wasm/load_instance.rs
@@ -1,20 +1,29 @@
+use std::sync::Arc;
+
 use anyhow::bail;
 use anyhow::Result;
+use dprint_core::plugins::CancellationToken;
 use wasmer::Cranelift;
 use wasmer::EngineBuilder;
 use wasmer::EngineRef;
+use wasmer::ExportError;
+use wasmer::Function;
 use wasmer::Imports;
 use wasmer::Instance;
+use wasmer::Memory;
 use wasmer::Module;
 use wasmer::Store;
 
 use super::instance::get_current_plugin_schema_version;
+use super::ImportObjectEnvironment;
 use super::PluginSchemaVersion;
 
-#[derive(Clone)]
 pub struct WasmInstance {
-  pub inner: wasmer::Instance,
-  pub engine: wasmer::Engine,
+  inner: wasmer::Instance,
+  // note: keep the engine alive for the duration of the instance
+  // otherwise it could be cleaned up before the instance is dropped
+  _engine: wasmer::Engine,
+  env: Box<dyn ImportObjectEnvironment>,
   version: PluginSchemaVersion,
 }
 
@@ -22,17 +31,33 @@ impl WasmInstance {
   pub fn version(&self) -> PluginSchemaVersion {
     self.version
   }
+
+  pub fn set_token(&self, store: &mut Store, token: Arc<dyn CancellationToken>) {
+    self.env.set_token(store, token);
+  }
+
+  pub fn get_memory(&self, name: &str) -> Result<&Memory, ExportError> {
+    self.inner.exports.get_memory(name)
+  }
+
+  pub fn get_function(&self, name: &str) -> Result<&Function, ExportError> {
+    self.inner.exports.get_function(name)
+  }
 }
 
 /// Loads a compiled wasm module from the specified bytes.
-pub fn load_instance(store: &mut Store, module: &WasmModule, import_object: &Imports) -> Result<WasmInstance> {
+pub fn load_instance(store: &mut Store, module: &WasmModule, env: Box<dyn ImportObjectEnvironment>, import_object: &Imports) -> Result<WasmInstance> {
   let instance = Instance::new(store, &module.inner, import_object);
   match instance {
-    Ok(instance) => Ok(WasmInstance {
-      inner: instance,
-      engine: module.engine.clone(),
-      version: module.version,
-    }),
+    Ok(instance) => {
+      env.initialize(store, &instance)?;
+      Ok(WasmInstance {
+        inner: instance,
+        _engine: module.engine.clone(),
+        env,
+        version: module.version,
+      })
+    }
     Err(err) => bail!("Error instantiating module: {:#}", err),
   }
 }
@@ -53,6 +78,10 @@ impl WasmModule {
     })
   }
 
+  pub fn version(&self) -> PluginSchemaVersion {
+    self.version
+  }
+
   pub fn inner(&self) -> &wasmer::Module {
     &self.inner
   }
diff --git a/crates/dprint/src/plugins/implementations/wasm/mod.rs b/crates/dprint/src/plugins/implementations/wasm/mod.rs
index d9cf21293..301d16a9e 100644
--- a/crates/dprint/src/plugins/implementations/wasm/mod.rs
+++ b/crates/dprint/src/plugins/implementations/wasm/mod.rs
@@ -1,12 +1,10 @@
 mod compile;
-mod import_object;
 mod instance;
 mod load_instance;
 mod plugin;
 mod setup_wasm_plugin;
 
 pub use compile::*;
-pub use import_object::*;
 use instance::*;
 pub use load_instance::WasmModuleCreator;
 use load_instance::*;
diff --git a/crates/dprint/src/plugins/implementations/wasm/plugin.rs b/crates/dprint/src/plugins/implementations/wasm/plugin.rs
index aaa68114a..5f0905339 100644
--- a/crates/dprint/src/plugins/implementations/wasm/plugin.rs
+++ b/crates/dprint/src/plugins/implementations/wasm/plugin.rs
@@ -5,9 +5,11 @@ use dprint_core::async_runtime::FutureExt;
 use dprint_core::async_runtime::LocalBoxFuture;
 use dprint_core::plugins::process::HostFormatCallback;
 use dprint_core::plugins::CancellationToken;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
 use dprint_core::plugins::ConfigChange;
 use dprint_core::plugins::CriticalFormatError;
 use dprint_core::plugins::FileMatchingInfo;
+use dprint_core::plugins::FormatRange;
 use dprint_core::plugins::FormatResult;
 use dprint_core::plugins::HostFormatRequest;
 use std::cell::RefCell;
@@ -61,15 +63,15 @@ impl<TEnvironment: Environment> Plugin for WasmPlugin<TEnvironment> {
   }
 
   async fn initialize(&self) -> Result<Rc<dyn InitializedPlugin>> {
+    let environment = self.environment.clone();
+    let plugin_name = self.info().name.clone();
     let plugin: Rc<dyn InitializedPlugin> = Rc::new(InitializedWasmPlugin::new(
-      self.info().name.to_string(),
+      plugin_name.clone(),
       self.module.clone(),
       Arc::new({
         move |store, module, host_format_sender| {
-          let (import_object, env) = create_pools_import_object(store, host_format_sender);
-          let instance = load_instance(store, module, &import_object)?;
-          env.as_mut(store).initialize(&instance.inner)?;
-          Ok(instance)
+          let (import_object, env) = create_pools_import_object(environment.clone(), &plugin_name, module.version(), store, host_format_sender);
+          load_instance(store, module, env, &import_object)
         }
       }),
       self.environment.clone(),
@@ -82,8 +84,10 @@ impl<TEnvironment: Environment> Plugin for WasmPlugin<TEnvironment> {
 struct WasmPluginFormatMessage {
   file_path: PathBuf,
   file_bytes: Vec<u8>,
+  range: FormatRange,
   config: Arc<FormatConfig>,
   override_config: ConfigKeyMap,
+  token: Arc<dyn CancellationToken>,
 }
 
 type WasmResponseSender<T> = tokio::sync::oneshot::Sender<T>;
@@ -91,6 +95,7 @@ type WasmResponseSender<T> = tokio::sync::oneshot::Sender<T>;
 enum WasmPluginMessage {
   LicenseText(WasmResponseSender<Result<String>>),
   ResolvedConfig(Arc<FormatConfig>, WasmResponseSender<Result<String>>),
+  CheckConfigUpdates(Arc<CheckConfigUpdatesMessage>, WasmResponseSender<Result<Vec<ConfigChange>>>),
   FileMatchingInfo(Arc<FormatConfig>, WasmResponseSender<Result<FileMatchingInfo>>),
   ConfigDiagnostics(Arc<FormatConfig>, WasmResponseSender<Result<Vec<ConfigurationDiagnostic>>>),
   FormatRequest(Arc<WasmPluginFormatMessage>, WasmResponseSender<FormatResult>),
@@ -101,7 +106,6 @@ type WasmPluginSender = std::sync::mpsc::Sender<WasmPluginMessage>;
 #[derive(Clone)]
 struct InstanceState {
   host_format_callback: HostFormatCallback,
-  token: Arc<dyn CancellationToken>,
 }
 
 struct WasmPluginSenderWithState {
@@ -231,12 +235,10 @@ impl<TEnvironment: Environment> InitializedWasmPlugin<TEnvironment> {
     dprint_core::async_runtime::spawn({
       let instance_state_cell = instance_state_cell.clone();
       async move {
-        while let Some((mut request, sender)) = host_format_rx.recv().await {
+        while let Some((request, sender)) = host_format_rx.recv().await {
           let instance_state = instance_state_cell.borrow().clone();
           match instance_state {
             Some(instance_state) => {
-              // forward the provided token on to the host format
-              request.token = instance_state.token.clone();
               let message = (instance_state.host_format_callback)(request).await;
               if sender.send(message).is_err() {
                 return; // disconnected
@@ -285,6 +287,12 @@ impl<TEnvironment: Environment> InitializedWasmPlugin<TEnvironment> {
                 break; // disconnected
               }
             }
+            WasmPluginMessage::CheckConfigUpdates(message, response) => {
+              let result = instance.check_config_updates(&message);
+              if response.send(result).is_err() {
+                break; // disconnected
+              }
+            }
             WasmPluginMessage::ConfigDiagnostics(config, response) => {
               let result = instance.config_diagnostics(&config);
               if response.send(result).is_err() {
@@ -304,7 +312,14 @@ impl<TEnvironment: Environment> InitializedWasmPlugin<TEnvironment> {
               }
             }
             WasmPluginMessage::FormatRequest(request, response) => {
-              let result = instance.format_text(&request.file_path, &request.file_bytes, &request.config, &request.override_config);
+              let result = instance.format_text(
+                &request.file_path,
+                &request.file_bytes,
+                request.range.clone(),
+                &request.config,
+                &request.override_config,
+                request.token.clone(),
+              );
               if response.send(result).is_err() {
                 break; // disconnected
               }
@@ -387,28 +402,35 @@ impl<TEnvironment: Environment> InitializedPlugin for InitializedWasmPlugin<TEnv
       .await
   }
 
-  async fn check_config_updates(&self, _plugin_config: ConfigKeyMap) -> Result<Vec<ConfigChange>> {
-    Ok(Vec::new()) // not supported atm
+  async fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
+    let message = Arc::new(message);
+    self
+      .with_instance(None, move |plugin_sender| {
+        let message = message.clone();
+        async move {
+          let (tx, rx) = tokio::sync::oneshot::channel();
+          plugin_sender.send(WasmPluginMessage::CheckConfigUpdates(message, tx))?;
+          rx.await?
+        }
+        .boxed_local()
+      })
+      .await
   }
 
   async fn format_text(&self, request: InitializedPluginFormatRequest) -> FormatResult {
-    // Wasm plugins do not currently support range formatting
-    // so always return back None for now.
-    if request.range.is_some() {
-      return Ok(None);
-    }
     if request.token.is_cancelled() {
       return Ok(None);
     }
     let message = Arc::new(WasmPluginFormatMessage {
       file_path: request.file_path,
       file_bytes: request.file_text,
+      range: request.range,
       config: request.config,
       override_config: request.override_config,
+      token: request.token,
     });
     let instance_state = InstanceState {
       host_format_callback: request.on_host_format,
-      token: request.token,
     };
     self
       .with_instance(Some(instance_state), move |plugin_sender| {
diff --git a/crates/dprint/src/plugins/plugin.rs b/crates/dprint/src/plugins/plugin.rs
index ba0038abb..f4bd37122 100644
--- a/crates/dprint/src/plugins/plugin.rs
+++ b/crates/dprint/src/plugins/plugin.rs
@@ -9,6 +9,7 @@ use dprint_core::configuration::ConfigurationDiagnostic;
 use dprint_core::configuration::GlobalConfiguration;
 use dprint_core::plugins::process::HostFormatCallback;
 use dprint_core::plugins::CancellationToken;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
 use dprint_core::plugins::ConfigChange;
 use dprint_core::plugins::FileMatchingInfo;
 use dprint_core::plugins::FormatConfigId;
@@ -29,7 +30,7 @@ pub trait Plugin: Send + Sync {
 
 pub struct FormatConfig {
   pub id: FormatConfigId,
-  pub raw: ConfigKeyMap,
+  pub plugin: ConfigKeyMap,
   pub global: GlobalConfiguration,
 }
 
@@ -54,7 +55,7 @@ pub trait InitializedPlugin {
   /// Gets the configuration diagnostics.
   async fn config_diagnostics(&self, config: Arc<FormatConfig>) -> Result<Vec<ConfigurationDiagnostic>>;
   /// Checks for any configuration changes based on the provided plugin config.
-  async fn check_config_updates(&self, plugin_config: ConfigKeyMap) -> Result<Vec<ConfigChange>>;
+  async fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>>;
   /// Formats the text in memory based on the file path and file text.
   async fn format_text(&self, format_request: InitializedPluginFormatRequest) -> FormatResult;
   /// Shuts down the plugin. This is used for process plugins.
@@ -127,7 +128,7 @@ impl InitializedPlugin for InitializedTestPlugin {
     Ok(vec![])
   }
 
-  async fn check_config_updates(&self, _plugin_config: ConfigKeyMap) -> Result<Vec<ConfigChange>> {
+  async fn check_config_updates(&self, _message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
     Ok(Vec::new())
   }
 
diff --git a/crates/dprint/src/plugins/repo/read_info_file.rs b/crates/dprint/src/plugins/repo/read_info_file.rs
index ca7fcbd5d..00939d6ee 100644
--- a/crates/dprint/src/plugins/repo/read_info_file.rs
+++ b/crates/dprint/src/plugins/repo/read_info_file.rs
@@ -193,7 +193,7 @@ mod test {
       assert_eq!(
         info_file,
         InfoFile {
-          plugin_system_schema_version: 3,
+          plugin_system_schema_version: 4,
           latest_plugins: vec![
             InfoFilePluginInfo {
               name: "dprint-plugin-typescript".to_string(),
diff --git a/crates/dprint/src/resolution.rs b/crates/dprint/src/resolution.rs
index e80d828b7..faaa0f710 100644
--- a/crates/dprint/src/resolution.rs
+++ b/crates/dprint/src/resolution.rs
@@ -13,6 +13,7 @@ use dprint_core::async_runtime::LocalBoxFuture;
 use dprint_core::configuration::ConfigKeyMap;
 use dprint_core::plugins::process::HostFormatCallback;
 use dprint_core::plugins::CancellationToken;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
 use dprint_core::plugins::ConfigChange;
 use dprint_core::plugins::CriticalFormatError;
 use dprint_core::plugins::FileMatchingInfo;
@@ -84,7 +85,7 @@ impl PluginWithConfig {
     hasher.write(self.info().version.as_bytes());
 
     // serialize the config keys in order to prevent the hash from changing
-    let sorted_config = self.format_config.raw.iter().collect::<BTreeMap<_, _>>();
+    let sorted_config = self.format_config.plugin.iter().collect::<BTreeMap<_, _>>();
     for (key, value) in sorted_config {
       hasher.write(key.as_bytes());
       value.hash(hasher);
@@ -179,8 +180,8 @@ impl InitializedPluginWithConfig {
     output_plugin_config_diagnostics(&self.info().name, &*self.instance, self.plugin.format_config.clone(), environment).await
   }
 
-  pub async fn check_config_updates(&self, plugin_config: ConfigKeyMap) -> Result<Vec<ConfigChange>> {
-    self.instance.check_config_updates(plugin_config).await
+  pub async fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
+    self.instance.check_config_updates(message).await
   }
 
   pub async fn format_text(&self, request: InitializedPluginWithConfigFormatRequest) -> FormatResult {
@@ -423,6 +424,10 @@ impl<TEnvironment: Environment> PluginsScopeAndPathsCollection<TEnvironment> {
     Ok(())
   }
 
+  pub fn len(&self) -> usize {
+    self.inner.len()
+  }
+
   pub fn iter(&self) -> impl Iterator<Item = &PluginsScopeAndPaths<TEnvironment>> {
     self.inner.iter()
   }
@@ -573,7 +578,7 @@ pub async fn resolve_plugins_scope<TEnvironment: Environment>(
       let format_config = Arc::new(FormatConfig {
         id: next_config_id,
         global: global_config,
-        raw: plugin_config.properties,
+        plugin: plugin_config.properties,
       });
       let file_matching_info = instance.file_matching_info(format_config.clone()).await?;
       Ok::<_, anyhow::Error>(Rc::new(PluginWithConfig::new(
diff --git a/crates/dprint/src/utils/glob/glob_matcher.rs b/crates/dprint/src/utils/glob/glob_matcher.rs
index 83ff5ac44..b82132eea 100644
--- a/crates/dprint/src/utils/glob/glob_matcher.rs
+++ b/crates/dprint/src/utils/glob/glob_matcher.rs
@@ -122,11 +122,14 @@ impl GlobMatcher {
         // this is a very strange state that we want to know more about,
         // so just always log directly to stderr in this scenario and maybe
         // eventually remove this code.
-        eprintln!(
-          "WARNING: Path prefix error for {} and {}. Please report this error in issue #540.",
-          &self.base_dir.display(),
-          path.display()
-        );
+        #[allow(clippy::print_stderr)]
+        {
+          eprintln!(
+            "WARNING: Path prefix error for {} and {}. Please report this error in issue #540.",
+            &self.base_dir.display(),
+            path.display()
+          );
+        }
         return GlobMatchesDetail::NotMatched;
       }
     } else if !path.is_absolute() {
diff --git a/docs/wasm-plugin-development.md b/docs/wasm-plugin-development.md
index 99fd36ea3..9d317f980 100644
--- a/docs/wasm-plugin-development.md
+++ b/docs/wasm-plugin-development.md
@@ -1,4 +1,4 @@
-# Creating a Wasm Plugin (Schema Version 3)
+# Creating a Wasm Plugin (Schema Version 4)
 
 Wasm plugins are the preferred way of developing plugins (as opposed to process plugins) because they are portable and run sandboxed in a Wasm runtime. They can be written in any language that supports compiling to a WebAssembly file (_.wasm_)—emscripten solutions do not work.
 
@@ -50,13 +50,8 @@ Implementing a Wasm plugin is easier if you're using Rust as there are several h
 
    use crate::configuration::Configuration; // import the Configuration from above
 
-   pub struct MyPluginHandler {}
-
-   impl MyPluginHandler {
-     const fn new() -> Self {
-       MyPluginHandler {}
-     }
-   }
+   #[derive(Default)]
+   pub struct MyPluginHandler;
 
    impl SyncPluginHandler<Configuration> for MyPluginHandler {
      fn plugin_info(&mut self) -> PluginInfo {
@@ -64,8 +59,6 @@ Implementing a Wasm plugin is easier if you're using Rust as there are several h
          name: env!("CARGO_PKG_NAME").to_string(),
          version: env!("CARGO_PKG_VERSION").to_string(),
          config_key: "keyGoesHere".to_string(),
-         file_extensions: vec!["txt_ps".to_string()],
-         file_names: vec![],
          help_url: "".to_string(),          // fill this in
          config_schema_url: "".to_string(), // leave this empty for now
          update_url: None,                  // leave this empty for now
@@ -76,7 +69,7 @@ Implementing a Wasm plugin is easier if you're using Rust as there are several h
        "License text goes here.".to_string()
      }
 
-     fn resolve_config(&mut self, config: ConfigKeyMap, global_config: &GlobalConfiguration) -> ResolveConfigurationResult<Configuration> {
+     fn resolve_config(&mut self, config: ConfigKeyMap, global_config: &GlobalConfiguration) -> PluginResolveConfigurationResult<Configuration> {
        // implement this... for example
        let mut config = config;
        let mut diagnostics = Vec::new();
@@ -87,6 +80,11 @@ Implementing a Wasm plugin is easier if you're using Rust as there are several h
        ResolveConfigurationResult {
          config: Configuration { line_width },
          diagnostics,
+         file_matching: FileMatchingInfo {
+          // these can be derived from the config
+          file_extensions: vec!["txt".to_string()],
+          file_names: vec![],
+         }
        }
      }
 
@@ -95,6 +93,7 @@ Implementing a Wasm plugin is easier if you're using Rust as there are several h
        file_path: &Path,
        file_text: &str,
        config: &Configuration,
+       token: &dyn dprint_core::plugins::CancellationToken,
        mut format_with_host: impl FnMut(&Path, String, &ConfigKeyMap) -> Result<Option<String>>,
      ) -> Result<Option<String>> {
        // format here
@@ -106,7 +105,7 @@ Implementing a Wasm plugin is easier if you're using Rust as there are several h
 
    ```rust
    // specify the plugin struct name and then an expression to create it
-   generate_plugin_code!(MyPluginHandler, MyPluginHandler::new());
+   generate_plugin_code!(MyPluginHandler, MyPluginHandler::default());
    ```
 
 6. Finally, compile with:
@@ -121,36 +120,26 @@ To format code using a different plugin, call the `format_with_host(file_path, f
 
 For example, this function is used by the markdown plugin to format code blocks.
 
-## Schema Version 3 Overview
+## Schema Version 4 Overview
 
 If you are not using `Rust`, then you must implement a lot of low level functionality.
 
-How it works:
-
-1. The Wasm plugin initializes a shared memory buffer. Data between the CLI and Wasm plugin is transferred in here.
-2. Generally, the Wasm plugin stores its own local byte array. The plugin should copy from the shared memory buffer into this byte array for composing the final received data or copy from this array into the shared memory buffer for sending back data. This is referred to as "shared bytes" below, but that could probably have used a better name like "local bytes" or something.
-3. The CLI/host will communicate by calling the Wasm exports.
-4. The plugin may get the CLI/host to format other text by using the provided Wasm imports.
-
 ## Wasm Exports
 
 Low level communication:
 
-- `get_wasm_memory_buffer_size() -> usize` - Called to get the size of the shared Wasm memory buffer.
-- `get_wasm_memory_buffer() -> *const u8` - Called to get a pointer to the Wasm memory buffer.
-- `clear_shared_bytes(capacity: usize)` - Called to get the plugin to clear its local byte array.
-- `set_buffer_with_shared_bytes(offset: usize, length: usize)` - Gets the plugin to set the Wasm memory buffer with the local byte array at the specified position and length.
-- `add_to_shared_bytes_from_buffer(length: usize)` - Gets the plugin to add to its shared bytes from the Wasm memory buffer. The plugin should keep track of the current index.
+- `get_shared_bytes_ptr() -> *const u8` - Called to get a pointer to the shared Wasm memory buffer.
+- `clear_shared_bytes(size: u32) -> *const u8` - Called to get the plugin to clear its shared byte array and return a pointer to it.
 
 Initialization functions:
 
-- `get_plugin_schema_version() -> u32` - Return `3`
-- `set_global_config()` - Called when the global configuration is done transferring over. Store it somewhere.
-- `set_plugin_config()` - Called when the plugin specific configuration is done transferring over. Store it somewhere.
-- `get_config_diagnostics() -> usize` - Called by the CLI to get the configuration diagnostics. Serialize the diagnostics as a JSON string, store it in the local bytes, and return the byte length.
-- `get_resolved_config() -> usize` - Called by the CLI to get the resolved configuration for display in the CLI. Serialize it as a JSON string, store it in the local bytes, and return the byte length.
-- `get_license_text() -> usize` - Store the plugin's license text in the local bytes and return the byte length.
-- `get_plugin_info() -> usize` - Store the plugin's JSON serialized information in the local bytes and return the byte length. The plugin info is a JSON object with the following properties:
+- `dprint_plugin_version_4() -> u32` - Return `4`, but the CLI never calls this function (it only checks for it in the exports)
+- `register_config(config_id: u32)` - Called when the plugin and global configuration is done transferring over. Store it somewhere.
+- `release_config(config_id: u32)` - Release the config from memory.
+- `get_config_diagnostics(config_id: u32) -> u32` - Called by the CLI to get the configuration diagnostics. Serialize the diagnostics as a JSON string, store it in the local bytes, and return the byte length.
+- `get_resolved_config(config_id: u32) -> u32` - Called by the CLI to get the resolved configuration for display in the CLI. Serialize it as a JSON string, store it in the local bytes, and return the byte length.
+- `get_license_text() -> u32` - Store the plugin's license text in the local bytes and return the byte length.
+- `get_plugin_info() -> u32` - Store the plugin's JSON serialized information in the local bytes and return the byte length. The plugin info is a JSON object with the following properties:
   - `name` - String saying the plugin name.
   - `version` - Version of the plugin (ex. `"0.1.0"`)
   - `configKey` - Configuration key to use for this plugin in the dprint configuration file.
@@ -162,12 +151,20 @@ Formatting functions:
 
 - `set_file_path()` - Called by the CLI for the plugin to take from its local byte array and store that data as the file path.
 - `set_override_config()` - Possibly called by the CLI for the plugin to take from its local byte array and store that data as the format specific configuration.
-- `format() -> u8`
+- `format(config_id: u32) -> u32`
   - Return `0` when there's no change.
   - `1` when there's a change.
   - `2` when there's an error.
-- `get_formatted_text() -> usize` - Plugin should put the formatted text into its local byte array and return the size of that data.
-- `get_error_text() -> usize` - Plugin should put the error text into its local byte array and return the size of that data.
+- `get_formatted_text() -> u32` - Plugin should put the formatted text into its local byte array and return the size of that data.
+- `get_error_text() -> u32` - Plugin should put the error text into its local byte array and return the size of that data.
+
+Optional functions:
+
+- `check_config_updates() -> u32` - Set the shared bytes with the input. Returns the length of the output which can be read from the shared bytes.
+  - Input: todo...
+  - Output: todo...
+- `format_range(config_id: u32, range_start: u32, range_end: u32) -> u32`
+  - Response is same as `format`
 
 ### Wasm Imports
 
@@ -177,19 +174,16 @@ Communication is done by using a shared Wasm buffer. Essentially, the plugin sto
 
 Low level communication:
 
-- `host_clear_bytes(length: u32)` - Tell the host to clear its local byte array and reinitialize it with the provided length.
-- `host_read_buffer(pointer: u32, length: u32)` - Tell the host to read from provided Wasm memory address and store it in its local byte array.
-- `host_write_buffer(pointer: u32, offset: u32, length: u32)` - Tell the host to write to the provided Wasm memory address using the provided offset and length of its local byte array.
+- `host_write_buffer(pointer: u32)` - Tell the host to write data to the provided Wasm memory address.
 
 High level functions:
 
-- `host_take_file_path()` - Tell the host to take the file path from its local byte array.
-- `host_take_override_config()` - Tell the host to take the override configuration from its local byte array.
-- `host_format() -> u8` - Tell the host to format using the file text in its local byte array.
+- `host_format(file_path_ptr: u32, file_path_len: u32, range_start: u32, range_end: u32, override_cfg_ptr: u32, override_cfg_len: u32, file_bytes_ptr: u32, file_bytes_len: u32) -> u32` - Tell the host to format using the file text in its local byte array. Provide `0` and `file_bytes_len` for no range formatting.
   - Returns `0` for no change (do nothing else, no transfer needed)
   - `1` for change (use `host_get_formatted_text()`)
   - `2` for error (use `host_get_error_text()`)
 - `host_get_formatted_text() -> u32` - Tell the host to store the formatted text in its local byte array and return back the byte length of that text.
 - `host_get_error_text() -> u32` - Tell the host to store the error text in its local byte array and return back the byte length of that error message.
+- `host_has_cancelled() -> u32` - Check if the host has cancelled the formatting request (`1`) or not (`0`).
 
 I recommend looking in the [`dprint-core` wasm module](https://github.com/dprint/dprint/blob/main/crates/core/src/plugins/wasm/mod.rs) for how to use these.
diff --git a/website/playground/package-lock.json b/website/playground/package-lock.json
index 0bc297c83..42f40c348 100644
--- a/website/playground/package-lock.json
+++ b/website/playground/package-lock.json
@@ -8,7 +8,8 @@
       "name": "playground",
       "version": "0.1.0",
       "dependencies": {
-        "@dprint/formatter": "^0.2.0",
+        "@dprint/formatter": "^0.4.1",
+        "allotment": "^1.20.2",
         "lz-string": "^1.4.4",
         "monaco-editor": "^0.31.1",
         "react": "^17.0.2",
@@ -16,7 +17,6 @@
         "react-monaco-editor": "^0.47.0",
         "react-scripts": "5.0.0",
         "react-spinners": "^0.11.0",
-        "react-split-pane": "^0.1.92",
         "workerize-loader": "^1.3.0"
       },
       "devDependencies": {
@@ -1855,9 +1855,10 @@
       "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg=="
     },
     "node_modules/@dprint/formatter": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/@dprint/formatter/-/formatter-0.2.0.tgz",
-      "integrity": "sha512-E1q1JaOPeEUBhG//IUayqJQvNpqprZ0OCF8B/bIhUqSsMIp0Y74PgF8JPvDCrOdq43qiGUYAavVO8iCBHOoU/A=="
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@dprint/formatter/-/formatter-0.4.1.tgz",
+      "integrity": "sha512-IB/GXdlMOvi0UhQQ9mcY15Fxcrc2JPadmo6tqefCNV0bptFq7YBpggzpqYXldBXDa04CbKJ+rDwO2eNRPE2+/g==",
+      "license": "MIT"
     },
     "node_modules/@emotion/cache": {
       "version": "11.4.0",
@@ -2685,6 +2686,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/@juggle/resize-observer": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
+      "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
+      "license": "Apache-2.0"
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3959,6 +3966,30 @@
         "ajv": "^6.9.1"
       }
     },
+    "node_modules/allotment": {
+      "version": "1.20.2",
+      "resolved": "https://registry.npmjs.org/allotment/-/allotment-1.20.2.tgz",
+      "integrity": "sha512-TaCuHfYNcsJS9EPk04M7TlG5Rl3vbAdHeAyrTE9D5vbpzV+wxnRoUrulDbfnzaQcPIZKpHJNixDOoZNuzliKEA==",
+      "license": "MIT",
+      "dependencies": {
+        "classnames": "^2.3.0",
+        "eventemitter3": "^5.0.0",
+        "lodash.clamp": "^4.0.0",
+        "lodash.debounce": "^4.0.0",
+        "lodash.isequal": "^4.5.0",
+        "use-resize-observer": "^9.0.0"
+      },
+      "peerDependencies": {
+        "react": "^17.0.0 || ^18.0.0",
+        "react-dom": "^17.0.0 || ^18.0.0"
+      }
+    },
+    "node_modules/allotment/node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+      "license": "MIT"
+    },
     "node_modules/ansi-escapes": {
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -4841,6 +4872,12 @@
       "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
       "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
     },
+    "node_modules/classnames": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+      "license": "MIT"
+    },
     "node_modules/clean-css": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz",
@@ -10451,11 +10488,23 @@
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
     },
+    "node_modules/lodash.clamp": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz",
+      "integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==",
+      "license": "MIT"
+    },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
     },
+    "node_modules/lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+      "license": "MIT"
+    },
     "node_modules/lodash.memoize": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -12952,11 +13001,6 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
-    "node_modules/react-lifecycles-compat": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
-      "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
-    },
     "node_modules/react-monaco-editor": {
       "version": "0.47.0",
       "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.47.0.tgz",
@@ -13062,28 +13106,6 @@
         "react-dom": "^16.0.0 || ^17.0.0"
       }
     },
-    "node_modules/react-split-pane": {
-      "version": "0.1.92",
-      "resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.92.tgz",
-      "integrity": "sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==",
-      "dependencies": {
-        "prop-types": "^15.7.2",
-        "react-lifecycles-compat": "^3.0.4",
-        "react-style-proptype": "^3.2.2"
-      },
-      "peerDependencies": {
-        "react": "^16.0.0-0",
-        "react-dom": "^16.0.0-0"
-      }
-    },
-    "node_modules/react-style-proptype": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/react-style-proptype/-/react-style-proptype-3.2.2.tgz",
-      "integrity": "sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==",
-      "dependencies": {
-        "prop-types": "^15.5.4"
-      }
-    },
     "node_modules/readable-stream": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -14852,6 +14874,19 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/use-resize-observer": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz",
+      "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==",
+      "license": "MIT",
+      "dependencies": {
+        "@juggle/resize-observer": "^3.3.1"
+      },
+      "peerDependencies": {
+        "react": "16.8.0 - 18",
+        "react-dom": "16.8.0 - 18"
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -17101,9 +17136,9 @@
       "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg=="
     },
     "@dprint/formatter": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/@dprint/formatter/-/formatter-0.2.0.tgz",
-      "integrity": "sha512-E1q1JaOPeEUBhG//IUayqJQvNpqprZ0OCF8B/bIhUqSsMIp0Y74PgF8JPvDCrOdq43qiGUYAavVO8iCBHOoU/A=="
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@dprint/formatter/-/formatter-0.4.1.tgz",
+      "integrity": "sha512-IB/GXdlMOvi0UhQQ9mcY15Fxcrc2JPadmo6tqefCNV0bptFq7YBpggzpqYXldBXDa04CbKJ+rDwO2eNRPE2+/g=="
     },
     "@emotion/cache": {
       "version": "11.4.0",
@@ -17721,6 +17756,11 @@
         }
       }
     },
+    "@juggle/resize-observer": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
+      "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
+    },
     "@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -18667,6 +18707,26 @@
       "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
       "requires": {}
     },
+    "allotment": {
+      "version": "1.20.2",
+      "resolved": "https://registry.npmjs.org/allotment/-/allotment-1.20.2.tgz",
+      "integrity": "sha512-TaCuHfYNcsJS9EPk04M7TlG5Rl3vbAdHeAyrTE9D5vbpzV+wxnRoUrulDbfnzaQcPIZKpHJNixDOoZNuzliKEA==",
+      "requires": {
+        "classnames": "^2.3.0",
+        "eventemitter3": "^5.0.0",
+        "lodash.clamp": "^4.0.0",
+        "lodash.debounce": "^4.0.0",
+        "lodash.isequal": "^4.5.0",
+        "use-resize-observer": "^9.0.0"
+      },
+      "dependencies": {
+        "eventemitter3": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+          "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
+        }
+      }
+    },
     "ansi-escapes": {
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -19330,6 +19390,11 @@
       "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
       "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
     },
+    "classnames": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
+    },
     "clean-css": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz",
@@ -23379,11 +23444,21 @@
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
     },
+    "lodash.clamp": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz",
+      "integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg=="
+    },
     "lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
     },
+    "lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
+    },
     "lodash.memoize": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -25077,11 +25152,6 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
-    "react-lifecycles-compat": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
-      "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
-    },
     "react-monaco-editor": {
       "version": "0.47.0",
       "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.47.0.tgz",
@@ -25158,24 +25228,6 @@
         "@emotion/react": "^11.1.4"
       }
     },
-    "react-split-pane": {
-      "version": "0.1.92",
-      "resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.92.tgz",
-      "integrity": "sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==",
-      "requires": {
-        "prop-types": "^15.7.2",
-        "react-lifecycles-compat": "^3.0.4",
-        "react-style-proptype": "^3.2.2"
-      }
-    },
-    "react-style-proptype": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/react-style-proptype/-/react-style-proptype-3.2.2.tgz",
-      "integrity": "sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==",
-      "requires": {
-        "prop-types": "^15.5.4"
-      }
-    },
     "readable-stream": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -26461,6 +26513,14 @@
         "punycode": "^2.1.0"
       }
     },
+    "use-resize-observer": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz",
+      "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==",
+      "requires": {
+        "@juggle/resize-observer": "^3.3.1"
+      }
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/website/playground/package.json b/website/playground/package.json
index 451eb2cdc..ae035126f 100644
--- a/website/playground/package.json
+++ b/website/playground/package.json
@@ -4,7 +4,8 @@
   "private": true,
   "homepage": "https://dprint.dev/playground",
   "dependencies": {
-    "@dprint/formatter": "^0.2.0",
+    "@dprint/formatter": "^0.4.1",
+    "allotment": "^1.20.2",
     "lz-string": "^1.4.4",
     "monaco-editor": "^0.31.1",
     "react": "^17.0.2",
@@ -12,7 +13,6 @@
     "react-monaco-editor": "^0.47.0",
     "react-scripts": "5.0.0",
     "react-spinners": "^0.11.0",
-    "react-split-pane": "^0.1.92",
     "workerize-loader": "^1.3.0"
   },
   "scripts": {
diff --git a/website/playground/src/FormatterWorker.ts b/website/playground/src/FormatterWorker.ts
index 2ba866d62..731e2eedb 100644
--- a/website/playground/src/FormatterWorker.ts
+++ b/website/playground/src/FormatterWorker.ts
@@ -1,10 +1,11 @@
 // eslint-disable-next-line import/no-webpack-loader-syntax
 import createWorker from "workerize-loader!./formatter.worker";
 
-import type { PluginInfo } from "@dprint/formatter";
+import type { FileMatchingInfo, PluginInfo } from "@dprint/formatter";
 
 const formatterWorker = createWorker();
 const pluginInfoListeners: ((info: PluginInfo) => void)[] = [];
+const fileMatchingListeners: ((info: FileMatchingInfo) => void)[] = [];
 const formatListeners: ((text: string) => void)[] = [];
 const errorListeners: ((err: string) => void)[] = [];
 
@@ -15,6 +16,11 @@ formatterWorker.addEventListener("message", ev => {
         listener(ev.data.info);
       }
       break;
+    case "FileMatching":
+      for (const listener of fileMatchingListeners) {
+        listener(ev.data.info);
+      }
+      break;
     case "Format":
       for (const listener of formatListeners) {
         listener(ev.data.text);
@@ -61,6 +67,17 @@ export function removeOnPluginInfo(listener: (info: PluginInfo) => void) {
   }
 }
 
+export function addOnFileMatchingInfo(listener: (info: FileMatchingInfo) => void) {
+  fileMatchingListeners.push(listener);
+}
+
+export function removeOnFileMatchingInfo(listener: (info: FileMatchingInfo) => void) {
+  const index = fileMatchingListeners.indexOf(listener);
+  if (index >= 0) {
+    fileMatchingListeners.splice(index, 1);
+  }
+}
+
 export function addOnFormat(listener: (text: string) => void) {
   formatListeners.push(listener);
 }
diff --git a/website/playground/src/Playground.css b/website/playground/src/Playground.css
index cfe90680d..20e9a2ed6 100644
--- a/website/playground/src/Playground.css
+++ b/website/playground/src/Playground.css
@@ -1,14 +1,33 @@
 /* header */
-.appHeader {
+html,
+body,
+#App,
+#root {
+  width: 100%;
+  height: 100%;
+}
+
+#App {
+  display: flex;
+  flex-direction: column;
+}
+
+#AppHeader {
   background-color: #282c34;
   display: flex;
   width: 100%;
+  height: 53px;
   justify-content: space-between;
   align-items: center;
   padding: 0;
   border-bottom: 1px solid #000;
 }
 
+#AppBody {
+  width: 100%;
+  flex-grow: 1;
+}
+
 h1 {
   margin: 0;
   padding-left: 16px;
diff --git a/website/playground/src/Playground.tsx b/website/playground/src/Playground.tsx
index c9ea1b502..ca48eeab1 100644
--- a/website/playground/src/Playground.tsx
+++ b/website/playground/src/Playground.tsx
@@ -1,13 +1,15 @@
-import type { PluginInfo } from "@dprint/formatter";
+import type { FileMatchingInfo, PluginInfo } from "@dprint/formatter";
+import { Allotment } from "allotment";
 import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react";
-import SplitPane from "react-split-pane";
 import { CodeEditor, ExternalLink } from "./components";
 import { Spinner } from "./components";
 import "./external/react-splitpane.css";
 import * as formatterWorker from "./FormatterWorker";
-import "./Playground.css";
 import { getLanguageFromPluginUrl } from "./plugins";
 
+import "./Playground.css";
+import "allotment/dist/style.css";
+
 export interface PlaygroundProps {
   configText: string;
   onConfigTextChanged: (text: string) => void;
@@ -15,6 +17,7 @@ export interface PlaygroundProps {
   onTextChanged: (text: string) => void;
   formattedText: string;
   selectedPluginInfo: PluginInfo;
+  fileMatchingInfo: FileMatchingInfo;
   selectedPluginUrl: string;
   pluginUrls: string[];
   onSelectPluginUrl: (pluginUrl: string) => void;
@@ -27,6 +30,7 @@ export function Playground({
   text,
   onTextChanged,
   formattedText,
+  fileMatchingInfo,
   selectedPluginUrl,
   selectedPluginInfo,
   pluginUrls,
@@ -37,8 +41,12 @@ export function Playground({
   const [fileExtension, setFileExtension] = useState<string | undefined>(undefined);
 
   useEffect(() => {
-    setFileExtension(selectedPluginInfo.fileExtensions[0]);
-  }, [selectedPluginInfo]);
+    if (fileMatchingInfo.fileExtensions.length > 0) {
+      if (fileExtension == null || !fileMatchingInfo.fileExtensions.includes(fileExtension)) {
+        setFileExtension(fileMatchingInfo.fileExtensions[0]);
+      }
+    }
+  }, [fileMatchingInfo, fileExtension]);
 
   useEffect(() => {
     const timeout = setTimeout(() => {
@@ -81,93 +89,84 @@ export function Playground({
   }, [setFileExtension]);
 
   return (
-    <div className="App">
-      <SplitPane split="horizontal" defaultSize={53} allowResize={false}>
-        <header className="appHeader">
-          <h1 id="title">dprint - Playground</h1>
-          <div id="headerRight">
-            <a href="/overview">Overview</a>
-            <a href="/playground">Playground</a>
-            <a href="/sponsor">Sponsor</a>
-            <ExternalLink url="https://github.com/dprint/dprint" text="View on GitHub" />
-          </div>
-        </header>
-        <SplitPane
-          split="vertical"
-          minSize={50}
-          defaultSize="50%"
-          allowResize={true}
-          pane1Style={{ overflowX: "hidden", overflowY: "hidden" }}
-          pane2Style={{ overflowX: "hidden", overflowY: "hidden" }}
-        >
-          <SplitPane
-            split="horizontal"
-            allowResize={true}
-            defaultSize="60%"
-            pane1Style={{ overflowX: "hidden", overflowY: "hidden" }}
-            pane2Style={{ overflowX: "hidden", overflowY: "hidden" }}
-          >
-            <div className="container">
-              <div className="playgroundSubTitle">
-                <div className="row">
-                  <div className="column">
-                    Plugin:
-                  </div>
-                  <div className="column" style={{ flex: 1, display: "flex" }}>
-                    <select
-                      onChange={e => {
-                        if (e.target.selectedIndex >= pluginUrls.length) {
-                          let url = prompt("Please provide a Wasm plugin url:", "");
-                          if (url != null && url.trim().length > 0) {
-                            onSelectPluginUrl(url);
+    <div id="App">
+      <header id="AppHeader">
+        <h1 id="title">dprint - Playground</h1>
+        <div id="headerRight">
+          <a href="/overview">Overview</a>
+          <a href="/playground">Playground</a>
+          <a href="/sponsor">Sponsor</a>
+          <ExternalLink url="https://github.com/dprint/dprint" text="View on GitHub" />
+        </div>
+      </header>
+      <div id="AppBody">
+        <Allotment>
+          <Allotment.Pane preferredSize="50%">
+            <Allotment vertical={true}>
+              <div className="container">
+                <div className="playgroundSubTitle">
+                  <div className="row">
+                    <div className="column">
+                      Plugin:
+                    </div>
+                    <div className="column" style={{ flex: 1, display: "flex" }}>
+                      <select
+                        onChange={e => {
+                          if (e.target.selectedIndex >= pluginUrls.length) {
+                            let url = prompt("Please provide a Wasm plugin url:", "");
+                            if (url != null && url.trim().length > 0) {
+                              onSelectPluginUrl(url);
+                            } else {
+                              e.preventDefault();
+                            }
                           } else {
-                            e.preventDefault();
+                            onSelectPluginUrl(pluginUrls[e.target.selectedIndex]);
                           }
-                        } else {
-                          onSelectPluginUrl(pluginUrls[e.target.selectedIndex]);
-                        }
-                      }}
-                      style={{ flex: 1 }}
-                      value={selectedPluginUrl}
-                    >
-                      {pluginUrls.map((pluginUrl, i) => {
-                        return (
-                          <option key={i} value={pluginUrl}>
-                            {pluginUrl}
-                          </option>
-                        );
-                      })}
-                      <option key="custom">Custom</option>
-                    </select>
-                  </div>
-                  <div className="column" style={{ display: "flex" }}>
-                    <select value={fileExtension} onChange={onFileExtensionChange}>
-                      {selectedPluginInfo.fileExtensions.map((ext, i) => <option key={i} value={ext}>{"."}{ext}</option>)}
-                    </select>
+                        }}
+                        style={{ flex: 1 }}
+                        value={selectedPluginUrl}
+                      >
+                        {pluginUrls.map((pluginUrl, i) => {
+                          return (
+                            <option key={i} value={pluginUrl}>
+                              {pluginUrl}
+                            </option>
+                          );
+                        })}
+                        <option key="custom">Custom</option>
+                      </select>
+                    </div>
+                    <div className="column" style={{ display: "flex" }}>
+                      <select value={fileExtension} onChange={onFileExtensionChange}>
+                        {fileMatchingInfo.fileExtensions.map((ext, i) => <option key={i} value={ext}>{"."}{ext}</option>)}
+                      </select>
+                    </div>
                   </div>
                 </div>
+                <CodeEditor
+                  language={getLanguageFromPluginUrl(selectedPluginUrl) ?? "plaintext"}
+                  onChange={onTextChanged}
+                  text={text}
+                  lineWidth={lineWidth}
+                  onScrollTopChange={setScrollTop}
+                  scrollTop={scrollTop}
+                />
               </div>
-              <CodeEditor
-                language={getLanguageFromPluginUrl(selectedPluginUrl) ?? "plaintext"}
-                onChange={onTextChanged}
-                text={text}
-                lineWidth={lineWidth}
-                onScrollTopChange={setScrollTop}
-                scrollTop={scrollTop}
-              />
-            </div>
-            <div className="container">
-              <div className="playgroundSubTitle">
-                Configuration
-              </div>
-              <CodeEditor
-                language={"json"}
-                onChange={onConfigTextChanged}
-                jsonSchemaUrl={selectedPluginInfo?.configSchemaUrl}
-                text={configText}
-              />
-            </div>
-          </SplitPane>
+              <Allotment.Pane preferredSize="40%">
+                <div className="container">
+                  <div className="playgroundSubTitle">
+                    Configuration
+                  </div>
+                  <CodeEditor
+                    language={"json"}
+                    onChange={onConfigTextChanged}
+                    jsonSchemaUrl={selectedPluginInfo?.configSchemaUrl}
+                    text={configText}
+                  />
+                </div>
+              </Allotment.Pane>
+            </Allotment>
+          </Allotment.Pane>
           <div className="container">
             {isLoading ? <Spinner /> : (
               <CodeEditor
@@ -180,8 +179,8 @@ export function Playground({
               />
             )}
           </div>
-        </SplitPane>
-      </SplitPane>
+        </Allotment>
+      </div>
     </div>
   );
 }
diff --git a/website/playground/src/formatter.worker.ts b/website/playground/src/formatter.worker.ts
index 82dfd9047..fc3f6ff26 100644
--- a/website/playground/src/formatter.worker.ts
+++ b/website/playground/src/formatter.worker.ts
@@ -34,9 +34,7 @@ function loadUrl(url: string) {
 
       postPluginInfo(newFormatter);
 
-      if (config) {
-        setConfigSync(newFormatter, config);
-      }
+      setConfigSync(newFormatter, config ?? {});
 
       if (nextFormat) {
         formatSync(newFormatter, nextFormat.filePath, nextFormat.fileText);
@@ -60,9 +58,7 @@ function setConfig(providedConfig: Record<string, unknown>) {
 function refresh() {
   if (formatter) {
     formatter.then(f => {
-      if (config) {
-        setConfigSync(f, config);
-      }
+      setConfigSync(f, config ?? {});
       if (nextFormat) {
         formatSync(f, nextFormat.filePath, nextFormat.fileText);
       }
@@ -80,12 +76,16 @@ function format(filePath: string, fileText: string) {
 
 function setConfigSync(f: Formatter, config: Record<string, unknown>) {
   doHandlingError(() => f.setConfig({}, config));
+  postMessage({
+    type: "FileMatching",
+    info: f.getFileMatchingInfo(),
+  });
 }
 
 function formatSync(f: Formatter, filePath: string, fileText: string) {
   let result;
   try {
-    result = f.formatText(filePath, fileText);
+    result = f.formatText({ filePath, fileText });
   } catch (err: any) {
     result = err.message;
   }
diff --git a/website/playground/src/index.tsx b/website/playground/src/index.tsx
index 1b3ea8f14..f10f8bbd6 100644
--- a/website/playground/src/index.tsx
+++ b/website/playground/src/index.tsx
@@ -1,4 +1,4 @@
-import type { PluginInfo } from "@dprint/formatter";
+import type { FileMatchingInfo, PluginInfo } from "@dprint/formatter";
 import React, { useEffect, useState } from "react";
 import ReactDOM from "react-dom";
 import { Spinner } from "./components";
@@ -16,6 +16,7 @@ function Loader() {
   const [pluginUrls, setPluginUrls] = useState<string[]>([]);
   const [pluginUrl, setPluginUrl] = useState(initialUrl.pluginUrl);
   const [pluginInfo, setPluginInfo] = useState<PluginInfo | undefined>();
+  const [fileMatchingInfo, setFileMatchingInfo] = useState<FileMatchingInfo | undefined>();
   const [text, setText] = useState(initialUrl.text);
   const [configText, setConfigText] = useState(initialUrl.configText ?? "");
   const [defaultConfigText, setDefaultConfigText] = useState("");
@@ -43,11 +44,13 @@ function Loader() {
 
   useEffect(() => {
     formatterWorker.addOnPluginInfo(onPluginInfo);
+    formatterWorker.addOnFileMatchingInfo(onFileMatchingInfo);
     formatterWorker.addOnFormat(onFormat);
     formatterWorker.addOnError(onError);
 
     return () => {
       formatterWorker.removeOnPluginInfo(onPluginInfo);
+      formatterWorker.removeOnFileMatchingInfo(onFileMatchingInfo);
       formatterWorker.removeOnError(onError);
       formatterWorker.removeOnFormat(onFormat);
     };
@@ -56,6 +59,10 @@ function Loader() {
       setPluginInfo(pluginInfo);
     }
 
+    function onFileMatchingInfo(info: FileMatchingInfo) {
+      setFileMatchingInfo(info);
+    }
+
     function onFormat(text: string) {
       setFormattedText(text);
     }
@@ -120,7 +127,7 @@ function Loader() {
     };
   }, [pluginUrl, pluginInfo]);
 
-  if (pluginUrl == null || pluginInfo == null) {
+  if (pluginUrl == null || pluginInfo == null || fileMatchingInfo == null) {
     return <Spinner />;
   }
 
@@ -134,6 +141,7 @@ function Loader() {
       pluginUrls={pluginUrls}
       selectedPluginUrl={pluginUrl}
       selectedPluginInfo={pluginInfo}
+      fileMatchingInfo={fileMatchingInfo}
       onSelectPluginUrl={url => {
         setPluginInfo(undefined);
         if (!pluginUrls.includes(url)) {


Test patch:
diff --git a/crates/dprint/src/environment/test_environment.rs b/crates/dprint/src/environment/test_environment.rs
index bb2ffce73..e2af9c8bb 100644
--- a/crates/dprint/src/environment/test_environment.rs
+++ b/crates/dprint/src/environment/test_environment.rs
@@ -1,6 +1,7 @@
 use anyhow::anyhow;
 use anyhow::bail;
 use anyhow::Result;
+use once_cell::sync::Lazy;
 use parking_lot::Condvar;
 use parking_lot::Mutex;
 use path_clean::PathClean;
@@ -21,6 +22,7 @@ use super::Environment;
 use super::FilePermissions;
 use super::UrlDownloader;
 use crate::plugins::CompilationResult;
+use crate::utils::get_bytes_hash;
 use crate::utils::LogLevel;
 
 #[derive(Default)]
@@ -107,7 +109,6 @@ pub struct TestEnvironment {
   multi_selection_result: Arc<Mutex<Option<Vec<usize>>>>,
   confirm_results: Arc<Mutex<Vec<Result<Option<bool>>>>>,
   is_stdout_machine_readable: Arc<Mutex<bool>>,
-  wasm_compile_result: Arc<Mutex<Option<CompilationResult>>>,
   dir_info_error: Arc<Mutex<Option<std::io::Error>>>,
   std_in_pipe: Arc<Mutex<(Option<TestPipeWriter>, TestPipeReader)>>,
   std_out_pipe: Arc<Mutex<(Option<TestPipeWriter>, TestPipeReader)>>,
@@ -133,7 +134,6 @@ impl TestEnvironment {
       multi_selection_result: Arc::new(Mutex::new(None)),
       confirm_results: Default::default(),
       is_stdout_machine_readable: Arc::new(Mutex::new(false)),
-      wasm_compile_result: Arc::new(Mutex::new(None)),
       dir_info_error: Arc::new(Mutex::new(None)),
       std_in_pipe: Arc::new(Mutex::new({
         let pipe = create_test_pipe();
@@ -214,10 +214,6 @@ impl TestEnvironment {
     *self.log_level.lock() = value;
   }
 
-  pub fn set_wasm_compile_result(&self, value: CompilationResult) {
-    *self.wasm_compile_result.lock() = Some(value);
-  }
-
   pub fn stdout_reader(&self) -> Box<dyn Read + Send> {
     Box::new(self.std_out_pipe.lock().1.clone())
   }
@@ -531,9 +527,26 @@ impl Environment for TestEnvironment {
     *self.log_level.lock()
   }
 
-  fn compile_wasm(&self, _: &[u8]) -> Result<CompilationResult> {
-    let wasm_compile_result = self.wasm_compile_result.lock();
-    Ok(wasm_compile_result.clone().expect("Expected compilation result to be set."))
+  fn compile_wasm(&self, bytes: &[u8]) -> Result<CompilationResult> {
+    use std::collections::hash_map::Entry;
+
+    static COMPILE_RESULTS: Lazy<Mutex<HashMap<u64, CompilationResult>>> = Lazy::new(Default::default);
+
+    let hash = get_bytes_hash(bytes);
+    {
+      // hold the lock while compiling in order to prevent other
+      // threads from compiling at the same time
+      let mut results = COMPILE_RESULTS.lock();
+      let entry = results.entry(hash);
+      match entry {
+        Entry::Occupied(entry) => Ok(entry.get().clone()),
+        Entry::Vacant(entry) => {
+          let value = crate::plugins::compile_wasm(bytes).unwrap();
+          entry.insert(value.clone());
+          Ok(value)
+        }
+      }
+    }
   }
 
   fn wasm_cache_key(&self) -> String {
diff --git a/crates/dprint/src/environment/test_environment_builder.rs b/crates/dprint/src/environment/test_environment_builder.rs
index af9666c47..aaa4920be 100644
--- a/crates/dprint/src/environment/test_environment_builder.rs
+++ b/crates/dprint/src/environment/test_environment_builder.rs
@@ -1,3 +1,4 @@
+use indexmap::IndexMap;
 use serde::Serialize;
 use std::collections::HashMap;
 use std::path::Path;
@@ -7,6 +8,7 @@ use super::Environment;
 use super::TestEnvironment;
 use crate::test_helpers;
 use crate::test_helpers::TestProcessPluginFile;
+use crate::test_helpers::WASM_PLUGIN_0_1_0_BYTES;
 use crate::utils::get_sha256_checksum;
 
 pub struct TestConfigFileBuilder {
@@ -15,7 +17,7 @@ pub struct TestConfigFileBuilder {
   includes: Option<Vec<String>>,
   excludes: Option<Vec<String>>,
   plugins: Option<Vec<String>>,
-  sections: HashMap<String, String>,
+  sections: IndexMap<String, String>,
 }
 
 impl TestConfigFileBuilder {
@@ -26,7 +28,7 @@ impl TestConfigFileBuilder {
       includes: None,
       excludes: None,
       plugins: None,
-      sections: HashMap::new(),
+      sections: Default::default(),
     }
   }
 
@@ -74,6 +76,18 @@ impl TestConfigFileBuilder {
     self.add_plugin(&format!("https://plugins.dprint.dev/test-plugin.wasm@{}", checksum))
   }
 
+  /// This is a v3 old Wasm plugin.
+  pub fn add_remote_wasm_plugin_0_1_0(&mut self) -> &mut Self {
+    self.add_plugin("https://plugins.dprint.dev/test-plugin-0.1.0.wasm")
+  }
+
+  pub fn add_remote_wasm_plugin_0_1_0_with_checksum(&mut self) -> &mut Self {
+    self.add_plugin(&format!(
+      "https://plugins.dprint.dev/test-plugin-0.1.0.wasm@{}",
+      &crate::utils::get_sha256_checksum(&WASM_PLUGIN_0_1_0_BYTES)
+    ))
+  }
+
   pub fn add_config_section(&mut self, name: &str, text: &str) -> &mut Self {
     self.sections.insert(name.to_string(), text.to_string());
     self
@@ -171,7 +185,7 @@ impl TestInfoFileBuilder {
   pub fn to_string(&self) -> String {
     let mut parts = Vec::new();
     parts.push("\"schemaVersion\": 4".to_string());
-    parts.push(format!("\"pluginSystemSchemaVersion\": {}", self.plugin_schema_version.unwrap_or(3)));
+    parts.push(format!("\"pluginSystemSchemaVersion\": {}", self.plugin_schema_version.unwrap_or(4)));
     let plugins_text = serde_json::to_string_pretty(&self.plugins).unwrap();
     parts.push(format!("\"latest\": {}", plugins_text));
     format!("{{\n{}\n}}", parts.join(",\n"))
@@ -254,6 +268,13 @@ impl TestEnvironmentBuilder {
     self
   }
 
+  pub fn add_remote_wasm_0_1_0_plugin(&mut self) -> &mut Self {
+    self
+      .environment
+      .add_remote_file("https://plugins.dprint.dev/test-plugin-0.1.0.wasm", test_helpers::WASM_PLUGIN_0_1_0_BYTES);
+    self
+  }
+
   pub fn add_remote_wasm_plugin_at_url(&mut self, url: &str) -> &mut Self {
     self.environment.add_remote_file(url, test_helpers::WASM_PLUGIN_BYTES);
     self
diff --git a/crates/dprint/src/test_helpers.rs b/crates/dprint/src/test_helpers.rs
index 7aba093ec..cf2c1295e 100644
--- a/crates/dprint/src/test_helpers.rs
+++ b/crates/dprint/src/test_helpers.rs
@@ -10,7 +10,6 @@ use thiserror::Error;
 
 use crate::arg_parser::parse_args;
 use crate::environment::TestEnvironment;
-use crate::plugins::CompilationResult;
 use crate::plugins::PluginCache;
 use crate::plugins::PluginResolver;
 use crate::run_cli::run_cli;
@@ -44,9 +43,10 @@ pub static TEST_PROCESS_PLUGIN_PATH: Lazy<PathBuf> = Lazy::new(|| {
 });
 
 // Regenerate this by running `./rebuild.sh` in /crates/test-plugin
-pub static WASM_PLUGIN_BYTES: &'static [u8] = include_bytes!("../../test-plugin/test_plugin.wasm");
+pub static WASM_PLUGIN_BYTES: &'static [u8] = include_bytes!("../../test-plugin/test_plugin.wasm"); // 0.2.0
+/// This is an old v3 interface Wasm plugin at 0.1.0
+pub static WASM_PLUGIN_0_1_0_BYTES: &'static [u8] = include_bytes!("../../test-plugin/test_plugin_0_1_0.wasm");
 // cache these so it only has to be done once across all tests
-static COMPILATION_RESULT: Lazy<CompilationResult> = Lazy::new(|| crate::plugins::compile_wasm(WASM_PLUGIN_BYTES).unwrap());
 pub static PROCESS_PLUGIN_ZIP_BYTES: Lazy<Vec<u8>> = Lazy::new(|| {
   let buf: Vec<u8> = Vec::new();
   let w = std::io::Cursor::new(buf);
@@ -119,7 +119,6 @@ pub fn run_test_cli(args: Vec<&str>, environment: &TestEnvironment) -> Result<()
 pub fn run_test_cli_with_stdin(args: Vec<&str>, environment: &TestEnvironment, stdin_reader: TestStdInReader) -> Result<(), TestAppError> {
   let mut args: Vec<String> = args.into_iter().map(String::from).collect();
   args.insert(0, String::from(""));
-  environment.set_wasm_compile_result(COMPILATION_RESULT.clone());
   let plugin_cache = PluginCache::new(environment.clone());
   let plugin_resolver = Rc::new(PluginResolver::new(environment.clone(), plugin_cache));
   let args = parse_args(args, stdin_reader).map_err(|err| Into::<AppError>::into(err))?;
diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml
index eafdab7ca..ffaefb06b 100644
--- a/crates/test-plugin/Cargo.toml
+++ b/crates/test-plugin/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "test-plugin"
-version = "0.1.0"
+version = "0.2.0"
 authors = ["David Sherret <dsherret@gmail.com>"]
 edition = "2021"
 
diff --git a/crates/test-plugin/src/lib.rs b/crates/test-plugin/src/lib.rs
index 94ef27090..9b2ad1d0e 100644
--- a/crates/test-plugin/src/lib.rs
+++ b/crates/test-plugin/src/lib.rs
@@ -1,19 +1,26 @@
 use anyhow::bail;
 use anyhow::Result;
+use dprint_core::configuration::get_nullable_vec;
 use dprint_core::configuration::get_unknown_property_diagnostics;
 use dprint_core::configuration::get_value;
 use dprint_core::configuration::ConfigKeyMap;
+use dprint_core::configuration::ConfigKeyValue;
+use dprint_core::configuration::ConfigurationDiagnostic;
 use dprint_core::configuration::GlobalConfiguration;
-use dprint_core::configuration::ResolveConfigurationResult;
 use dprint_core::generate_plugin_code;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
+use dprint_core::plugins::ConfigChange;
+use dprint_core::plugins::ConfigChangeKind;
 use dprint_core::plugins::FileMatchingInfo;
 use dprint_core::plugins::FormatResult;
 use dprint_core::plugins::PluginInfo;
+use dprint_core::plugins::PluginResolveConfigurationResult;
+use dprint_core::plugins::SyncFormatRequest;
+use dprint_core::plugins::SyncHostFormatRequest;
 use dprint_core::plugins::SyncPluginHandler;
-use dprint_core::plugins::SyncPluginInfo;
 use serde::Deserialize;
 use serde::Serialize;
-use std::path::Path;
+use std::io::Write;
 use std::path::PathBuf;
 
 #[derive(Clone, Serialize, Deserialize)]
@@ -34,34 +41,50 @@ impl TestWasmPlugin {
 }
 
 impl SyncPluginHandler<Configuration> for TestWasmPlugin {
-  fn resolve_config(&mut self, config: ConfigKeyMap, global_config: &GlobalConfiguration) -> ResolveConfigurationResult<Configuration> {
+  fn resolve_config(&mut self, config: ConfigKeyMap, global_config: &GlobalConfiguration) -> PluginResolveConfigurationResult<Configuration> {
+    fn get_string_vec(config: &mut ConfigKeyMap, key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> Option<Vec<String>> {
+      get_nullable_vec(
+        config,
+        key,
+        |value, _index, diagnostics| match value {
+          ConfigKeyValue::String(value) => Some(value),
+          _ => {
+            diagnostics.push(ConfigurationDiagnostic {
+              property_name: key.to_string(),
+              message: "Expected only string values.".to_string(),
+            });
+            None
+          }
+        },
+        diagnostics,
+      )
+    }
+
     let mut config = config;
     let mut diagnostics = Vec::new();
     let ending = get_value(&mut config, "ending", String::from("formatted"), &mut diagnostics);
     let line_width = get_value(&mut config, "line_width", global_config.line_width.unwrap_or(120), &mut diagnostics);
 
+    let file_extensions = get_string_vec(&mut config, "file_extensions", &mut diagnostics).unwrap_or_else(|| vec!["txt".to_string()]);
+    let file_names = get_string_vec(&mut config, "file_names", &mut diagnostics).unwrap_or_else(|| vec![]);
+
     diagnostics.extend(get_unknown_property_diagnostics(config));
 
-    ResolveConfigurationResult {
+    PluginResolveConfigurationResult {
       config: Configuration { ending, line_width },
       diagnostics,
+      file_matching: FileMatchingInfo { file_extensions, file_names },
     }
   }
 
-  fn plugin_info(&mut self) -> SyncPluginInfo {
-    SyncPluginInfo {
-      info: PluginInfo {
-        name: env!("CARGO_PKG_NAME").to_string(),
-        version: env!("CARGO_PKG_VERSION").to_string(),
-        config_key: "test-plugin".to_string(),
-        help_url: "https://dprint.dev/plugins/test".to_string(),
-        config_schema_url: "https://plugins.dprint.dev/test/schema.json".to_string(),
-        update_url: Some("https://plugins.dprint.dev/dprint/test-plugin/latest.json".to_string()),
-      },
-      file_matching: FileMatchingInfo {
-        file_extensions: vec!["txt".to_string()],
-        file_names: vec![],
-      },
+  fn plugin_info(&mut self) -> PluginInfo {
+    PluginInfo {
+      name: env!("CARGO_PKG_NAME").to_string(),
+      version: env!("CARGO_PKG_VERSION").to_string(),
+      config_key: "test-plugin".to_string(),
+      help_url: "https://dprint.dev/plugins/test".to_string(),
+      config_schema_url: "https://plugins.dprint.dev/test/schema.json".to_string(),
+      update_url: Some("https://plugins.dprint.dev/dprint/test-plugin/latest.json".to_string()),
     }
   }
 
@@ -69,13 +92,49 @@ impl SyncPluginHandler<Configuration> for TestWasmPlugin {
     std::str::from_utf8(include_bytes!("../LICENSE")).unwrap().into()
   }
 
-  fn format(
-    &mut self,
-    _: &Path,
-    file_bytes: Vec<u8>,
-    config: &Configuration,
-    mut format_with_host: impl FnMut(&Path, Vec<u8>, &ConfigKeyMap) -> FormatResult,
-  ) -> FormatResult {
+  fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
+    let mut changes = Vec::new();
+    if message.config.contains_key("should_add") {
+      changes.extend([
+        ConfigChange {
+          path: vec!["should_add".to_string().into()],
+          kind: ConfigChangeKind::Set(ConfigKeyValue::String("new_value_wasm".to_string())),
+        },
+        ConfigChange {
+          path: vec!["new_prop1".to_string().into()],
+          kind: ConfigChangeKind::Add(ConfigKeyValue::Array(vec![ConfigKeyValue::String("new_value_wasm".to_string())])),
+        },
+        ConfigChange {
+          path: vec!["new_prop2".to_string().into()],
+          kind: ConfigChangeKind::Add(ConfigKeyValue::Object(ConfigKeyMap::from([(
+            "new_prop".to_string(),
+            ConfigKeyValue::String("new_value_wasm".to_string()),
+          )]))),
+        },
+      ]);
+    }
+    if message.config.contains_key("should_set") {
+      changes.push(ConfigChange {
+        path: vec!["should_set".to_string().into()],
+        kind: ConfigChangeKind::Set(ConfigKeyValue::String("new_value_wasm".to_string())),
+      });
+    }
+    if message.config.contains_key("should_remove") {
+      changes.push(ConfigChange {
+        path: vec!["should_remove".to_string().into()],
+        kind: ConfigChangeKind::Remove,
+      });
+    }
+    if message.config.contains_key("should_set_past_version") {
+      changes.push(ConfigChange {
+        path: vec!["should_set_past_version".to_string().into()],
+        kind: ConfigChangeKind::Set(ConfigKeyValue::String(message.old_version.unwrap())),
+      });
+    }
+    Ok(changes)
+  }
+
+  fn format(&mut self, request: SyncFormatRequest<Configuration>, mut format_with_host: impl FnMut(SyncHostFormatRequest) -> FormatResult) -> FormatResult {
     fn handle_host_response(result: FormatResult, original_text: &str) -> Result<String> {
       match result {
         Ok(Some(text)) => Ok(String::from_utf8(text).unwrap()),
@@ -84,8 +143,24 @@ impl SyncPluginHandler<Configuration> for TestWasmPlugin {
       }
     }
 
-    let file_text = String::from_utf8(file_bytes).unwrap();
-    let (had_suffix, file_text) = if let Some(text) = file_text.strip_suffix(&format!("_{}", config.ending)) {
+    let file_text = String::from_utf8(request.file_bytes).unwrap();
+    if file_text == "wait_cancellation" {
+      loop {
+        if request.token.is_cancelled() {
+          return Ok(None);
+        }
+      }
+    }
+
+    if let Some(output) = file_text.strip_prefix("stderr:") {
+      let mut stderr = dprint_core::plugins::wasm::WasiPrintFd(2);
+      stderr.write_all(output.as_bytes()).unwrap();
+    } else if let Some(output) = file_text.strip_prefix("stdout:") {
+      let mut stderr = dprint_core::plugins::wasm::WasiPrintFd(1);
+      stderr.write_all(output.as_bytes()).unwrap();
+    }
+
+    let (had_suffix, file_text) = if let Some(text) = file_text.strip_suffix(&format!("_{}", request.config.ending)) {
       (true, text.to_string())
     } else {
       (false, file_text)
@@ -93,11 +168,33 @@ impl SyncPluginHandler<Configuration> for TestWasmPlugin {
 
     let inner_format_text = if self.has_panicked {
       panic!("Previously panicked. Plugin should not have been used by the CLI again.")
+    } else if let Some(range) = request.range {
+      if let Some(new_text) = file_text.strip_prefix("plugin-range: ") {
+        format!(
+          "plugin-range: {}",
+          handle_host_response(
+            format_with_host(SyncHostFormatRequest {
+              file_path: &PathBuf::from("./test.txt_ps"),
+              file_bytes: new_text.as_bytes(),
+              range: Some(range),
+              override_config: &Default::default(),
+            }),
+            new_text
+          )?
+        )
+      } else {
+        format!("{}_{}_{}", &file_text[0..range.start], request.config.ending, &file_text[range.end..])
+      }
     } else if let Some(new_text) = file_text.strip_prefix("plugin: ") {
       format!(
         "plugin: {}",
         handle_host_response(
-          format_with_host(&PathBuf::from("./test.txt_ps"), new_text.to_string().into_bytes(), &ConfigKeyMap::new()),
+          format_with_host(SyncHostFormatRequest {
+            file_path: &PathBuf::from("./test.txt_ps"),
+            file_bytes: new_text.as_bytes(),
+            range: None,
+            override_config: &ConfigKeyMap::new(),
+          }),
           new_text,
         )?,
       )
@@ -107,7 +204,12 @@ impl SyncPluginHandler<Configuration> for TestWasmPlugin {
       format!(
         "plugin-config: {}",
         handle_host_response(
-          format_with_host(&PathBuf::from("./test.txt_ps"), new_text.to_string().into_bytes(), &config_map),
+          format_with_host(SyncHostFormatRequest {
+            file_path: &PathBuf::from("./test.txt_ps"),
+            file_bytes: new_text.as_bytes(),
+            range: None,
+            override_config: &config_map,
+          }),
           new_text
         )?
       )
@@ -131,7 +233,7 @@ impl SyncPluginHandler<Configuration> for TestWasmPlugin {
     if had_suffix && inner_format_text == file_text {
       Ok(None)
     } else {
-      Ok(Some(format!("{}_{}", inner_format_text, config.ending).into_bytes()))
+      Ok(Some(format!("{}_{}", inner_format_text, request.config.ending).into_bytes()))
     }
   }
 }
diff --git a/crates/test-plugin/test_plugin.wasm b/crates/test-plugin/test_plugin.wasm
index 2ab4b7740..4b04d2433 100644
Binary files a/crates/test-plugin/test_plugin.wasm and b/crates/test-plugin/test_plugin.wasm differ
diff --git a/crates/test-plugin/test_plugin_0_1_0.wasm b/crates/test-plugin/test_plugin_0_1_0.wasm
new file mode 100644
index 000000000..2ab4b7740
Binary files /dev/null and b/crates/test-plugin/test_plugin_0_1_0.wasm differ
diff --git a/crates/test-process-plugin/src/main.rs b/crates/test-process-plugin/src/main.rs
index 7fca7a22b..1b653b7e1 100644
--- a/crates/test-process-plugin/src/main.rs
+++ b/crates/test-process-plugin/src/main.rs
@@ -4,6 +4,7 @@ use anyhow::bail;
 use anyhow::Result;
 use dprint_core::async_runtime::async_trait;
 use dprint_core::async_runtime::LocalBoxFuture;
+use dprint_core::configuration::get_nullable_vec;
 use dprint_core::configuration::get_unknown_property_diagnostics;
 use dprint_core::configuration::get_value;
 use dprint_core::configuration::ConfigKeyMap;
@@ -14,6 +15,7 @@ use dprint_core::plugins::process::get_parent_process_id_from_cli_args;
 use dprint_core::plugins::process::handle_process_stdio_messages;
 use dprint_core::plugins::process::start_parent_process_checker_task;
 use dprint_core::plugins::AsyncPluginHandler;
+use dprint_core::plugins::CheckConfigUpdatesMessage;
 use dprint_core::plugins::ConfigChange;
 use dprint_core::plugins::ConfigChangeKind;
 use dprint_core::plugins::FileMatchingInfo;
@@ -58,7 +60,14 @@ impl AsyncPluginHandler for TestProcessPluginHandler {
   fn plugin_info(&self) -> PluginInfo {
     PluginInfo {
       name: String::from(env!("CARGO_PKG_NAME")),
-      version: String::from(env!("CARGO_PKG_VERSION")),
+      version: {
+        let exe_path = std::env::current_exe().unwrap();
+        if exe_path.to_string_lossy().contains("temp-plugin-0.3.0") {
+          "0.3.0".to_string()
+        } else {
+          String::from(env!("CARGO_PKG_VERSION"))
+        }
+      },
       config_key: "testProcessPlugin".to_string(),
       help_url: "https://dprint.dev/plugins/test-process".to_string(),
       config_schema_url: "".to_string(),
@@ -71,37 +80,22 @@ impl AsyncPluginHandler for TestProcessPluginHandler {
   }
 
   async fn resolve_config(&self, config: ConfigKeyMap, global_config: GlobalConfiguration) -> PluginResolveConfigurationResult<Configuration> {
-    // todo: way to do something like get_value in dprint-core, but with vectors
     fn get_string_vec(config: &mut ConfigKeyMap, key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> Option<Vec<String>> {
-      match config.shift_remove(key) {
-        Some(value) => match value {
-          ConfigKeyValue::Array(values) => {
-            let mut result = Vec::with_capacity(values.len());
-            for value in values {
-              match value {
-                ConfigKeyValue::String(value) => {
-                  result.push(value);
-                }
-                _ => {
-                  diagnostics.push(ConfigurationDiagnostic {
-                    property_name: key.to_string(),
-                    message: "Expected only string values.".to_string(),
-                  });
-                }
-              }
-            }
-            Some(result)
-          }
+      get_nullable_vec(
+        config,
+        key,
+        |value, _index, diagnostics| match value {
+          ConfigKeyValue::String(value) => Some(value),
           _ => {
             diagnostics.push(ConfigurationDiagnostic {
               property_name: key.to_string(),
-              message: "Expected an array.".to_string(),
+              message: "Expected only string values.".to_string(),
             });
             None
           }
         },
-        None => None,
-      }
+        diagnostics,
+      )
     }
 
     let mut config = config;
@@ -121,9 +115,9 @@ impl AsyncPluginHandler for TestProcessPluginHandler {
     }
   }
 
-  async fn check_config_updates(&self, config: ConfigKeyMap) -> Result<Vec<ConfigChange>> {
+  async fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
     let mut changes = Vec::new();
-    if config.contains_key("should_add") {
+    if message.config.contains_key("should_add") {
       changes.extend([
         ConfigChange {
           path: vec!["should_add".to_string().into()],
@@ -142,18 +136,24 @@ impl AsyncPluginHandler for TestProcessPluginHandler {
         },
       ]);
     }
-    if config.contains_key("should_set") {
+    if message.config.contains_key("should_set") {
       changes.push(ConfigChange {
         path: vec!["should_set".to_string().into()],
         kind: ConfigChangeKind::Set(ConfigKeyValue::String("new_value".to_string())),
       });
     }
-    if config.contains_key("should_remove") {
+    if message.config.contains_key("should_remove") {
       changes.push(ConfigChange {
         path: vec!["should_remove".to_string().into()],
         kind: ConfigChangeKind::Remove,
       });
     }
+    if message.config.contains_key("should_set_past_version") {
+      changes.push(ConfigChange {
+        path: vec!["should_set_past_version".to_string().into()],
+        kind: ConfigChangeKind::Set(ConfigKeyValue::String(message.old_version.unwrap())),
+      });
+    }
     Ok(changes)
   }
 
@@ -169,9 +169,23 @@ impl AsyncPluginHandler for TestProcessPluginHandler {
       (false, file_text.to_string())
     };
 
-    let inner_format_text = if let Some(range) = &request.range {
-      let text = format!("{}_{}_{}", &file_text[0..range.start], request.config.ending, &file_text[range.end..]);
-      text
+    let inner_format_text = if let Some(range) = request.range {
+      if let Some(new_text) = file_text.strip_prefix("plugin-range: ") {
+        let result = (format_with_host)(HostFormatRequest {
+          file_path: PathBuf::from("./test.txt"),
+          file_bytes: new_text.to_string().into_bytes(),
+          range: Some(range),
+          override_config: Default::default(),
+          token: request.token.clone(),
+        })
+        .await?;
+        format!(
+          "plugin-range: {}",
+          result.map(|r| String::from_utf8(r).unwrap()).unwrap_or_else(|| new_text.to_string())
+        )
+      } else {
+        format!("{}_{}_{}", &file_text[0..range.start], request.config.ending, &file_text[range.end..])
+      }
     } else if file_text.starts_with("wait_cancellation") {
       request.token.wait_cancellation().await;
       return Ok(None);