Detailed changes
@@ -100,12 +100,12 @@ name = "ai"
version = "0.1.0"
dependencies = [
"anyhow",
- "assets",
"collections",
"editor",
"futures 0.3.28",
"gpui",
"isahc",
+ "rust-embed",
"serde",
"serde_json",
"util",
@@ -210,15 +210,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
-[[package]]
-name = "assets"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "gpui",
- "rust-embed",
-]
-
[[package]]
name = "async-broadcast"
version = "0.4.1"
@@ -1402,7 +1393,6 @@ name = "copilot_button"
version = "0.1.0"
dependencies = [
"anyhow",
- "assets",
"context_menu",
"copilot",
"editor",
@@ -6114,7 +6104,6 @@ name = "settings"
version = "0.1.0"
dependencies = [
"anyhow",
- "assets",
"collections",
"fs",
"futures 0.3.28",
@@ -6123,6 +6112,7 @@ dependencies = [
"lazy_static",
"postage",
"pretty_assertions",
+ "rust-embed",
"schemars",
"serde",
"serde_derive",
@@ -7749,6 +7739,7 @@ dependencies = [
"lazy_static",
"log",
"rand 0.8.5",
+ "rust-embed",
"serde",
"serde_json",
"smol",
@@ -7819,7 +7810,6 @@ name = "vim"
version = "0.1.0"
dependencies = [
"anyhow",
- "assets",
"async-compat",
"async-trait",
"collections",
@@ -8647,7 +8637,6 @@ name = "workspace"
version = "0.1.0"
dependencies = [
"anyhow",
- "assets",
"async-recursion 1.0.4",
"bincode",
"call",
@@ -8747,7 +8736,6 @@ dependencies = [
"activity_indicator",
"ai",
"anyhow",
- "assets",
"async-compression",
"async-recursion 0.3.2",
"async-tar",
@@ -2,7 +2,6 @@
members = [
"crates/activity_indicator",
"crates/ai",
- "crates/assets",
"crates/auto_update",
"crates/breadcrumbs",
"crates/call",
@@ -88,6 +87,7 @@ parking_lot = { version = "0.11.1" }
postage = { version = "0.5", features = ["futures-traits"] }
rand = { version = "0.8.5" }
regex = { version = "1.5" }
+rust-embed = { version = "6.3", features = ["include-exclude"] }
schemars = { version = "0.8" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -9,12 +9,12 @@ path = "src/ai.rs"
doctest = false
[dependencies]
-assets = { path = "../assets"}
collections = { path = "../collections"}
editor = { path = "../editor" }
gpui = { path = "../gpui" }
util = { path = "../util" }
+rust-embed.workspace = true
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
@@ -1,5 +1,4 @@
use anyhow::{anyhow, Result};
-use assets::Assets;
use collections::HashMap;
use editor::Editor;
use futures::AsyncBufReadExt;
@@ -16,6 +15,14 @@ use std::{io, sync::Arc};
use util::channel::{ReleaseChannel, RELEASE_CHANNEL};
use util::{ResultExt, TryFutureExt};
+use rust_embed::RustEmbed;
+use std::str;
+
+#[derive(RustEmbed)]
+#[folder = "../../assets/contexts"]
+#[exclude = "*.DS_Store"]
+pub struct ContextAssets;
+
actions!(ai, [Assist]);
// Data types for chat completion requests
@@ -173,7 +180,7 @@ impl Assistant {
let assist_task = cx.spawn(|_, mut cx| {
async move {
// TODO: We should have a get_string method on assets. This is repateated elsewhere.
- let content = Assets::get("contexts/system.zmd").unwrap();
+ let content = ContextAssets::get("system.zmd").unwrap();
let mut system_message = std::str::from_utf8(content.data.as_ref())
.unwrap()
.to_string();
@@ -1,14 +0,0 @@
-[package]
-name = "assets"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/assets.rs"
-doctest = false
-
-[dependencies]
-gpui = { path = "../gpui" }
-anyhow.workspace = true
-rust-embed = { version = "6.3", features = ["include-exclude"] }
@@ -1,29 +0,0 @@
-use std::process::Command;
-
-fn main() {
- let output = Command::new("npm")
- .current_dir("../../styles")
- .args(["install", "--no-save"])
- .output()
- .expect("failed to run npm");
- if !output.status.success() {
- panic!(
- "failed to install theme dependencies {}",
- String::from_utf8_lossy(&output.stderr)
- );
- }
-
- let output = Command::new("npm")
- .current_dir("../../styles")
- .args(["run", "build"])
- .output()
- .expect("failed to run npm");
- if !output.status.success() {
- panic!(
- "build script failed {}",
- String::from_utf8_lossy(&output.stderr)
- );
- }
-
- println!("cargo:rerun-if-changed=../../styles/src");
-}
@@ -9,7 +9,6 @@ path = "src/copilot_button.rs"
doctest = false
[dependencies]
-assets = { path = "../assets" }
copilot = { path = "../copilot" }
editor = { path = "../editor" }
fs = { path = "../fs" }
@@ -315,9 +315,7 @@ async fn configure_disabled_globs(
let settings_editor = workspace
.update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
- settings::initial_user_settings_content(&assets::Assets)
- .as_ref()
- .into()
+ settings::initial_user_settings_content().as_ref().into()
})
})?
.await?
@@ -12,7 +12,6 @@ doctest = false
test-support = ["gpui/test-support", "fs/test-support"]
[dependencies]
-assets = { path = "../assets" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" }
@@ -25,6 +24,7 @@ futures.workspace = true
json_comments = "0.2"
lazy_static.workspace = true
postage.workspace = true
+rust-embed.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
@@ -1,6 +1,5 @@
-use crate::settings_store::parse_json_with_comments;
+use crate::{settings_store::parse_json_with_comments, SettingsAssets};
use anyhow::{Context, Result};
-use assets::Assets;
use collections::BTreeMap;
use gpui::{keymap_matcher::Binding, AppContext};
use schemars::{
@@ -10,11 +9,11 @@ use schemars::{
};
use serde::Deserialize;
use serde_json::{value::RawValue, Value};
-use util::ResultExt;
+use util::{asset_str, ResultExt};
#[derive(Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)]
-pub struct KeymapFileContent(Vec<KeymapBlock>);
+pub struct KeymapFile(Vec<KeymapBlock>);
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct KeymapBlock {
@@ -40,11 +39,10 @@ impl JsonSchema for KeymapAction {
#[derive(Deserialize)]
struct ActionWithData(Box<str>, Box<RawValue>);
-impl KeymapFileContent {
+impl KeymapFile {
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
- let content = Assets::get(asset_path).unwrap().data;
- let content_str = std::str::from_utf8(content.as_ref()).unwrap();
- Self::parse(content_str)?.add_to_cx(cx)
+ let content = asset_str::<SettingsAssets>(asset_path);
+ Self::parse(content.as_ref())?.add_to_cx(cx)
}
pub fn parse(content: &str) -> Result<Self> {
@@ -83,40 +81,40 @@ impl KeymapFileContent {
}
Ok(())
}
-}
-pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Value {
- let mut root_schema = SchemaSettings::draft07()
- .with(|settings| settings.option_add_null_type = false)
- .into_generator()
- .into_root_schema_for::<KeymapFileContent>();
+ pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
+ let mut root_schema = SchemaSettings::draft07()
+ .with(|settings| settings.option_add_null_type = false)
+ .into_generator()
+ .into_root_schema_for::<KeymapFile>();
- let action_schema = Schema::Object(SchemaObject {
- subschemas: Some(Box::new(SubschemaValidation {
- one_of: Some(vec![
- Schema::Object(SchemaObject {
- instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
- enum_values: Some(
- action_names
- .iter()
- .map(|name| Value::String(name.to_string()))
- .collect(),
- ),
- ..Default::default()
- }),
- Schema::Object(SchemaObject {
- instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
- ..Default::default()
- }),
- ]),
+ let action_schema = Schema::Object(SchemaObject {
+ subschemas: Some(Box::new(SubschemaValidation {
+ one_of: Some(vec![
+ Schema::Object(SchemaObject {
+ instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
+ enum_values: Some(
+ action_names
+ .iter()
+ .map(|name| Value::String(name.to_string()))
+ .collect(),
+ ),
+ ..Default::default()
+ }),
+ Schema::Object(SchemaObject {
+ instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
+ ..Default::default()
+ }),
+ ]),
+ ..Default::default()
+ })),
..Default::default()
- })),
- ..Default::default()
- });
+ });
- root_schema
- .definitions
- .insert("KeymapAction".to_owned(), action_schema);
+ root_schema
+ .definitions
+ .insert("KeymapAction".to_owned(), action_schema);
- serde_json::to_value(root_schema).unwrap()
+ serde_json::to_value(root_schema).unwrap()
+ }
}
@@ -2,31 +2,37 @@ mod keymap_file;
mod settings_file;
mod settings_store;
-use gpui::AssetSource;
-pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
+use rust_embed::RustEmbed;
+use std::{borrow::Cow, str};
+use util::asset_str;
+
+pub use keymap_file::KeymapFile;
pub use settings_file::*;
pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
-use std::{borrow::Cow, str};
-pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
-const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
-const INITIAL_LOCAL_SETTINGS_ASSET_PATH: &str = "settings/initial_local_settings.json";
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "settings/*"]
+#[include = "keymaps/*"]
+#[exclude = "*.DS_Store"]
+pub struct SettingsAssets;
pub fn default_settings() -> Cow<'static, str> {
- asset_str(&assets::Assets, DEFAULT_SETTINGS_ASSET_PATH)
+ asset_str::<SettingsAssets>("settings/default.json")
+}
+
+pub fn default_keymap() -> Cow<'static, str> {
+ asset_str::<SettingsAssets>("keymaps/default.json")
}
-pub fn initial_user_settings_content(assets: &dyn AssetSource) -> Cow<'_, str> {
- asset_str(assets, INITIAL_USER_SETTINGS_ASSET_PATH)
+pub fn vim_keymap() -> Cow<'static, str> {
+ asset_str::<SettingsAssets>("keymaps/vim.json")
}
-pub fn initial_local_settings_content(assets: &dyn AssetSource) -> Cow<'_, str> {
- asset_str(assets, INITIAL_LOCAL_SETTINGS_ASSET_PATH)
+pub fn initial_user_settings_content() -> Cow<'static, str> {
+ asset_str::<SettingsAssets>("settings/initial_user_settings.json")
}
-fn asset_str<'a>(assets: &'a dyn AssetSource, path: &str) -> Cow<'a, str> {
- match assets.load(path).unwrap() {
- Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
- Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
- }
+pub fn initial_local_settings_content() -> Cow<'static, str> {
+ asset_str::<SettingsAssets>("settings/initial_local_settings.json")
}
@@ -1,6 +1,5 @@
use crate::{settings_store::SettingsStore, Setting};
use anyhow::Result;
-use assets::Assets;
use fs::Fs;
use futures::{channel::mpsc, StreamExt};
use gpui::{executor::Background, AppContext};
@@ -111,7 +110,7 @@ async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
Err(err) => {
if let Some(e) = err.downcast_ref::<std::io::Error>() {
if e.kind() == ErrorKind::NotFound {
- return Ok(crate::initial_user_settings_content(&Assets).to_string());
+ return Ok(crate::initial_user_settings_content().to_string());
}
}
return Err(err);
@@ -21,6 +21,7 @@ isahc.workspace = true
smol.workspace = true
url = "2.2"
rand.workspace = true
+rust-embed.workspace = true
tempdir = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
@@ -7,6 +7,7 @@ pub mod paths;
pub mod test;
use std::{
+ borrow::Cow,
cmp::{self, Ordering},
ops::{AddAssign, Range, RangeInclusive},
panic::Location,
@@ -284,6 +285,14 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
}
}
+/// Get an embedded file as a string.
+pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
+ match A::get(path).unwrap().data {
+ Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()),
+ Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()),
+ }
+}
+
// copy unstable standard feature option unzip
// https://github.com/rust-lang/rust/issues/87800
// Remove when this ship in Rust 1.66 or 1.67
@@ -24,7 +24,6 @@ nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", f
tokio = { version = "1.15", "optional" = true }
serde_json.workspace = true
-assets = { path = "../assets" }
collections = { path = "../collections" }
command_palette = { path = "../command_palette" }
editor = { path = "../editor" }
@@ -27,7 +27,7 @@ impl<'a> VimTestContext<'a> {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
});
- settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap();
+ settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
});
// Setup search toolbars and keypress hook
@@ -19,7 +19,6 @@ test-support = [
]
[dependencies]
-assets = { path = "../assets" }
db = { path = "../db" }
call = { path = "../call" }
client = { path = "../client" }
@@ -17,7 +17,6 @@ path = "src/main.rs"
[dependencies]
activity_indicator = { path = "../activity_indicator" }
-assets = { path = "../assets" }
auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" }
call = { path = "../call" }
@@ -1,3 +1,5 @@
+use std::process::Command;
+
fn main() {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
@@ -21,4 +23,32 @@ fn main() {
// Register exported Objective-C selectors, protocols, etc
println!("cargo:rustc-link-arg=-Wl,-ObjC");
+
+ // Install dependencies for theme-generation
+ let output = Command::new("npm")
+ .current_dir("../../styles")
+ .args(["install", "--no-save"])
+ .output()
+ .expect("failed to run npm");
+ if !output.status.success() {
+ panic!(
+ "failed to install theme dependencies {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+
+ // Regenerate themes
+ let output = Command::new("npm")
+ .current_dir("../../styles")
+ .args(["run", "build"])
+ .output()
+ .expect("failed to run npm");
+ if !output.status.success() {
+ panic!(
+ "build script failed {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+
+ println!("cargo:rerun-if-changed=../../styles/src");
}
@@ -4,6 +4,10 @@ use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../../assets"]
+#[include = "fonts/**/*"]
+#[include = "icons/**/*"]
+#[include = "themes/**/*"]
+#[include = "*.md"]
#[exclude = "*.DS_Store"]
pub struct Assets;
@@ -3,6 +3,7 @@ pub use language::*;
use node_runtime::NodeRuntime;
use rust_embed::RustEmbed;
use std::{borrow::Cow, str, sync::Arc};
+use util::asset_str;
mod c;
mod elixir;
@@ -179,10 +180,7 @@ fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
for path in LanguageDir::iter() {
if let Some(remainder) = path.strip_prefix(name) {
if remainder.starts_with(filename_prefix) {
- let contents = match LanguageDir::get(path.as_ref()).unwrap().data {
- Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
- Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
- };
+ let contents = asset_str::<LanguageDir>(path.as_ref());
match &mut result {
None => result = Some(contents),
Some(r) => r.to_mut().push_str(contents.as_ref()),
@@ -6,7 +6,7 @@ use gpui::AppContext;
use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
use node_runtime::NodeRuntime;
use serde_json::json;
-use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore};
+use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
use smol::fs;
use staff_mode::StaffMode;
use std::{
@@ -143,7 +143,7 @@ impl LspAdapter for JsonLspAdapter {
},
{
"fileMatch": [schema_file_match(&paths::KEYMAP)],
- "schema": keymap_file_json_schema(&action_names),
+ "schema": KeymapFile::generate_json_schema(&action_names),
}
]
}
@@ -2,7 +2,6 @@
#![allow(non_snake_case)]
use anyhow::{anyhow, Context, Result};
-use assets::Assets;
use backtrace::Backtrace;
use cli::{
ipc::{self, IpcSender},
@@ -58,7 +57,8 @@ use staff_mode::StaffMode;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
use zed::{
- self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
+ assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace,
+ languages, menus,
};
fn main() {
@@ -1,7 +1,9 @@
+pub mod assets;
pub mod languages;
pub mod menus;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
+
use anyhow::Context;
use assets::Assets;
use breadcrumbs::Breadcrumbs;
@@ -30,12 +32,11 @@ use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar};
use serde::Deserialize;
use serde_json::to_string_pretty;
-use settings::{
- initial_local_settings_content, KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH,
-};
+use settings::{initial_local_settings_content, KeymapFile, SettingsStore};
use std::{borrow::Cow, str, sync::Arc};
use terminal_view::terminal_panel::{self, TerminalPanel};
use util::{
+ asset_str,
channel::ReleaseChannel,
paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
ResultExt,
@@ -149,7 +150,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
open_bundled_file(
workspace,
- "licenses.md",
+ asset_str::<Assets>("licenses.md"),
"Open Source License Attribution",
"Markdown",
cx,
@@ -169,9 +170,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
cx.add_action(
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
- settings::initial_user_settings_content(&Assets)
- .as_ref()
- .into()
+ settings::initial_user_settings_content().as_ref().into()
})
.detach_and_log_err(cx);
},
@@ -181,7 +180,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
open_bundled_file(
workspace,
- "keymaps/default.json",
+ settings::default_keymap(),
"Default Key Bindings",
"JSON",
cx,
@@ -194,7 +193,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
cx: &mut ViewContext<Workspace>| {
open_bundled_file(
workspace,
- DEFAULT_SETTINGS_ASSET_PATH,
+ settings::default_settings(),
"Default Settings",
"JSON",
cx,
@@ -521,11 +520,11 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
pub fn load_default_keymap(cx: &mut AppContext) {
for path in ["keymaps/default.json", "keymaps/vim.json"] {
- KeymapFileContent::load_asset(path, cx).unwrap();
+ KeymapFile::load_asset(path, cx).unwrap();
}
if let Some(asset_path) = settings::get::<BaseKeymap>(cx).asset_path() {
- KeymapFileContent::load_asset(asset_path, cx).unwrap();
+ KeymapFile::load_asset(asset_path, cx).unwrap();
}
}
@@ -536,7 +535,7 @@ pub fn handle_keymap_file_changes(
cx.spawn(move |mut cx| async move {
let mut settings_subscription = None;
while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
- if let Ok(keymap_content) = KeymapFileContent::parse(&user_keymap_content) {
+ if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) {
cx.update(|cx| {
cx.clear_bindings();
load_default_keymap(cx);
@@ -613,11 +612,7 @@ fn open_local_settings_file(
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
if buffer.read(cx).is_empty() {
buffer.update(cx, |buffer, cx| {
- buffer.edit(
- [(0..0, initial_local_settings_content(&Assets))],
- None,
- cx,
- )
+ buffer.edit([(0..0, initial_local_settings_content())], None, cx)
});
}
}
@@ -693,7 +688,7 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Works
fn open_bundled_file(
workspace: &mut Workspace,
- asset_path: &'static str,
+ text: Cow<'static, str>,
title: &'static str,
language: &'static str,
cx: &mut ViewContext<Workspace>,
@@ -705,13 +700,9 @@ fn open_bundled_file(
.update(&mut cx, |workspace, cx| {
workspace.with_local_workspace(cx, |workspace, cx| {
let project = workspace.project();
- let buffer = project.update(cx, |project, cx| {
- let text = Assets::get(asset_path)
- .map(|f| f.data)
- .unwrap_or_else(|| Cow::Borrowed(b"File not found"));
- let text = str::from_utf8(text.as_ref()).unwrap();
+ let buffer = project.update(cx, move |project, cx| {
project
- .create_buffer(text, language, cx)
+ .create_buffer(text.as_ref(), language, cx)
.expect("creating buffers on a local workspace always succeeds")
});
let buffer = cx.add_model(|cx| {