.gitignore 🔗
@@ -18,3 +18,4 @@ DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
+**/*.db
Antonio Scandurra created
.gitignore | 1
Cargo.lock | 330
Cargo.toml | 4
crates/activity_indicator/src/activity_indicator.rs | 2
crates/auto_update/Cargo.toml | 1
crates/auto_update/src/auto_update.rs | 34
crates/auto_update/src/update_notification.rs | 9
crates/breadcrumbs/src/breadcrumbs.rs | 5
crates/client/src/client.rs | 7
crates/client/src/telemetry.rs | 26
crates/collab/src/integration_tests.rs | 167
crates/collab_ui/src/collab_ui.rs | 8
crates/collab_ui/src/contact_notification.rs | 2
crates/command_palette/src/command_palette.rs | 5
crates/db/Cargo.toml | 12
crates/db/README.md | 5
crates/db/src/db.rs | 430 +
crates/db/src/items.rs | 311
crates/db/src/kvp.rs | 110
crates/db/src/migrations.rs | 15
crates/db/src/query.rs | 314
crates/diagnostics/src/diagnostics.rs | 40
crates/diagnostics/src/items.rs | 4
crates/editor/Cargo.toml | 2
crates/editor/src/editor.rs | 6
crates/editor/src/editor_tests.rs | 7
crates/editor/src/items.rs | 106
crates/editor/src/persistence.rs | 36
crates/editor/src/test/editor_lsp_test_context.rs | 11
crates/file_finder/src/file_finder.rs | 30
crates/gpui/Cargo.toml | 1
crates/gpui/grammars/context-predicate/bindings/node/binding.cc | 12
crates/gpui/src/presenter.rs | 33
crates/gpui_macros/Cargo.toml | 1
crates/journal/src/journal.rs | 6
crates/project/src/project.rs | 3
crates/project_panel/src/project_panel.rs | 22
crates/search/src/buffer_search.rs | 3
crates/search/src/project_search.rs | 22
crates/settings/Cargo.toml | 1
crates/settings/src/settings.rs | 51
crates/sqlez/.gitignore | 2
crates/sqlez/Cargo.lock | 150
crates/sqlez/Cargo.toml | 16
crates/sqlez/src/bindable.rs | 352 +
crates/sqlez/src/connection.rs | 334 +
crates/sqlez/src/domain.rs | 56
crates/sqlez/src/lib.rs | 11
crates/sqlez/src/migrations.rs | 260
crates/sqlez/src/savepoint.rs | 148
crates/sqlez/src/statement.rs | 491 +
crates/sqlez/src/thread_safe_connection.rs | 359 +
crates/sqlez/src/typed_statements.rs | 60
crates/sqlez/src/util.rs | 32
crates/sqlez_macros/Cargo.toml | 17
crates/sqlez_macros/src/sqlez_macros.rs | 93
crates/terminal/Cargo.toml | 1
crates/terminal/src/persistence.rs | 52
crates/terminal/src/terminal.rs | 47
crates/terminal/src/terminal_container_view.rs | 73
crates/terminal/src/tests/terminal_test_context.rs | 12
crates/theme/src/theme.rs | 8
crates/theme_testbench/src/theme_testbench.rs | 26
crates/util/Cargo.toml | 1
crates/util/src/channel.rs | 40
crates/util/src/lib.rs | 40
crates/util/src/paths.rs | 0
crates/vim/src/test/vim_test_context.rs | 11
crates/workspace/Cargo.toml | 7
crates/workspace/src/dock.rs | 35
crates/workspace/src/item.rs | 905 ++
crates/workspace/src/notifications.rs | 280
crates/workspace/src/pane.rs | 22
crates/workspace/src/pane_group.rs | 14
crates/workspace/src/persistence.rs | 836 ++
crates/workspace/src/persistence/model.rs | 315
crates/workspace/src/searchable.rs | 2
crates/workspace/src/shared_screen.rs | 32
crates/workspace/src/workspace.rs | 985 --
crates/workspace/test.db | 0
crates/zed/Cargo.toml | 1
crates/zed/src/feedback.rs | 4
crates/zed/src/main.rs | 53
crates/zed/src/zed.rs | 205
styles/src/styleTree/app.ts | 2
styles/src/styleTree/simpleMessageNotification.ts | 31
86 files changed, 6,920 insertions(+), 1,696 deletions(-)
@@ -18,3 +18,4 @@ DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
+**/*.db
@@ -14,7 +14,7 @@ version = "0.1.0"
dependencies = [
"auto_update",
"editor",
- "futures 0.3.24",
+ "futures 0.3.25",
"gpui",
"language",
"project",
@@ -51,16 +51,16 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
- "getrandom 0.2.7",
+ "getrandom 0.2.8",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
-version = "0.7.19"
+version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
@@ -145,9 +145,12 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.65"
+version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
+checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
+dependencies = [
+ "backtrace",
+]
[[package]]
name = "arrayref"
@@ -195,9 +198,9 @@ dependencies = [
[[package]]
name = "async-channel"
-version = "1.7.1"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
+checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
dependencies = [
"concurrent-queue",
"event-listener",
@@ -232,15 +235,15 @@ dependencies = [
[[package]]
name = "async-executor"
-version = "1.4.1"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
+checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b"
dependencies = [
+ "async-lock",
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
- "once_cell",
"slab",
]
@@ -258,22 +261,22 @@ dependencies = [
[[package]]
name = "async-io"
-version = "1.9.0"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7"
+checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794"
dependencies = [
+ "async-lock",
"autocfg 1.1.0",
"concurrent-queue",
"futures-lite",
"libc",
"log",
- "once_cell",
"parking",
"polling",
"slab",
"socket2",
"waker-fn",
- "winapi 0.3.9",
+ "windows-sys 0.42.0",
]
[[package]]
@@ -303,26 +306,26 @@ name = "async-pipe"
version = "0.1.3"
source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553"
dependencies = [
- "futures 0.3.24",
+ "futures 0.3.25",
"log",
]
[[package]]
name = "async-process"
-version = "1.5.0"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c"
+checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4"
dependencies = [
"async-io",
+ "async-lock",
"autocfg 1.1.0",
"blocking",
"cfg-if 1.0.0",
"event-listener",
"futures-lite",
"libc",
- "once_cell",
"signal-hook",
- "winapi 0.3.9",
+ "windows-sys 0.42.0",
]
[[package]]
@@ -336,6 +339,17 @@ dependencies = [
"syn",
]
+[[package]]
+name = "async-recursion"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "async-stream"
version = "0.3.3"
@@ -377,9 +391,9 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.57"
+version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
+checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
dependencies = [
"proc-macro2",
"quote",
@@ -441,6 +455,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
+ "db",
"gpui",
"isahc",
"lazy_static",
@@ -474,15 +489,15 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
-version = "0.5.16"
+version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043"
+checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43"
dependencies = [
"async-trait",
"axum-core",
"base64",
"bitflags",
- "bytes 1.2.1",
+ "bytes 1.3.0",
"futures-util",
"headers",
"http",
@@ -497,7 +512,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
- "sha-1 0.10.0",
+ "sha-1 0.10.1",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
@@ -509,12 +524,12 @@ dependencies = [
[[package]]
name = "axum-core"
-version = "0.2.8"
+version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b"
+checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc"
dependencies = [
"async-trait",
- "bytes 1.2.1",
+ "bytes 1.3.0",
"futures-util",
"http",
"http-body",
@@ -530,7 +545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb"
dependencies = [
"axum",
- "bytes 1.2.1",
+ "bytes 1.3.0",
"futures-util",
"http",
"mime",
@@ -574,15 +589,15 @@ dependencies = [
[[package]]
name = "base64"
-version = "0.13.0"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64ct"
-version = "1.5.2"
+version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474"
+checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf"
[[package]]
name = "bincode"
@@ -648,16 +663,16 @@ dependencies = [
[[package]]
name = "blocking"
-version = "1.2.0"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc"
+checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8"
dependencies = [
"async-channel",
+ "async-lock",
"async-task",
"atomic-waker",
"fastrand",
"futures-lite",
- "once_cell",
]
[[package]]
@@ -744,9 +759,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.11.0"
+version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "bytecheck"
@@ -771,9 +786,9 @@ dependencies = [
[[package]]
name = "bytemuck"
-version = "1.12.1"
+version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da"
+checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
[[package]]
name = "byteorder"
@@ -793,15 +808,9 @@ dependencies = [
[[package]]
name = "bytes"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
-
-[[package]]
-name = "cache-padded"
-version = "1.2.0"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
+checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
[[package]]
name = "call"
@@ -811,7 +820,7 @@ dependencies = [
"async-broadcast",
"client",
"collections",
- "futures 0.3.24",
+ "futures 0.3.25",
"gpui",
"live_kit_client",
"media",
@@ -894,9 +903,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
[[package]]
name = "cc"
-version = "1.0.73"
+version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
dependencies = [
"jobserver",
]
@@ -924,16 +933,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.22"
+version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"serde",
- "time 0.1.44",
+ "time 0.1.45",
"wasm-bindgen",
"winapi 0.3.9",
]
@@ -981,9 +990,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "3.2.22"
+version = "3.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
+checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
dependencies = [
"atty",
"bitflags",
@@ -993,7 +1002,7 @@ dependencies = [
"once_cell",
"strsim 0.10.0",
"termcolor",
- "textwrap 0.15.1",
+ "textwrap 0.16.0",
]
[[package]]
@@ -1023,7 +1032,7 @@ name = "cli"
version = "0.1.0"
dependencies = [
"anyhow",
- "clap 3.2.22",
+ "clap 3.2.23",
"core-foundation",
"core-services",
"dirs 3.0.2",
@@ -1037,11 +1046,11 @@ name = "client"
version = "0.1.0"
dependencies = [
"anyhow",
- "async-recursion",
+ "async-recursion 0.3.2",
"async-tungstenite",
"collections",
"db",
- "futures 0.3.24",
+ "futures 0.3.25",
"gpui",
"image",
"isahc",
@@ -1057,11 +1066,11 @@ dependencies = [
"sum_tree",
"tempfile",
"thiserror",
- "time 0.3.15",
+ "time 0.3.17",
"tiny_http",
"url",
"util",
- "uuid 1.2.1",
+ "uuid 1.2.2",
]
[[package]]
@@ -1073,9 +1082,9 @@ dependencies = [
[[package]]
name = "cmake"
-version = "0.1.48"
+version = "0.1.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a"
+checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c"
dependencies = [
"cc",
]
@@ -1129,7 +1138,7 @@ dependencies = [
"axum-extra",
"base64",
"call",
- "clap 3.2.22",
+ "clap 3.2.23",
"client",
"collections",
"ctor",
@@ -1138,7 +1147,7 @@ dependencies = [
"env_logger",
"envy",
"fs",
- "futures 0.3.24",
+ "futures 0.3.25",
"git",
"gpui",
"hyper",
@@ -1165,7 +1174,7 @@ dependencies = [
"sha-1 0.9.8",
"sqlx",
"theme",
- "time 0.3.15",
+ "time 0.3.17",
"tokio",
"tokio-tungstenite",
"toml",
@@ -1189,7 +1198,7 @@ dependencies = [
"clock",
"collections",
"editor",
- "futures 0.3.24",
+ "futures 0.3.25",
"fuzzy",
"gpui",
"log",
@@ -1238,11 +1247,11 @@ dependencies = [
[[package]]
name = "concurrent-queue"
-version = "1.2.4"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
+checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
dependencies = [
- "cache-padded",
+ "crossbeam-utils 0.8.14",
]
[[package]]
@@ -1469,7 +1478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.12",
+ "crossbeam-utils 0.8.14",
]
[[package]]
@@ -1480,30 +1489,30 @@ checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
- "crossbeam-utils 0.8.12",
+ "crossbeam-utils 0.8.14",
]
[[package]]
name = "crossbeam-epoch"
-version = "0.9.11"
+version = "0.9.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348"
+checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
dependencies = [
"autocfg 1.1.0",
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.12",
- "memoffset",
+ "crossbeam-utils 0.8.14",
+ "memoffset 0.7.1",
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
-version = "0.3.6"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7"
+checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.12",
+ "crossbeam-utils 0.8.14",
]
[[package]]
@@ -1519,9 +1528,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
-version = "0.8.12"
+version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
+checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
dependencies = [
"cfg-if 1.0.0",
]
@@ -1548,9 +1557,9 @@ dependencies = [
[[package]]
name = "ctor"
-version = "0.1.23"
+version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn",
@@ -1573,9 +1582,9 @@ dependencies = [
[[package]]
name = "curl-sys"
-version = "0.4.56+curl-7.83.1"
+version = "0.4.59+curl-7.86.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6093e169dd4de29e468fa649fbae11cdcd5551c81fe5bf1b0677adad7ef3d26f"
+checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
dependencies = [
"cc",
"libc",
@@ -1589,9 +1598,9 @@ dependencies = [
[[package]]
name = "cxx"
-version = "1.0.79"
+version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8"
+checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -1601,9 +1610,9 @@ dependencies = [
[[package]]
name = "cxx-build"
-version = "1.0.79"
+version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86"
+checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
dependencies = [
"cc",
"codespan-reporting",
@@ -1616,15 +1625,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
-version = "1.0.79"
+version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78"
+checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
[[package]]
name = "cxxbridge-macro"
-version = "1.0.79"
+version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f"
+checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
dependencies = [
"proc-macro2",
"quote",
@@ -1641,7 +1650,7 @@ dependencies = [
"hashbrown 0.12.3",
"lock_api",
"once_cell",
- "parking_lot_core 0.9.4",
+ "parking_lot_core 0.9.5",
]
[[package]]
@@ -1660,15 +1669,18 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
+ "env_logger",
"gpui",
+ "indoc",
"lazy_static",
"log",
"parking_lot 0.11.2",
- "rusqlite",
- "rusqlite_migration",
"serde",
- "serde_rusqlite",
+ "smol",
+ "sqlez",
+ "sqlez_macros",
"tempdir",
+ "util",
]
[[package]]
@@ -1683,12 +1695,13 @@ dependencies = [
[[package]]
name = "dhat"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0684eaa19a59be283a6f99369917b679bd4d1d06604b2eb2e2f87b4bbd67668d"
+checksum = "4f2aaf837aaf456f6706cb46386ba8dffd4013a757e36f4ea05c20dd46b209a3"
dependencies = [
"backtrace",
"lazy_static",
+ "mintex",
"parking_lot 0.12.1",
"rustc-hash",
"serde",
@@ -1728,9 +1741,9 @@ dependencies = [
[[package]]
name = "digest"
-version = "0.10.5"
+version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [
"block-buffer 0.10.3",
"crypto-common",
@@ -1845,9 +1858,10 @@ dependencies = [
"collections",
"context_menu",
"ctor",
+ "db",
"drag_and_drop",
"env_logger",
- "futures 0.3.24",
+ "futures 0.3.25",
"fuzzy",
"git",
"gpui",
@@ -1868,6 +1882,7 @@ dependencies = [
"smallvec",
"smol",
"snippet",
+ "sqlez",
"sum_tree",
"text",
"theme",
@@ -1897,9 +1912,9 @@ dependencies = [
[[package]]
name = "env_logger"
-version = "0.9.1"
+version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272"
+checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
dependencies = [
"atty",
"humantime",
@@ -1988,12 +2003,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
-[[package]]
-name = "fallible-streaming-iterator"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
-
[[package]]
name = "fastrand"
version = "1.8.0"
@@ -2041,12 +2050,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
-version = "1.0.24"
+version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [
"crc32fast",
- "miniz_oxide 0.5.4",
+ "miniz_oxide 0.6.2",
]
[[package]]
@@ -2167,7 +2176,7 @@ dependencies = [
"async-trait",
"collections",
"fsevent",
- "futures 0.3.24",
+ "futures 0.3.25",
"git2",
"gpui",
"lazy_static",
@@ -2244,9 +2253,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
[[package]]
name = "futures"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
+checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
dependencies = [
"futures-channel",
"futures-core",
@@ -2259,9 +2268,9 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
+checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
dependencies = [
"futures-core",
"futures-sink",
@@ -2269,15 +2278,15 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
+checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
[[package]]
name = "futures-executor"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
+checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [
"futures-core",
"futures-task",
@@ -2286,9 +2295,9 @@ dependencies = [
[[package]]
name = "futures-intrusive"
-version = "0.4.0"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e"
+checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5"
dependencies = [
"futures-core",
"lock_api",
@@ -2297,9 +2306,9 @@ dependencies = [
[[package]]
name = "futures-io"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
+checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
@@ -2318,9 +2327,9 @@ dependencies = [
[[package]]
name = "futures-macro"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
+checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [
"proc-macro2",
"quote",
@@ -2329,21 +2338,21 @@ dependencies = [
[[package]]
name = "futures-sink"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
+checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
[[package]]
name = "futures-task"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
+checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
[[package]]
name = "futures-util"
-version = "0.3.24"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
+checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [
"futures 0.1.31",
"futures-channel",
@@ -2399,9 +2408,9 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.7"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
"libc",
@@ -2437,7 +2446,7 @@ dependencies = [
"async-trait",
"clock",
"collections",
- "futures 0.3.24",
+ "futures 0.3.25",
"git2",
"lazy_static",
"log",
@@ -2515,7 +2524,7 @@ dependencies = [
"etagere",
"font-kit",
"foreign-types",
- "futures 0.3.24",
+ "futures 0.3.25",
"gpui_macros",
"image",
"itertools",
@@ -2540,8 +2549,9 @@ dependencies = [
"simplelog",
"smallvec",
"smol",
+ "sqlez",
"sum_tree",
- "time 0.3.15",
+ "time 0.3.17",
"tiny-skia",
"tree-sitter",
"usvg",
@@ -2560,11 +2570,11 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.3.14"
+version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
+checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
dependencies = [
- "bytes 1.2.1",
+ "bytes 1.3.0",
"fnv",
"futures-core",
"futures-sink",
@@ -2612,7 +2622,7 @@ checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
dependencies = [
"base64",
"bitflags",
- "bytes 1.2.1",
+ "bytes 1.3.0",
"headers-core",
"http",
"httpdate",
@@ -2696,7 +2706,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
- "digest 0.10.5",
+ "digest 0.10.6",
]
[[package]]
@@ -2705,7 +2715,7 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
- "bytes 1.2.1",
+ "bytes 1.3.0",
"fnv",
"itoa",
]
@@ -2716,7 +2726,7 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
- "bytes 1.2.1",
+ "bytes 1.3.0",
"http",
"pin-project-lite 0.2.9",
]
@@ -2747,11 +2757,11 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
-version = "0.14.20"
+version = "0.14.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
+checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
dependencies = [
- "bytes 1.2.1",
+ "bytes 1.3.0",
"futures-channel",
"futures-core",
"futures-util",
@@ -2787,7 +2797,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
- "bytes 1.2.1",
+ "bytes 1.3.0",
"hyper",
"native-tls",
"tokio",
@@ -2796,9 +2806,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
-version = "0.1.51"
+version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -2834,7 +2844,7 @@ version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
dependencies = [
- "crossbeam-utils 0.8.12",
+ "crossbeam-utils 0.8.14",
"globset",
"lazy_static",
"log",
@@ -2867,9 +2877,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "1.9.1"
+version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg 1.1.0",
"hashbrown 0.12.3",
@@ -2941,9 +2951,9 @@ dependencies = [
[[package]]
name = "ipnet"
-version = "2.5.0"
+version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
+checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745"
[[package]]
name = "is-terminal"
@@ -2965,7 +2975,7 @@ checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
dependencies = [
"async-channel",
"castaway",
- "crossbeam-utils 0.8.12",
+ "crossbeam-utils 0.8.14",
"curl",
"curl-sys",
"encoding_rs",
@@ -45,6 +45,8 @@ members = [
"crates/search",
"crates/settings",
"crates/snippet",
+ "crates/sqlez",
+ "crates/sqlez_macros",
"crates/sum_tree",
"crates/terminal",
"crates/text",
@@ -67,7 +69,6 @@ rand = { version = "0.8" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
-sqlx = { git = "https://github.com/launchbadge/sqlx", rev = "4b7053807c705df312bcb9b6281e184bf7534eb3" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
@@ -82,3 +83,4 @@ split-debuginfo = "unpacked"
[profile.release]
debug = true
+
@@ -11,7 +11,7 @@ use settings::Settings;
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc};
use util::ResultExt;
-use workspace::{ItemHandle, StatusItemView, Workspace};
+use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(lsp_status, [ShowErrorMessage]);
@@ -8,6 +8,7 @@ path = "src/auto_update.rs"
doctest = false
[dependencies]
+db = { path = "../db" }
client = { path = "../client" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
@@ -1,17 +1,18 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
-use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
+use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
+use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, WeakViewHandle,
};
use lazy_static::lazy_static;
use serde::Deserialize;
-use settings::ReleaseChannel;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
+use util::channel::ReleaseChannel;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -41,7 +42,6 @@ pub struct AutoUpdater {
current_version: AppVersion,
http_client: Arc<dyn HttpClient>,
pending_poll: Option<Task<()>>,
- db: project::Db,
server_url: String,
}
@@ -55,11 +55,11 @@ impl Entity for AutoUpdater {
type Event = ();
}
-pub fn init(db: project::Db, http_client: Arc<dyn HttpClient>, cx: &mut MutableAppContext) {
+pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut MutableAppContext) {
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
- let server_url = ZED_SERVER_URL.to_string();
+ let server_url = server_url;
let auto_updater = cx.add_model(|cx| {
- let updater = AutoUpdater::new(version, db.clone(), http_client, server_url.clone());
+ let updater = AutoUpdater::new(version, http_client, server_url.clone());
updater.start_polling(cx).detach();
updater
});
@@ -120,14 +120,12 @@ impl AutoUpdater {
fn new(
current_version: AppVersion,
- db: project::Db,
http_client: Arc<dyn HttpClient>,
server_url: String,
) -> Self {
Self {
status: AutoUpdateStatus::Idle,
current_version,
- db,
http_client,
server_url,
pending_poll: None,
@@ -297,20 +295,28 @@ impl AutoUpdater {
should_show: bool,
cx: &AppContext,
) -> Task<Result<()>> {
- let db = self.db.clone();
cx.background().spawn(async move {
if should_show {
- db.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?;
+ KEY_VALUE_STORE
+ .write_kvp(
+ SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
+ "".to_string(),
+ )
+ .await?;
} else {
- db.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?;
+ KEY_VALUE_STORE
+ .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
+ .await?;
}
Ok(())
})
}
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
- let db = self.db.clone();
- cx.background()
- .spawn(async move { Ok(db.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some()) })
+ cx.background().spawn(async move {
+ Ok(KEY_VALUE_STORE
+ .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
+ .is_some())
+ })
}
}
@@ -5,8 +5,9 @@ use gpui::{
Element, Entity, MouseButton, View, ViewContext,
};
use menu::Cancel;
-use settings::{ReleaseChannel, Settings};
-use workspace::Notification;
+use settings::Settings;
+use util::channel::ReleaseChannel;
+use workspace::notifications::Notification;
pub struct UpdateNotification {
version: AppVersion,
@@ -27,9 +28,9 @@ impl View for UpdateNotification {
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
let theme = cx.global::<Settings>().theme.clone();
- let theme = &theme.update_notification;
+ let theme = &theme.simple_message_notification;
- let app_name = cx.global::<ReleaseChannel>().name();
+ let app_name = cx.global::<ReleaseChannel>().display_name();
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
Flex::column()
@@ -4,7 +4,10 @@ use gpui::{
use itertools::Itertools;
use search::ProjectSearchView;
use settings::Settings;
-use workspace::{ItemEvent, ItemHandle, ToolbarItemLocation, ToolbarItemView};
+use workspace::{
+ item::{ItemEvent, ItemHandle},
+ ToolbarItemLocation, ToolbarItemView,
+};
pub enum Event {
UpdateLocation,
@@ -11,7 +11,6 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{Request, StatusCode},
};
-use db::Db;
use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
use gpui::{
actions,
@@ -26,7 +25,6 @@ use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
use serde::Deserialize;
-use settings::ReleaseChannel;
use std::{
any::TypeId,
collections::HashMap,
@@ -41,6 +39,7 @@ use std::{
use telemetry::Telemetry;
use thiserror::Error;
use url::Url;
+use util::channel::ReleaseChannel;
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
@@ -1278,8 +1277,8 @@ impl Client {
}
}
- pub fn start_telemetry(&self, db: Db) {
- self.telemetry.start(db.clone());
+ pub fn start_telemetry(&self) {
+ self.telemetry.start();
}
pub fn report_event(&self, kind: &str, properties: Value) {
@@ -1,5 +1,5 @@
use crate::http::HttpClient;
-use db::Db;
+use db::kvp::KEY_VALUE_STORE;
use gpui::{
executor::Background,
serde_json::{self, value::Map, Value},
@@ -10,7 +10,6 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
-use settings::ReleaseChannel;
use std::{
io::Write,
mem,
@@ -19,7 +18,7 @@ use std::{
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tempfile::NamedTempFile;
-use util::{post_inc, ResultExt, TryFutureExt};
+use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
pub struct Telemetry {
@@ -107,7 +106,7 @@ impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
let platform = cx.platform();
let release_channel = if cx.has_global::<ReleaseChannel>() {
- Some(cx.global::<ReleaseChannel>().name())
+ Some(cx.global::<ReleaseChannel>().display_name())
} else {
None
};
@@ -148,18 +147,21 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
- pub fn start(self: &Arc<Self>, db: Db) {
+ pub fn start(self: &Arc<Self>) {
let this = self.clone();
self.executor
.spawn(
async move {
- let device_id = if let Ok(Some(device_id)) = db.read_kvp("device_id") {
- device_id
- } else {
- let device_id = Uuid::new_v4().to_string();
- db.write_kvp("device_id", &device_id)?;
- device_id
- };
+ let device_id =
+ if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
+ device_id
+ } else {
+ let device_id = Uuid::new_v4().to_string();
+ KEY_VALUE_STORE
+ .write_kvp("device_id".to_string(), device_id.clone())
+ .await?;
+ device_id
+ };
let device_id: Arc<str> = device_id.into();
let mut state = this.state.lock();
@@ -3,6 +3,7 @@ use crate::{
rpc::{Executor, Server},
AppState,
};
+
use ::rpc::Peer;
use anyhow::anyhow;
use call::{room, ActiveCall, ParticipantLocation, Room};
@@ -49,7 +50,7 @@ use std::{
use theme::ThemeRegistry;
use unindent::Unindent as _;
use util::post_inc;
-use workspace::{shared_screen::SharedScreen, Item, SplitDirection, ToggleFollow, Workspace};
+use workspace::{item::Item, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace};
#[ctor::ctor]
fn init_logger() {
@@ -899,8 +900,15 @@ async fn test_host_disconnect(
let project_b = client_b.build_remote_project(project_id, cx_b).await;
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
- let (_, workspace_b) =
- cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
+ let (_, workspace_b) = cx_b.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project_b.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
@@ -3691,8 +3699,15 @@ async fn test_collaborating_with_code_actions(
// Join the project as client B.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
- let (_window_b, workspace_b) =
- cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
+ let (_window_b, workspace_b) = cx_b.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project_b.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -3912,8 +3927,15 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
- let (_window_b, workspace_b) =
- cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
+ let (_window_b, workspace_b) = cx_b.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project_b.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
@@ -4943,6 +4965,129 @@ async fn test_following(
);
}
+#[gpui::test]
+async fn test_following_tab_order(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ let mut server = TestServer::start(cx_a.background()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "1.txt": "one",
+ "2.txt": "two",
+ "3.txt": "three",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let workspace_a = client_a.build_workspace(&project_a, cx_a);
+ let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+ let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+
+ let client_b_id = project_a.read_with(cx_a, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+
+ //Open 1, 3 in that order on client A
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+
+ let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
+ pane.update(cx, |pane, cx| {
+ pane.items()
+ .map(|item| {
+ item.project_path(cx)
+ .unwrap()
+ .path
+ .to_str()
+ .unwrap()
+ .to_owned()
+ })
+ .collect::<Vec<_>>()
+ })
+ };
+
+ //Verify that the tabs opened in the order we expect
+ assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
+
+ //Follow client B as client A
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace
+ .toggle_follow(&ToggleFollow(client_b_id), cx)
+ .unwrap()
+ })
+ .await
+ .unwrap();
+
+ //Open just 2 on client B
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+
+ // Verify that newly opened followed file is at the end
+ assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+
+ //Open just 1 on client B
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
+ deterministic.run_until_parked();
+
+ // Verify that following into 1 did not reorder
+ assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+}
+
#[gpui::test(iterations = 10)]
async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
@@ -6041,7 +6186,13 @@ impl TestClient {
) -> ViewHandle<Workspace> {
let (_, root_view) = cx.add_window(|_| EmptyView);
cx.add_view(&root_view, |cx| {
- Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
})
}
@@ -50,7 +50,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
.await?;
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
- let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
+ let mut workspace = Workspace::new(
+ Default::default(),
+ 0,
+ project,
+ app_state.default_item_factory,
+ cx,
+ );
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
});
@@ -6,7 +6,7 @@ use gpui::{
elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext,
View, ViewContext,
};
-use workspace::Notification;
+use workspace::notifications::Notification;
impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
@@ -350,8 +350,9 @@ mod tests {
});
let project = Project::test(app_state.fs.clone(), [], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let editor = cx.add_view(&workspace, |cx| {
let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx);
@@ -12,16 +12,20 @@ test-support = []
[dependencies]
collections = { path = "../collections" }
+gpui = { path = "../gpui" }
+sqlez = { path = "../sqlez" }
+sqlez_macros = { path = "../sqlez_macros" }
+util = { path = "../util" }
anyhow = "1.0.57"
+indoc = "1.0.4"
async-trait = "0.1"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
-rusqlite = { version = "0.28.0", features = ["bundled", "serde_json"] }
-rusqlite_migration = { git = "https://github.com/cljoly/rusqlite_migration", rev = "c433555d7c1b41b103426e35756eb3144d0ebbc6" }
-serde = { workspace = true }
-serde_rusqlite = "0.31.0"
+serde = { version = "1.0", features = ["derive"] }
+smol = "1.2"
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
+env_logger = "0.9.1"
tempdir = { version = "0.3.7" }
@@ -0,0 +1,5 @@
+# Building Queries
+
+First, craft your test data. The examples folder shows a template for building a test-db, and can be ran with `cargo run --example [your-example]`.
+
+To actually use and test your queries, import the generated DB file into https://sqliteonline.com/
@@ -1,119 +1,365 @@
-mod kvp;
-mod migrations;
+pub mod kvp;
+pub mod query;
-use std::fs;
+// Re-export
+pub use anyhow;
+use anyhow::Context;
+pub use indoc::indoc;
+pub use lazy_static;
+use parking_lot::{Mutex, RwLock};
+pub use smol;
+pub use sqlez;
+pub use sqlez_macros;
+pub use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
+pub use util::paths::DB_DIR;
+
+use sqlez::domain::Migrator;
+use sqlez::thread_safe_connection::ThreadSafeConnection;
+use sqlez_macros::sql;
+use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
-use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::time::{SystemTime, UNIX_EPOCH};
+use util::{async_iife, ResultExt};
+use util::channel::ReleaseChannel;
-use anyhow::Result;
-use log::error;
-use parking_lot::Mutex;
-use rusqlite::Connection;
+const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
+ PRAGMA foreign_keys=TRUE;
+);
-use migrations::MIGRATIONS;
+const DB_INITIALIZE_QUERY: &'static str = sql!(
+ PRAGMA journal_mode=WAL;
+ PRAGMA busy_timeout=1;
+ PRAGMA case_sensitive_like=TRUE;
+ PRAGMA synchronous=NORMAL;
+);
-#[derive(Clone)]
-pub enum Db {
- Real(Arc<RealDb>),
- Null,
-}
+const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
-pub struct RealDb {
- connection: Mutex<Connection>,
- path: Option<PathBuf>,
+const DB_FILE_NAME: &'static str = "db.sqlite";
+
+lazy_static::lazy_static! {
+ static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
+ pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
+ pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
-impl Db {
- /// Open or create a database at the given directory path.
- pub fn open(db_dir: &Path, channel: &'static str) -> Self {
- // Use 0 for now. Will implement incrementing and clearing of old db files soon TM
- let current_db_dir = db_dir.join(Path::new(&format!("0-{}", channel)));
- fs::create_dir_all(¤t_db_dir)
- .expect("Should be able to create the database directory");
- let db_path = current_db_dir.join(Path::new("db.sqlite"));
-
- Connection::open(db_path)
- .map_err(Into::into)
- .and_then(|connection| Self::initialize(connection))
- .map(|connection| {
- Db::Real(Arc::new(RealDb {
- connection,
- path: Some(db_dir.to_path_buf()),
- }))
- })
- .unwrap_or_else(|e| {
- error!(
- "Connecting to file backed db failed. Reverting to null db. {}",
- e
- );
- Self::Null
- })
- }
+/// Open or create a database at the given directory path.
+/// This will retry a couple times if there are failures. If opening fails once, the db directory
+/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
+/// In either case, static variables are set so that the user can be notified.
+pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &ReleaseChannel) -> ThreadSafeConnection<M> {
+ let release_channel_name = release_channel.dev_name();
+ let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
- /// Open a in memory database for testing and as a fallback.
- #[cfg(any(test, feature = "test-support"))]
- pub fn open_in_memory() -> Self {
- Connection::open_in_memory()
- .map_err(Into::into)
- .and_then(|connection| Self::initialize(connection))
- .map(|connection| {
- Db::Real(Arc::new(RealDb {
- connection,
- path: None,
- }))
- })
- .unwrap_or_else(|e| {
- error!(
- "Connecting to in memory db failed. Reverting to null db. {}",
- e
- );
- Self::Null
- })
- }
+ let connection = async_iife!({
+ // Note: This still has a race condition where 1 set of migrations succeeds
+ // (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
+ // This will cause the first connection to have the database taken out
+ // from under it. This *should* be fine though. The second dabatase failure will
+ // cause errors in the log and so should be observed by developers while writing
+ // soon-to-be good migrations. If user databases are corrupted, we toss them out
+ // and try again from a blank. As long as running all migrations from start to end
+ // on a blank database is ok, this race condition will never be triggered.
+ //
+ // Basically: Don't ever push invalid migrations to stable or everyone will have
+ // a bad time.
+
+ // If no db folder, create one at 0-{channel}
+ create_dir_all(&main_db_dir).context("Could not create db directory")?;
+ let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
+
+ // Optimistically open databases in parallel
+ if !DB_FILE_OPERATIONS.is_locked() {
+ // Try building a connection
+ if let Some(connection) = open_main_db(&db_path).await {
+ return Ok(connection)
+ };
+ }
+
+ // Take a lock in the failure case so that we move the db once per process instead
+ // of potentially multiple times from different threads. This shouldn't happen in the
+ // normal path
+ let _lock = DB_FILE_OPERATIONS.lock();
+ if let Some(connection) = open_main_db(&db_path).await {
+ return Ok(connection)
+ };
+
+ let backup_timestamp = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("System clock is set before the unix timestamp, Zed does not support this region of spacetime")
+ .as_millis();
+
+ // If failed, move 0-{channel} to {current unix timestamp}-{channel}
+ let backup_db_dir = db_dir.join(Path::new(&format!(
+ "{}-{}",
+ backup_timestamp,
+ release_channel_name,
+ )));
+
+ std::fs::rename(&main_db_dir, &backup_db_dir)
+ .context("Failed clean up corrupted database, panicking.")?;
- fn initialize(mut conn: Connection) -> Result<Mutex<Connection>> {
- MIGRATIONS.to_latest(&mut conn)?;
+ // Set a static ref with the failed timestamp and error so we can notify the user
+ {
+ let mut guard = BACKUP_DB_PATH.write();
+ *guard = Some(backup_db_dir);
+ }
+
+ // Create a new 0-{channel}
+ create_dir_all(&main_db_dir).context("Should be able to create the database directory")?;
+ let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
- conn.pragma_update(None, "journal_mode", "WAL")?;
- conn.pragma_update(None, "synchronous", "NORMAL")?;
- conn.pragma_update(None, "foreign_keys", true)?;
- conn.pragma_update(None, "case_sensitive_like", true)?;
+ // Try again
+ open_main_db(&db_path).await.context("Could not newly created db")
+ }).await.log_err();
- Ok(Mutex::new(conn))
+ if let Some(connection) = connection {
+ return connection;
}
+
+ // Set another static ref so that we can escalate the notification
+ ALL_FILE_DB_FAILED.store(true, Ordering::Release);
+
+ // If still failed, create an in memory db with a known name
+ open_fallback_db().await
+}
- pub fn persisting(&self) -> bool {
- self.real().and_then(|db| db.path.as_ref()).is_some()
- }
+async fn open_main_db<M: Migrator>(db_path: &PathBuf) -> Option<ThreadSafeConnection<M>> {
+ log::info!("Opening main db");
+ ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
+ .with_db_initialization_query(DB_INITIALIZE_QUERY)
+ .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
+ .build()
+ .await
+ .log_err()
+}
- pub fn real(&self) -> Option<&RealDb> {
- match self {
- Db::Real(db) => Some(&db),
- _ => None,
- }
- }
+async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection<M> {
+ log::info!("Opening fallback db");
+ ThreadSafeConnection::<M>::builder(FALLBACK_DB_NAME, false)
+ .with_db_initialization_query(DB_INITIALIZE_QUERY)
+ .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
+ .build()
+ .await
+ .expect(
+ "Fallback in memory database failed. Likely initialization queries or migrations have fundamental errors",
+ )
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection<M> {
+ use sqlez::thread_safe_connection::locking_queue;
+
+ ThreadSafeConnection::<M>::builder(db_name, false)
+ .with_db_initialization_query(DB_INITIALIZE_QUERY)
+ .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
+ // Serialize queued writes via a mutex and run them synchronously
+ .with_write_queue_constructor(locking_queue())
+ .build()
+ .await
+ .unwrap()
}
-impl Drop for Db {
- fn drop(&mut self) {
- match self {
- Db::Real(real_db) => {
- let lock = real_db.connection.lock();
+/// Implements a basic DB wrapper for a given domain
+#[macro_export]
+macro_rules! define_connection {
+ (pub static ref $id:ident: $t:ident<()> = $migrations:expr;) => {
+ pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>);
- let _ = lock.pragma_update(None, "analysis_limit", "500");
- let _ = lock.pragma_update(None, "optimize", "");
+ impl ::std::ops::Deref for $t {
+ type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
}
- Db::Null => {}
}
- }
+
+ impl $crate::sqlez::domain::Domain for $t {
+ fn name() -> &'static str {
+ stringify!($t)
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ $migrations
+ }
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ $crate::lazy_static::lazy_static! {
+ pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
+ }
+
+ #[cfg(not(any(test, feature = "test-support")))]
+ $crate::lazy_static::lazy_static! {
+ pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
+ }
+ };
+ (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
+ pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
+
+ impl ::std::ops::Deref for $t {
+ type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<($($d),+, $t)>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+ }
+
+ impl $crate::sqlez::domain::Domain for $t {
+ fn name() -> &'static str {
+ stringify!($t)
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ $migrations
+ }
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ $crate::lazy_static::lazy_static! {
+ pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
+ }
+
+ #[cfg(not(any(test, feature = "test-support")))]
+ $crate::lazy_static::lazy_static! {
+ pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
+ }
+ };
}
#[cfg(test)]
mod tests {
- use crate::migrations::MIGRATIONS;
+ use std::{fs, thread};
+
+ use sqlez::{domain::Domain, connection::Connection};
+ use sqlez_macros::sql;
+ use tempdir::TempDir;
- #[test]
- fn test_migrations() {
- assert!(MIGRATIONS.validate().is_ok());
+ use crate::{open_db, DB_FILE_NAME};
+
+ // Test bad migration panics
+ #[gpui::test]
+ #[should_panic]
+ async fn test_bad_migration_panics() {
+ enum BadDB {}
+
+ impl Domain for BadDB {
+ fn name() -> &'static str {
+ "db_tests"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test(value);),
+ // failure because test already exists
+ sql!(CREATE TABLE test(value);)]
+ }
+ }
+
+ let tempdir = TempDir::new("DbTests").unwrap();
+ let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ }
+
+ /// Test that DB exists but corrupted (causing recreate)
+ #[gpui::test]
+ async fn test_db_corruption() {
+ enum CorruptedDB {}
+
+ impl Domain for CorruptedDB {
+ fn name() -> &'static str {
+ "db_tests"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test(value);)]
+ }
+ }
+
+ enum GoodDB {}
+
+ impl Domain for GoodDB {
+ fn name() -> &'static str {
+ "db_tests" //Notice same name
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test2(value);)] //But different migration
+ }
+ }
+
+ let tempdir = TempDir::new("DbTests").unwrap();
+ {
+ let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ assert!(corrupt_db.persistent());
+ }
+
+
+ let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
+
+ let mut corrupted_backup_dir = fs::read_dir(
+ tempdir.path()
+ ).unwrap().find(|entry| {
+ !entry.as_ref().unwrap().file_name().to_str().unwrap().starts_with("0")
+ }
+ ).unwrap().unwrap().path();
+ corrupted_backup_dir.push(DB_FILE_NAME);
+
+ dbg!(&corrupted_backup_dir);
+
+ let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy());
+ assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()().unwrap().is_none());
+ }
+
+ /// Test that DB exists but corrupted (causing recreate)
+ #[gpui::test]
+ async fn test_simultaneous_db_corruption() {
+ enum CorruptedDB {}
+
+ impl Domain for CorruptedDB {
+ fn name() -> &'static str {
+ "db_tests"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test(value);)]
+ }
+ }
+
+ enum GoodDB {}
+
+ impl Domain for GoodDB {
+ fn name() -> &'static str {
+ "db_tests" //Notice same name
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test2(value);)] //But different migration
+ }
+ }
+
+ let tempdir = TempDir::new("DbTests").unwrap();
+ {
+ // Setup the bad database
+ let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ assert!(corrupt_db.persistent());
+ }
+
+ // Try to connect to it a bunch of times at once
+ let mut guards = vec![];
+ for _ in 0..10 {
+ let tmp_path = tempdir.path().to_path_buf();
+ let guard = thread::spawn(move || {
+ let good_db = smol::block_on(open_db::<GoodDB>(tmp_path.as_path(), &util::channel::ReleaseChannel::Dev));
+ assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
+ });
+
+ guards.push(guard);
+
+ }
+
+ for guard in guards.into_iter() {
+ assert!(guard.join().is_ok());
+ }
}
}
@@ -1,311 +0,0 @@
-use std::{ffi::OsStr, fmt::Display, hash::Hash, os::unix::prelude::OsStrExt, path::PathBuf};
-
-use anyhow::Result;
-use collections::HashSet;
-use rusqlite::{named_params, params};
-
-use super::Db;
-
-pub(crate) const ITEMS_M_1: &str = "
-CREATE TABLE items(
- id INTEGER PRIMARY KEY,
- kind TEXT
-) STRICT;
-CREATE TABLE item_path(
- item_id INTEGER PRIMARY KEY,
- path BLOB
-) STRICT;
-CREATE TABLE item_query(
- item_id INTEGER PRIMARY KEY,
- query TEXT
-) STRICT;
-";
-
-#[derive(PartialEq, Eq, Hash, Debug)]
-pub enum SerializedItemKind {
- Editor,
- Terminal,
- ProjectSearch,
- Diagnostics,
-}
-
-impl Display for SerializedItemKind {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&format!("{:?}", self))
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Hash)]
-pub enum SerializedItem {
- Editor(usize, PathBuf),
- Terminal(usize),
- ProjectSearch(usize, String),
- Diagnostics(usize),
-}
-
-impl SerializedItem {
- fn kind(&self) -> SerializedItemKind {
- match self {
- SerializedItem::Editor(_, _) => SerializedItemKind::Editor,
- SerializedItem::Terminal(_) => SerializedItemKind::Terminal,
- SerializedItem::ProjectSearch(_, _) => SerializedItemKind::ProjectSearch,
- SerializedItem::Diagnostics(_) => SerializedItemKind::Diagnostics,
- }
- }
-
- fn id(&self) -> usize {
- match self {
- SerializedItem::Editor(id, _)
- | SerializedItem::Terminal(id)
- | SerializedItem::ProjectSearch(id, _)
- | SerializedItem::Diagnostics(id) => *id,
- }
- }
-}
-
-impl Db {
- fn write_item(&self, serialized_item: SerializedItem) -> Result<()> {
- self.real()
- .map(|db| {
- let mut lock = db.connection.lock();
- let tx = lock.transaction()?;
-
- // Serialize the item
- let id = serialized_item.id();
- {
- let mut stmt = tx.prepare_cached(
- "INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))",
- )?;
-
- dbg!("inserting item");
- stmt.execute(params![id, serialized_item.kind().to_string()])?;
- }
-
- // Serialize item data
- match &serialized_item {
- SerializedItem::Editor(_, path) => {
- dbg!("inserting path");
- let mut stmt = tx.prepare_cached(
- "INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
- )?;
-
- let path_bytes = path.as_os_str().as_bytes();
- stmt.execute(params![id, path_bytes])?;
- }
- SerializedItem::ProjectSearch(_, query) => {
- dbg!("inserting query");
- let mut stmt = tx.prepare_cached(
- "INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
- )?;
-
- stmt.execute(params![id, query])?;
- }
- _ => {}
- }
-
- tx.commit()?;
-
- let mut stmt = lock.prepare_cached("SELECT id, kind FROM items")?;
- let _ = stmt
- .query_map([], |row| {
- let zero: usize = row.get(0)?;
- let one: String = row.get(1)?;
-
- dbg!(zero, one);
- Ok(())
- })?
- .collect::<Vec<Result<(), _>>>();
-
- Ok(())
- })
- .unwrap_or(Ok(()))
- }
-
- fn delete_item(&self, item_id: usize) -> Result<()> {
- self.real()
- .map(|db| {
- let lock = db.connection.lock();
-
- let mut stmt = lock.prepare_cached(
- r#"
- DELETE FROM items WHERE id = (:id);
- DELETE FROM item_path WHERE id = (:id);
- DELETE FROM item_query WHERE id = (:id);
- "#,
- )?;
-
- stmt.execute(named_params! {":id": item_id})?;
-
- Ok(())
- })
- .unwrap_or(Ok(()))
- }
-
- fn take_items(&self) -> Result<HashSet<SerializedItem>> {
- self.real()
- .map(|db| {
- let mut lock = db.connection.lock();
-
- let tx = lock.transaction()?;
-
- // When working with transactions in rusqlite, need to make this kind of scope
- // To make the borrow stuff work correctly. Don't know why, rust is wild.
- let result = {
- let mut editors_stmt = tx.prepare_cached(
- r#"
- SELECT items.id, item_path.path
- FROM items
- LEFT JOIN item_path
- ON items.id = item_path.item_id
- WHERE items.kind = ?;
- "#,
- )?;
-
- let editors_iter = editors_stmt.query_map(
- [SerializedItemKind::Editor.to_string()],
- |row| {
- let id: usize = row.get(0)?;
-
- let buf: Vec<u8> = row.get(1)?;
- let path: PathBuf = OsStr::from_bytes(&buf).into();
-
- Ok(SerializedItem::Editor(id, path))
- },
- )?;
-
- let mut terminals_stmt = tx.prepare_cached(
- r#"
- SELECT items.id
- FROM items
- WHERE items.kind = ?;
- "#,
- )?;
- let terminals_iter = terminals_stmt.query_map(
- [SerializedItemKind::Terminal.to_string()],
- |row| {
- let id: usize = row.get(0)?;
-
- Ok(SerializedItem::Terminal(id))
- },
- )?;
-
- let mut search_stmt = tx.prepare_cached(
- r#"
- SELECT items.id, item_query.query
- FROM items
- LEFT JOIN item_query
- ON items.id = item_query.item_id
- WHERE items.kind = ?;
- "#,
- )?;
- let searches_iter = search_stmt.query_map(
- [SerializedItemKind::ProjectSearch.to_string()],
- |row| {
- let id: usize = row.get(0)?;
- let query = row.get(1)?;
-
- Ok(SerializedItem::ProjectSearch(id, query))
- },
- )?;
-
- #[cfg(debug_assertions)]
- let tmp =
- searches_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
- #[cfg(debug_assertions)]
- debug_assert!(tmp.len() == 0 || tmp.len() == 1);
- #[cfg(debug_assertions)]
- let searches_iter = tmp.into_iter();
-
- let mut diagnostic_stmt = tx.prepare_cached(
- r#"
- SELECT items.id
- FROM items
- WHERE items.kind = ?;
- "#,
- )?;
-
- let diagnostics_iter = diagnostic_stmt.query_map(
- [SerializedItemKind::Diagnostics.to_string()],
- |row| {
- let id: usize = row.get(0)?;
-
- Ok(SerializedItem::Diagnostics(id))
- },
- )?;
-
- #[cfg(debug_assertions)]
- let tmp =
- diagnostics_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
- #[cfg(debug_assertions)]
- debug_assert!(tmp.len() == 0 || tmp.len() == 1);
- #[cfg(debug_assertions)]
- let diagnostics_iter = tmp.into_iter();
-
- let res = editors_iter
- .chain(terminals_iter)
- .chain(diagnostics_iter)
- .chain(searches_iter)
- .collect::<Result<HashSet<SerializedItem>, rusqlite::Error>>()?;
-
- let mut delete_stmt = tx.prepare_cached(
- r#"
- DELETE FROM items;
- DELETE FROM item_path;
- DELETE FROM item_query;
- "#,
- )?;
-
- delete_stmt.execute([])?;
-
- res
- };
-
- tx.commit()?;
-
- Ok(result)
- })
- .unwrap_or(Ok(HashSet::default()))
- }
-}
-
-#[cfg(test)]
-mod test {
- use anyhow::Result;
-
- use super::*;
-
- #[test]
- fn test_items_round_trip() -> Result<()> {
- let db = Db::open_in_memory();
-
- let mut items = vec![
- SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
- SerializedItem::Terminal(1),
- SerializedItem::ProjectSearch(2, "Test query!".to_string()),
- SerializedItem::Diagnostics(3),
- ]
- .into_iter()
- .collect::<HashSet<_>>();
-
- for item in items.iter() {
- dbg!("Inserting... ");
- db.write_item(item.clone())?;
- }
-
- assert_eq!(items, db.take_items()?);
-
- // Check that it's empty, as expected
- assert_eq!(HashSet::default(), db.take_items()?);
-
- for item in items.iter() {
- db.write_item(item.clone())?;
- }
-
- items.remove(&SerializedItem::ProjectSearch(2, "Test query!".to_string()));
- db.delete_item(2)?;
-
- assert_eq!(items, db.take_items()?);
-
- Ok(())
- }
-}
@@ -1,82 +1,62 @@
-use anyhow::Result;
-use rusqlite::OptionalExtension;
-
-use super::Db;
-
-pub(crate) const KVP_M_1_UP: &str = "
-CREATE TABLE kv_store(
- key TEXT PRIMARY KEY,
- value TEXT NOT NULL
-) STRICT;
-";
-
-impl Db {
- pub fn read_kvp(&self, key: &str) -> Result<Option<String>> {
- self.real()
- .map(|db| {
- let lock = db.connection.lock();
- let mut stmt = lock.prepare_cached("SELECT value FROM kv_store WHERE key = (?)")?;
-
- Ok(stmt.query_row([key], |row| row.get(0)).optional()?)
- })
- .unwrap_or(Ok(None))
+use sqlez_macros::sql;
+
+use crate::{define_connection, query};
+
+define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
+ &[sql!(
+ CREATE TABLE IF NOT EXISTS kv_store(
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ ) STRICT;
+ )];
+);
+
+impl KeyValueStore {
+ query! {
+ pub fn read_kvp(key: &str) -> Result<Option<String>> {
+ SELECT value FROM kv_store WHERE key = (?)
+ }
}
- pub fn write_kvp(&self, key: &str, value: &str) -> Result<()> {
- self.real()
- .map(|db| {
- let lock = db.connection.lock();
-
- let mut stmt = lock.prepare_cached(
- "INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))",
- )?;
-
- stmt.execute([key, value])?;
-
- Ok(())
- })
- .unwrap_or(Ok(()))
+ query! {
+ pub async fn write_kvp(key: String, value: String) -> Result<()> {
+ INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
+ }
}
- pub fn delete_kvp(&self, key: &str) -> Result<()> {
- self.real()
- .map(|db| {
- let lock = db.connection.lock();
-
- let mut stmt = lock.prepare_cached("DELETE FROM kv_store WHERE key = (?)")?;
-
- stmt.execute([key])?;
-
- Ok(())
- })
- .unwrap_or(Ok(()))
+ query! {
+ pub async fn delete_kvp(key: String) -> Result<()> {
+ DELETE FROM kv_store WHERE key = (?)
+ }
}
}
#[cfg(test)]
mod tests {
- use anyhow::Result;
-
- use super::*;
-
- #[test]
- fn test_kvp() -> Result<()> {
- let db = Db::open_in_memory();
+ use crate::kvp::KeyValueStore;
- assert_eq!(db.read_kvp("key-1")?, None);
+ #[gpui::test]
+ async fn test_kvp() {
+ let db = KeyValueStore(crate::open_test_db("test_kvp").await);
- db.write_kvp("key-1", "one")?;
- assert_eq!(db.read_kvp("key-1")?, Some("one".to_string()));
+ assert_eq!(db.read_kvp("key-1").unwrap(), None);
- db.write_kvp("key-1", "one-2")?;
- assert_eq!(db.read_kvp("key-1")?, Some("one-2".to_string()));
+ db.write_kvp("key-1".to_string(), "one".to_string())
+ .await
+ .unwrap();
+ assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
- db.write_kvp("key-2", "two")?;
- assert_eq!(db.read_kvp("key-2")?, Some("two".to_string()));
+ db.write_kvp("key-1".to_string(), "one-2".to_string())
+ .await
+ .unwrap();
+ assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
- db.delete_kvp("key-1")?;
- assert_eq!(db.read_kvp("key-1")?, None);
+ db.write_kvp("key-2".to_string(), "two".to_string())
+ .await
+ .unwrap();
+ assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
- Ok(())
+ db.delete_kvp("key-1".to_string()).await.unwrap();
+ assert_eq!(db.read_kvp("key-1").unwrap(), None);
}
}
@@ -1,15 +0,0 @@
-use rusqlite_migration::{Migrations, M};
-
-// use crate::items::ITEMS_M_1;
-use crate::kvp::KVP_M_1_UP;
-
-// This must be ordered by development time! Only ever add new migrations to the end!!
-// Bad things will probably happen if you don't monotonically edit this vec!!!!
-// And no re-ordering ever!!!!!!!!!! The results of these migrations are on the user's
-// file system and so everything we do here is locked in _f_o_r_e_v_e_r_.
-lazy_static::lazy_static! {
- pub static ref MIGRATIONS: Migrations<'static> = Migrations::new(vec![
- M::up(KVP_M_1_UP),
- // M::up(ITEMS_M_1),
- ]);
-}
@@ -0,0 +1,314 @@
+#[macro_export]
+macro_rules! query {
+ ($vis:vis fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
+ $vis fn $id(&self) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.exec(sql_stmt)?().context(::std::format!(
+ "Error in {}, exec failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt,
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.exec(sql_stmt)?().context(::std::format!(
+ "Error in {}, exec failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ self.write(move |connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.exec_bound::<$arg_type>(sql_stmt)?($arg)
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ self.write(move |connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select::<$return_type>(sql_stmt)?(())
+ .context(::std::format!(
+ "Error in {}, select_row failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+ pub async fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select::<$return_type>(sql_stmt)?(())
+ .context(::std::format!(
+ "Error in {}, select_row failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row::<$return_type>(sql_stmt)?()
+ .context(::std::format!(
+ "Error in {}, select_row failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_row::<$return_type>(sql_stmt)?()
+ .context(::std::format!(
+ "Error in {}, select_row failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+
+ }
+ };
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+
+ }
+ };
+ ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_row_bound::<($($arg_type),+), $return_type>(indoc! { $sql })?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ $vis fn $id(&self) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row::<$return_type>(indoc! { $sql })?()
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_row::<$return_type>(sql_stmt)?()
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ pub fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+}
@@ -29,7 +29,10 @@ use std::{
sync::Arc,
};
use util::TryFutureExt;
-use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
+use workspace::{
+ item::{Item, ItemEvent, ItemHandle},
+ ItemNavHistory, Pane, Workspace,
+};
actions!(diagnostics, [Deploy]);
@@ -503,7 +506,7 @@ impl ProjectDiagnosticsEditor {
}
}
-impl workspace::Item for ProjectDiagnosticsEditor {
+impl Item for ProjectDiagnosticsEditor {
fn tab_content(
&self,
_detail: Option<usize>,
@@ -571,7 +574,7 @@ impl workspace::Item for ProjectDiagnosticsEditor {
unreachable!()
}
- fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
+ fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
Editor::to_item_events(event)
}
@@ -581,7 +584,11 @@ impl workspace::Item for ProjectDiagnosticsEditor {
});
}
- fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
+ fn clone_on_split(
+ &self,
+ _workspace_id: workspace::WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Self>
where
Self: Sized,
{
@@ -610,6 +617,20 @@ impl workspace::Item for ProjectDiagnosticsEditor {
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
}
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("diagnostics")
+ }
+
+ fn deserialize(
+ project: ModelHandle<Project>,
+ workspace: WeakViewHandle<Workspace>,
+ _workspace_id: workspace::WorkspaceId,
+ _item_id: workspace::ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<Result<ViewHandle<Self>>> {
+ Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
+ }
}
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
@@ -781,8 +802,15 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
// Create some diagnostics
project.update(cx, |project, cx| {
@@ -7,7 +7,7 @@ use gpui::{
use language::Diagnostic;
use project::Project;
use settings::Settings;
-use workspace::StatusItemView;
+use workspace::{item::ItemHandle, StatusItemView};
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
@@ -219,7 +219,7 @@ impl View for DiagnosticIndicator {
impl StatusItemView for DiagnosticIndicator {
fn set_active_pane_item(
&mut self,
- active_pane_item: Option<&dyn workspace::ItemHandle>,
+ active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
@@ -23,6 +23,7 @@ test-support = [
drag_and_drop = { path = "../drag_and_drop" }
text = { path = "../text" }
clock = { path = "../clock" }
+db = { path = "../db" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
@@ -37,6 +38,7 @@ snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" }
theme = { path = "../theme" }
util = { path = "../util" }
+sqlez = { path = "../sqlez" }
workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"
@@ -9,6 +9,7 @@ mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod multi_buffer;
+mod persistence;
pub mod selections_collection;
#[cfg(test)]
@@ -80,7 +81,7 @@ use std::{
pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{ItemNavHistory, Workspace};
+use workspace::{ItemNavHistory, Workspace, WorkspaceId};
use crate::git::diff_hunk_to_display;
@@ -372,6 +373,7 @@ pub fn init(cx: &mut MutableAppContext) {
workspace::register_project_item::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
+ workspace::register_deserializable_item::<Editor>(cx);
}
trait InvalidationRegion {
@@ -582,6 +584,7 @@ pub struct Editor {
pending_rename: Option<RenameState>,
searchable: bool,
cursor_shape: CursorShape,
+ workspace_id: Option<WorkspaceId>,
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
input_enabled: bool,
leader_replica_id: Option<u16>,
@@ -1235,6 +1238,7 @@ impl Editor {
searchable: true,
override_text_style: None,
cursor_shape: Default::default(),
+ workspace_id: None,
keymap_context_layers: Default::default(),
input_enabled: true,
leader_replica_id: None,
@@ -22,7 +22,10 @@ use util::{
assert_set_eq,
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
};
-use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
+use workspace::{
+ item::{FollowableItem, ItemHandle},
+ NavigationEntry, Pane,
+};
#[gpui::test]
fn test_edit_events(cx: &mut MutableAppContext) {
@@ -475,7 +478,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
cx.set_global(DragAndDrop::<Workspace>::default());
- use workspace::Item;
+ use workspace::item::Item;
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
@@ -1,13 +1,8 @@
-use crate::{
- display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
- movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
- MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
-};
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
use futures::FutureExt;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
- RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
+ RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
@@ -22,11 +17,17 @@ use std::{
path::{Path, PathBuf},
};
use text::Selection;
-use util::TryFutureExt;
+use util::{ResultExt, TryFutureExt};
use workspace::{
+ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
- FollowableItem, Item, ItemEvent, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView,
- ToolbarItemLocation,
+ ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId,
+};
+
+use crate::{
+ display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
+ movement::surrounding_word, persistence::DB, Anchor, Autoscroll, Editor, Event, ExcerptId,
+ MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
};
pub const MAX_TAB_TITLE_LEN: usize = 24;
@@ -367,7 +368,7 @@ impl Item for Editor {
self.buffer.read(cx).is_singleton()
}
- fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
+ fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
@@ -490,7 +491,7 @@ impl Item for Editor {
Task::ready(Ok(()))
}
- fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
+ fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
let mut result = Vec::new();
match event {
Event::Closed => result.push(ItemEvent::CloseItem),
@@ -552,6 +553,87 @@ impl Item for Editor {
}));
Some(breadcrumbs)
}
+
+ fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+ let workspace_id = workspace.database_id();
+ let item_id = cx.view_id();
+
+ fn serialize(
+ buffer: ModelHandle<Buffer>,
+ workspace_id: WorkspaceId,
+ item_id: ItemId,
+ cx: &mut MutableAppContext,
+ ) {
+ if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
+ let path = file.abs_path(cx);
+
+ cx.background()
+ .spawn(async move {
+ DB.save_path(item_id, workspace_id, path.clone())
+ .await
+ .log_err()
+ })
+ .detach();
+ }
+ }
+
+ if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+ serialize(buffer.clone(), workspace_id, item_id, cx);
+
+ cx.subscribe(&buffer, |this, buffer, event, cx| {
+ if let Some(workspace_id) = this.workspace_id {
+ if let language::Event::FileHandleChanged = event {
+ serialize(buffer, workspace_id, cx.view_id(), cx);
+ }
+ }
+ })
+ .detach();
+ }
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("Editor")
+ }
+
+ fn deserialize(
+ project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ workspace_id: workspace::WorkspaceId,
+ item_id: ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<Result<ViewHandle<Self>>> {
+ let project_item: Result<_> = project.update(cx, |project, cx| {
+ // Look up the path with this key associated, create a self with that path
+ let path = DB
+ .get_path(item_id, workspace_id)?
+ .context("No path stored for this editor")?;
+
+ let (worktree, path) = project
+ .find_local_worktree(&path, cx)
+ .with_context(|| format!("No worktree for path: {path:?}"))?;
+ let project_path = ProjectPath {
+ worktree_id: worktree.read(cx).id(),
+ path: path.into(),
+ };
+
+ Ok(project.open_path(project_path, cx))
+ });
+
+ project_item
+ .map(|project_item| {
+ cx.spawn(|pane, mut cx| async move {
+ let (_, project_item) = project_item.await?;
+ let buffer = project_item
+ .downcast::<Buffer>()
+ .context("Project item at stored path was not a buffer")?;
+
+ Ok(cx.update(|cx| {
+ cx.add_view(pane, |cx| Editor::for_buffer(buffer, Some(project), cx))
+ }))
+ })
+ })
+ .unwrap_or_else(|error| Task::ready(Err(error)))
+ }
}
impl ProjectItem for Editor {
@@ -0,0 +1,36 @@
+use std::path::PathBuf;
+
+use db::sqlez_macros::sql;
+use db::{define_connection, query};
+use workspace::{ItemId, WorkspaceDb, WorkspaceId};
+
+define_connection!(
+ pub static ref DB: EditorDb<WorkspaceDb> =
+ &[sql! (
+ CREATE TABLE editors(
+ item_id INTEGER NOT NULL,
+ workspace_id INTEGER NOT NULL,
+ path BLOB NOT NULL,
+ PRIMARY KEY(item_id, workspace_id),
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+ ) STRICT;
+ )];
+);
+
+impl EditorDb {
+ query! {
+ pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
+ SELECT path FROM editors
+ WHERE item_id = ? AND workspace_id = ?
+ }
+ }
+
+ query! {
+ pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
+ INSERT OR REPLACE INTO editors(item_id, workspace_id, path)
+ VALUES (?, ?, ?)
+ }
+ }
+}
@@ -63,8 +63,15 @@ impl<'a> EditorLspTestContext<'a> {
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
- let (window_id, workspace) =
- cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
+ let (window_id, workspace) = cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
@@ -316,8 +316,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (window_id, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (window_id, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
@@ -371,8 +372,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -446,8 +448,9 @@ mod tests {
cx,
)
.await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
finder
@@ -471,8 +474,9 @@ mod tests {
cx,
)
.await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -524,8 +528,9 @@ mod tests {
cx,
)
.await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -563,8 +568,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
finder
@@ -17,6 +17,7 @@ collections = { path = "../collections" }
gpui_macros = { path = "../gpui_macros" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
+sqlez = { path = "../sqlez" }
async-task = "4.0.3"
backtrace = { version = "0.3", optional = true }
ctor = "0.1"
@@ -1,10 +1,10 @@
+#include "nan.h"
#include "tree_sitter/parser.h"
#include <node.h>
-#include "nan.h"
using namespace v8;
-extern "C" TSLanguage * tree_sitter_context_predicate();
+extern "C" TSLanguage *tree_sitter_context_predicate();
namespace {
@@ -16,13 +16,15 @@ void Init(Local<Object> exports, Local<Object> module) {
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
- Local<Object> instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
+ Local<Object> instance =
+ constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
Nan::SetInternalFieldPointer(instance, 0, tree_sitter_context_predicate());
- Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("context_predicate").ToLocalChecked());
+ Nan::Set(instance, Nan::New("name").ToLocalChecked(),
+ Nan::New("context_predicate").ToLocalChecked());
Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
}
NODE_MODULE(tree_sitter_context_predicate_binding, Init)
-} // namespace
+} // namespace
@@ -17,10 +17,15 @@ use crate::{
SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
WeakViewHandle,
};
+use anyhow::bail;
use collections::{HashMap, HashSet};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
use smallvec::SmallVec;
+use sqlez::{
+ bindable::{Bind, Column},
+ statement::Statement,
+};
use std::{
marker::PhantomData,
ops::{Deref, DerefMut, Range},
@@ -863,8 +868,9 @@ pub struct DebugContext<'a> {
pub app: &'a AppContext,
}
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Axis {
+ #[default]
Horizontal,
Vertical,
}
@@ -894,6 +900,31 @@ impl ToJson for Axis {
}
}
+impl Bind for Axis {
+ fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
+ match self {
+ Axis::Horizontal => "Horizontal",
+ Axis::Vertical => "Vertical",
+ }
+ .bind(statement, start_index)
+ }
+}
+
+impl Column for Axis {
+ fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
+ String::column(statement, start_index).and_then(|(axis_text, next_index)| {
+ Ok((
+ match axis_text.as_str() {
+ "Horizontal" => Axis::Horizontal,
+ "Vertical" => Axis::Vertical,
+ _ => bail!("Stored serialized item kind is incorrect"),
+ },
+ next_index,
+ ))
+ })
+ }
+}
+
pub trait Vector2FExt {
fn along(self, axis: Axis) -> f32;
}
@@ -12,3 +12,4 @@ doctest = false
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"
+
@@ -115,7 +115,7 @@ mod tests {
#[test]
fn test_heading_entry_defaults_to_hour_12() {
- let naive_time = NaiveTime::from_hms_milli(15, 0, 0, 0);
+ let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
let actual_heading_entry = heading_entry(naive_time, &None);
let expected_heading_entry = "# 3:00 PM";
@@ -124,7 +124,7 @@ mod tests {
#[test]
fn test_heading_entry_is_hour_12() {
- let naive_time = NaiveTime::from_hms_milli(15, 0, 0, 0);
+ let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour12));
let expected_heading_entry = "# 3:00 PM";
@@ -133,7 +133,7 @@ mod tests {
#[test]
fn test_heading_entry_is_hour_24() {
- let naive_time = NaiveTime::from_hms_milli(15, 0, 0, 0);
+ let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour24));
let expected_heading_entry = "# 15:00";
@@ -65,7 +65,6 @@ use std::{
use thiserror::Error;
use util::{defer, post_inc, ResultExt, TryFutureExt as _};
-pub use db::Db;
pub use fs::*;
pub use worktree::*;
@@ -758,6 +757,7 @@ impl Project {
&self.collaborators
}
+ /// Collect all worktrees, including ones that don't appear in the project panel
pub fn worktrees<'a>(
&'a self,
cx: &'a AppContext,
@@ -767,6 +767,7 @@ impl Project {
.filter_map(move |worktree| worktree.upgrade(cx))
}
+ /// Collect all user-visible worktrees, the ones that appear in the project panel
pub fn visible_worktrees<'a>(
&'a self,
cx: &'a AppContext,
@@ -1393,8 +1393,15 @@ mod tests {
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
@@ -1486,8 +1493,15 @@ mod tests {
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
select_path(&panel, "root1", cx);
@@ -14,8 +14,9 @@ use serde::Deserialize;
use settings::Settings;
use std::{any::Any, sync::Arc};
use workspace::{
+ item::ItemHandle,
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
- ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView,
+ Pane, ToolbarItemLocation, ToolbarItemView,
};
#[derive(Clone, Deserialize, PartialEq)]
@@ -24,9 +24,9 @@ use std::{
};
use util::ResultExt as _;
use workspace::{
+ item::{Item, ItemEvent, ItemHandle},
searchable::{Direction, SearchableItem, SearchableItemHandle},
- Item, ItemEvent, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView,
- Workspace,
+ ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
};
actions!(project_search, [SearchInNew, ToggleFocus]);
@@ -315,7 +315,7 @@ impl Item for ProjectSearchView {
.update(cx, |editor, cx| editor.reload(project, cx))
}
- fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
+ fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
@@ -353,6 +353,20 @@ impl Item for ProjectSearchView {
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
self.results_editor.breadcrumbs(theme, cx)
}
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ None
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: workspace::WorkspaceId,
+ _item_id: workspace::ItemId,
+ _cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ unimplemented!()
+ }
}
impl ProjectSearchView {
@@ -893,7 +907,7 @@ impl View for ProjectSearchBar {
impl ToolbarItemView for ProjectSearchBar {
fn set_active_pane_item(
&mut self,
- active_pane_item: Option<&dyn workspace::ItemHandle>,
+ active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
@@ -14,6 +14,7 @@ test-support = []
assets = { path = "../assets" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
+sqlez = { path = "../sqlez" }
fs = { path = "../fs" }
anyhow = "1.0.38"
futures = "0.3"
@@ -2,7 +2,7 @@ mod keymap_file;
pub mod settings_file;
pub mod watched_json;
-use anyhow::Result;
+use anyhow::{bail, Result};
use gpui::{
font_cache::{FamilyId, FontCache},
AssetSource,
@@ -14,6 +14,10 @@ use schemars::{
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
+use sqlez::{
+ bindable::{Bind, Column},
+ statement::Statement,
+};
use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
use theme::{Theme, ThemeRegistry};
use tree_sitter::Query;
@@ -55,24 +59,6 @@ pub struct FeatureFlags {
pub experimental_themes: bool,
}
-#[derive(Copy, Clone, PartialEq, Eq, Default)]
-pub enum ReleaseChannel {
- #[default]
- Dev,
- Preview,
- Stable,
-}
-
-impl ReleaseChannel {
- pub fn name(&self) -> &'static str {
- match self {
- ReleaseChannel::Dev => "Zed Dev",
- ReleaseChannel::Preview => "Zed Preview",
- ReleaseChannel::Stable => "Zed",
- }
- }
-}
-
impl FeatureFlags {
pub fn keymap_files(&self) -> Vec<&'static str> {
vec![]
@@ -244,6 +230,33 @@ pub enum DockAnchor {
Expanded,
}
+impl Bind for DockAnchor {
+ fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
+ match self {
+ DockAnchor::Bottom => "Bottom",
+ DockAnchor::Right => "Right",
+ DockAnchor::Expanded => "Expanded",
+ }
+ .bind(statement, start_index)
+ }
+}
+
+impl Column for DockAnchor {
+ fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
+ String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
+ Ok((
+ match anchor_text.as_ref() {
+ "Bottom" => DockAnchor::Bottom,
+ "Right" => DockAnchor::Right,
+ "Expanded" => DockAnchor::Expanded,
+ _ => bail!("Stored dock anchor is incorrect"),
+ },
+ next_index,
+ ))
+ })
+ }
+}
+
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct SettingsFileContent {
pub experiments: Option<FeatureFlags>,
@@ -0,0 +1,2 @@
+debug/
+target/
@@ -0,0 +1,150 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "anyhow"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
+dependencies = [
+ "backtrace",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "gimli"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+
+[[package]]
+name = "indoc"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
+
+[[package]]
+name = "libc"
+version = "0.2.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "object"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+
+[[package]]
+name = "sqlez"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "indoc",
+ "libsqlite3-sys",
+ "thread_local",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
@@ -0,0 +1,16 @@
+[package]
+name = "sqlez"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = { version = "1.0.38", features = ["backtrace"] }
+indoc = "1.0.7"
+libsqlite3-sys = { version = "0.24", features = ["bundled"] }
+smol = "1.2"
+thread_local = "1.1.4"
+lazy_static = "1.4"
+parking_lot = "0.11.1"
+futures = "0.3"
@@ -0,0 +1,352 @@
+use std::{
+ ffi::OsStr,
+ os::unix::prelude::OsStrExt,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+use anyhow::{Context, Result};
+
+use crate::statement::{SqlType, Statement};
+
+pub trait Bind {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32>;
+}
+
+pub trait Column: Sized {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)>;
+}
+
+impl Bind for bool {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement
+ .bind(self.then_some(1).unwrap_or(0), start_index)
+ .with_context(|| format!("Failed to bind bool at index {start_index}"))
+ }
+}
+
+impl Column for bool {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ i32::column(statement, start_index)
+ .map(|(i, next_index)| (i != 0, next_index))
+ .with_context(|| format!("Failed to read bool at index {start_index}"))
+ }
+}
+
+impl Bind for &[u8] {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement
+ .bind_blob(start_index, self)
+ .with_context(|| format!("Failed to bind &[u8] at index {start_index}"))?;
+ Ok(start_index + 1)
+ }
+}
+
+impl<const C: usize> Bind for &[u8; C] {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement
+ .bind_blob(start_index, self.as_slice())
+ .with_context(|| format!("Failed to bind &[u8; C] at index {start_index}"))?;
+ Ok(start_index + 1)
+ }
+}
+
+impl Bind for Vec<u8> {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement
+ .bind_blob(start_index, self)
+ .with_context(|| format!("Failed to bind Vec<u8> at index {start_index}"))?;
+ Ok(start_index + 1)
+ }
+}
+
+impl Column for Vec<u8> {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let result = statement
+ .column_blob(start_index)
+ .with_context(|| format!("Failed to read Vec<u8> at index {start_index}"))?;
+
+ Ok((Vec::from(result), start_index + 1))
+ }
+}
+
+impl Bind for f64 {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement
+ .bind_double(start_index, *self)
+ .with_context(|| format!("Failed to bind f64 at index {start_index}"))?;
+ Ok(start_index + 1)
+ }
+}
+
+impl Column for f64 {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let result = statement
+ .column_double(start_index)
+ .with_context(|| format!("Failed to parse f64 at index {start_index}"))?;
+
+ Ok((result, start_index + 1))
+ }
+}
+
+impl Bind for i32 {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement
+ .bind_int(start_index, *self)
+ .with_context(|| format!("Failed to bind i32 at index {start_index}"))?;
+
+ Ok(start_index + 1)
+ }
+}
+
+impl Column for i32 {
+ fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let result = statement.column_int(start_index)?;
+ Ok((result, start_index + 1))
+ }
+}
+
+impl Bind for i64 {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement
+ .bind_int64(start_index, *self)
+ .with_context(|| format!("Failed to bind i64 at index {start_index}"))?;
+ Ok(start_index + 1)
+ }
+}
+
+impl Column for i64 {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let result = statement.column_int64(start_index)?;
+ Ok((result, start_index + 1))
+ }
+}
+
+impl Bind for usize {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ (*self as i64)
+ .bind(statement, start_index)
+ .with_context(|| format!("Failed to bind usize at index {start_index}"))
+ }
+}
+
+impl Column for usize {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let result = statement.column_int64(start_index)?;
+ Ok((result as usize, start_index + 1))
+ }
+}
+
+impl Bind for &str {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement.bind_text(start_index, self)?;
+ Ok(start_index + 1)
+ }
+}
+
+impl Bind for Arc<str> {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement.bind_text(start_index, self.as_ref())?;
+ Ok(start_index + 1)
+ }
+}
+
+impl Bind for String {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ statement.bind_text(start_index, self)?;
+ Ok(start_index + 1)
+ }
+}
+
+impl Column for Arc<str> {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let result = statement.column_text(start_index)?;
+ Ok((Arc::from(result), start_index + 1))
+ }
+}
+
+impl Column for String {
+ fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let result = statement.column_text(start_index)?;
+ Ok((result.to_owned(), start_index + 1))
+ }
+}
+
+impl<T: Bind> Bind for Option<T> {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ if let Some(this) = self {
+ this.bind(statement, start_index)
+ } else {
+ statement.bind_null(start_index)?;
+ Ok(start_index + 1)
+ }
+ }
+}
+
+impl<T: Column> Column for Option<T> {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ if let SqlType::Null = statement.column_type(start_index)? {
+ Ok((None, start_index + 1))
+ } else {
+ T::column(statement, start_index).map(|(result, next_index)| (Some(result), next_index))
+ }
+ }
+}
+
+impl<T: Bind, const COUNT: usize> Bind for [T; COUNT] {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let mut current_index = start_index;
+ for binding in self {
+ current_index = binding.bind(statement, current_index)?
+ }
+
+ Ok(current_index)
+ }
+}
+
+impl<T: Column + Default + Copy, const COUNT: usize> Column for [T; COUNT] {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let mut array = [Default::default(); COUNT];
+ let mut current_index = start_index;
+ for i in 0..COUNT {
+ (array[i], current_index) = T::column(statement, current_index)?;
+ }
+ Ok((array, current_index))
+ }
+}
+
+impl<T: Bind> Bind for Vec<T> {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let mut current_index = start_index;
+ for binding in self.iter() {
+ current_index = binding.bind(statement, current_index)?
+ }
+
+ Ok(current_index)
+ }
+}
+
+impl<T: Bind> Bind for &[T] {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let mut current_index = start_index;
+ for binding in *self {
+ current_index = binding.bind(statement, current_index)?
+ }
+
+ Ok(current_index)
+ }
+}
+
+impl Bind for &Path {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ self.as_os_str().as_bytes().bind(statement, start_index)
+ }
+}
+
+impl Bind for Arc<Path> {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ self.as_ref().bind(statement, start_index)
+ }
+}
+
+impl Bind for PathBuf {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ (self.as_ref() as &Path).bind(statement, start_index)
+ }
+}
+
+impl Column for PathBuf {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let blob = statement.column_blob(start_index)?;
+
+ Ok((
+ PathBuf::from(OsStr::from_bytes(blob).to_owned()),
+ start_index + 1,
+ ))
+ }
+}
+
+/// Unit impls do nothing. This simplifies query macros
+impl Bind for () {
+ fn bind(&self, _statement: &Statement, start_index: i32) -> Result<i32> {
+ Ok(start_index)
+ }
+}
+
+impl Column for () {
+ fn column(_statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ Ok(((), start_index))
+ }
+}
+
+impl<T1: Bind, T2: Bind> Bind for (T1, T2) {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = self.0.bind(statement, start_index)?;
+ self.1.bind(statement, next_index)
+ }
+}
+
+impl<T1: Column, T2: Column> Column for (T1, T2) {
+ fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (first, next_index) = T1::column(statement, start_index)?;
+ let (second, next_index) = T2::column(statement, next_index)?;
+ Ok(((first, second), next_index))
+ }
+}
+
+impl<T1: Bind, T2: Bind, T3: Bind> Bind for (T1, T2, T3) {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = self.0.bind(statement, start_index)?;
+ let next_index = self.1.bind(statement, next_index)?;
+ self.2.bind(statement, next_index)
+ }
+}
+
+impl<T1: Column, T2: Column, T3: Column> Column for (T1, T2, T3) {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (first, next_index) = T1::column(statement, start_index)?;
+ let (second, next_index) = T2::column(statement, next_index)?;
+ let (third, next_index) = T3::column(statement, next_index)?;
+ Ok(((first, second, third), next_index))
+ }
+}
+
+impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind> Bind for (T1, T2, T3, T4) {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = self.0.bind(statement, start_index)?;
+ let next_index = self.1.bind(statement, next_index)?;
+ let next_index = self.2.bind(statement, next_index)?;
+ self.3.bind(statement, next_index)
+ }
+}
+
+impl<T1: Column, T2: Column, T3: Column, T4: Column> Column for (T1, T2, T3, T4) {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (first, next_index) = T1::column(statement, start_index)?;
+ let (second, next_index) = T2::column(statement, next_index)?;
+ let (third, next_index) = T3::column(statement, next_index)?;
+ let (fourth, next_index) = T4::column(statement, next_index)?;
+ Ok(((first, second, third, fourth), next_index))
+ }
+}
+
+impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind, T5: Bind> Bind for (T1, T2, T3, T4, T5) {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = self.0.bind(statement, start_index)?;
+ let next_index = self.1.bind(statement, next_index)?;
+ let next_index = self.2.bind(statement, next_index)?;
+ let next_index = self.3.bind(statement, next_index)?;
+ self.4.bind(statement, next_index)
+ }
+}
+
+impl<T1: Column, T2: Column, T3: Column, T4: Column, T5: Column> Column for (T1, T2, T3, T4, T5) {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (first, next_index) = T1::column(statement, start_index)?;
+ let (second, next_index) = T2::column(statement, next_index)?;
+ let (third, next_index) = T3::column(statement, next_index)?;
+ let (fourth, next_index) = T4::column(statement, next_index)?;
+ let (fifth, next_index) = T5::column(statement, next_index)?;
+ Ok(((first, second, third, fourth, fifth), next_index))
+ }
+}
@@ -0,0 +1,334 @@
+use std::{
+ cell::RefCell,
+ ffi::{CStr, CString},
+ marker::PhantomData,
+ path::Path,
+ ptr,
+};
+
+use anyhow::{anyhow, Result};
+use libsqlite3_sys::*;
+
+pub struct Connection {
+ pub(crate) sqlite3: *mut sqlite3,
+ persistent: bool,
+ pub(crate) write: RefCell<bool>,
+ _sqlite: PhantomData<sqlite3>,
+}
+unsafe impl Send for Connection {}
+
+impl Connection {
+ pub(crate) fn open(uri: &str, persistent: bool) -> Result<Self> {
+ let mut connection = Self {
+ sqlite3: 0 as *mut _,
+ persistent,
+ write: RefCell::new(true),
+ _sqlite: PhantomData,
+ };
+
+ let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE;
+ unsafe {
+ sqlite3_open_v2(
+ CString::new(uri)?.as_ptr(),
+ &mut connection.sqlite3,
+ flags,
+ 0 as *const _,
+ );
+
+ // Turn on extended error codes
+ sqlite3_extended_result_codes(connection.sqlite3, 1);
+
+ connection.last_error()?;
+ }
+
+ Ok(connection)
+ }
+
+ /// Attempts to open the database at uri. If it fails, a shared memory db will be opened
+ /// instead.
+ pub fn open_file(uri: &str) -> Self {
+ Self::open(uri, true).unwrap_or_else(|_| Self::open_memory(Some(uri)))
+ }
+
+ pub fn open_memory(uri: Option<&str>) -> Self {
+ let in_memory_path = if let Some(uri) = uri {
+ format!("file:{}?mode=memory&cache=shared", uri)
+ } else {
+ ":memory:".to_string()
+ };
+
+ Self::open(&in_memory_path, false).expect("Could not create fallback in memory db")
+ }
+
+ pub fn persistent(&self) -> bool {
+ self.persistent
+ }
+
+ pub fn can_write(&self) -> bool {
+ *self.write.borrow()
+ }
+
+ pub fn backup_main(&self, destination: &Connection) -> Result<()> {
+ unsafe {
+ let backup = sqlite3_backup_init(
+ destination.sqlite3,
+ CString::new("main")?.as_ptr(),
+ self.sqlite3,
+ CString::new("main")?.as_ptr(),
+ );
+ sqlite3_backup_step(backup, -1);
+ sqlite3_backup_finish(backup);
+ destination.last_error()
+ }
+ }
+
+ pub fn backup_main_to(&self, destination: impl AsRef<Path>) -> Result<()> {
+ let destination = Self::open_file(destination.as_ref().to_string_lossy().as_ref());
+ self.backup_main(&destination)
+ }
+
+ pub fn sql_has_syntax_error(&self, sql: &str) -> Option<(String, usize)> {
+ let sql = CString::new(sql).unwrap();
+ let mut remaining_sql = sql.as_c_str();
+ let sql_start = remaining_sql.as_ptr();
+
+ unsafe {
+ while {
+ let remaining_sql_str = remaining_sql.to_str().unwrap().trim();
+ remaining_sql_str != ";" && !remaining_sql_str.is_empty()
+ } {
+ let mut raw_statement = 0 as *mut sqlite3_stmt;
+ let mut remaining_sql_ptr = ptr::null();
+ sqlite3_prepare_v2(
+ self.sqlite3,
+ remaining_sql.as_ptr(),
+ -1,
+ &mut raw_statement,
+ &mut remaining_sql_ptr,
+ );
+
+ let res = sqlite3_errcode(self.sqlite3);
+ let offset = sqlite3_error_offset(self.sqlite3);
+ let message = sqlite3_errmsg(self.sqlite3);
+
+ sqlite3_finalize(raw_statement);
+
+ if res == 1 && offset >= 0 {
+ let err_msg =
+ String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes())
+ .into_owned();
+ let sub_statement_correction =
+ remaining_sql.as_ptr() as usize - sql_start as usize;
+
+ return Some((err_msg, offset as usize + sub_statement_correction));
+ }
+ remaining_sql = CStr::from_ptr(remaining_sql_ptr);
+ }
+ }
+ None
+ }
+
+ pub(crate) fn last_error(&self) -> Result<()> {
+ unsafe {
+ let code = sqlite3_errcode(self.sqlite3);
+ const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW];
+ if NON_ERROR_CODES.contains(&code) {
+ return Ok(());
+ }
+
+ let message = sqlite3_errmsg(self.sqlite3);
+ let message = if message.is_null() {
+ None
+ } else {
+ Some(
+ String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes())
+ .into_owned(),
+ )
+ };
+
+ Err(anyhow!(
+ "Sqlite call failed with code {} and message: {:?}",
+ code as isize,
+ message
+ ))
+ }
+ }
+
+ pub(crate) fn with_write<T>(&self, callback: impl FnOnce(&Connection) -> T) -> T {
+ *self.write.borrow_mut() = true;
+ let result = callback(self);
+ *self.write.borrow_mut() = false;
+ result
+ }
+}
+
+impl Drop for Connection {
+ fn drop(&mut self) {
+ unsafe { sqlite3_close(self.sqlite3) };
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use anyhow::Result;
+ use indoc::indoc;
+
+ use crate::connection::Connection;
+
+ #[test]
+ fn string_round_trips() -> Result<()> {
+ let connection = Connection::open_memory(Some("string_round_trips"));
+ connection
+ .exec(indoc! {"
+ CREATE TABLE text (
+ text TEXT
+ );"})
+ .unwrap()()
+ .unwrap();
+
+ let text = "Some test text";
+
+ connection
+ .exec_bound("INSERT INTO text (text) VALUES (?);")
+ .unwrap()(text)
+ .unwrap();
+
+ assert_eq!(
+ connection.select_row("SELECT text FROM text;").unwrap()().unwrap(),
+ Some(text.to_string())
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn tuple_round_trips() {
+ let connection = Connection::open_memory(Some("tuple_round_trips"));
+ connection
+ .exec(indoc! {"
+ CREATE TABLE test (
+ text TEXT,
+ integer INTEGER,
+ blob BLOB
+ );"})
+ .unwrap()()
+ .unwrap();
+
+ let tuple1 = ("test".to_string(), 64, vec![0, 1, 2, 4, 8, 16, 32, 64]);
+ let tuple2 = ("test2".to_string(), 32, vec![64, 32, 16, 8, 4, 2, 1, 0]);
+
+ let mut insert = connection
+ .exec_bound::<(String, usize, Vec<u8>)>(
+ "INSERT INTO test (text, integer, blob) VALUES (?, ?, ?)",
+ )
+ .unwrap();
+
+ insert(tuple1.clone()).unwrap();
+ insert(tuple2.clone()).unwrap();
+
+ assert_eq!(
+ connection
+ .select::<(String, usize, Vec<u8>)>("SELECT * FROM test")
+ .unwrap()()
+ .unwrap(),
+ vec![tuple1, tuple2]
+ );
+ }
+
+ #[test]
+ fn bool_round_trips() {
+ let connection = Connection::open_memory(Some("bool_round_trips"));
+ connection
+ .exec(indoc! {"
+ CREATE TABLE bools (
+ t INTEGER,
+ f INTEGER
+ );"})
+ .unwrap()()
+ .unwrap();
+
+ connection
+ .exec_bound("INSERT INTO bools(t, f) VALUES (?, ?)")
+ .unwrap()((true, false))
+ .unwrap();
+
+ assert_eq!(
+ connection
+ .select_row::<(bool, bool)>("SELECT * FROM bools;")
+ .unwrap()()
+ .unwrap(),
+ Some((true, false))
+ );
+ }
+
+ #[test]
+ fn backup_works() {
+ let connection1 = Connection::open_memory(Some("backup_works"));
+ connection1
+ .exec(indoc! {"
+ CREATE TABLE blobs (
+ data BLOB
+ );"})
+ .unwrap()()
+ .unwrap();
+ let blob = vec![0, 1, 2, 4, 8, 16, 32, 64];
+ connection1
+ .exec_bound::<Vec<u8>>("INSERT INTO blobs (data) VALUES (?);")
+ .unwrap()(blob.clone())
+ .unwrap();
+
+ // Backup connection1 to connection2
+ let connection2 = Connection::open_memory(Some("backup_works_other"));
+ connection1.backup_main(&connection2).unwrap();
+
+ // Delete the added blob and verify its deleted on the other side
+ let read_blobs = connection1
+ .select::<Vec<u8>>("SELECT * FROM blobs;")
+ .unwrap()()
+ .unwrap();
+ assert_eq!(read_blobs, vec![blob]);
+ }
+
+ #[test]
+ fn multi_step_statement_works() {
+ let connection = Connection::open_memory(Some("multi_step_statement_works"));
+
+ connection
+ .exec(indoc! {"
+ CREATE TABLE test (
+ col INTEGER
+ )"})
+ .unwrap()()
+ .unwrap();
+
+ connection
+ .exec(indoc! {"
+ INSERT INTO test(col) VALUES (2)"})
+ .unwrap()()
+ .unwrap();
+
+ assert_eq!(
+ connection
+ .select_row::<usize>("SELECT * FROM test")
+ .unwrap()()
+ .unwrap(),
+ Some(2)
+ );
+ }
+
+ #[test]
+ fn test_sql_has_syntax_errors() {
+ let connection = Connection::open_memory(Some("test_sql_has_syntax_errors"));
+ let first_stmt =
+ "CREATE TABLE kv_store(key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT ;";
+ let second_stmt = "SELECT FROM";
+
+ let second_offset = connection.sql_has_syntax_error(second_stmt).unwrap().1;
+
+ let res = connection
+ .sql_has_syntax_error(&format!("{}\n{}", first_stmt, second_stmt))
+ .map(|(_, offset)| offset);
+
+ assert_eq!(res, Some(first_stmt.len() + second_offset + 1));
+ }
+}
@@ -0,0 +1,56 @@
+use crate::connection::Connection;
+
+pub trait Domain: 'static {
+ fn name() -> &'static str;
+ fn migrations() -> &'static [&'static str];
+}
+
+pub trait Migrator: 'static {
+ fn migrate(connection: &Connection) -> anyhow::Result<()>;
+}
+
+impl Migrator for () {
+ fn migrate(_connection: &Connection) -> anyhow::Result<()> {
+ Ok(()) // Do nothing
+ }
+}
+
+impl<D: Domain> Migrator for D {
+ fn migrate(connection: &Connection) -> anyhow::Result<()> {
+ connection.migrate(Self::name(), Self::migrations())
+ }
+}
+
+impl<D1: Domain, D2: Domain> Migrator for (D1, D2) {
+ fn migrate(connection: &Connection) -> anyhow::Result<()> {
+ D1::migrate(connection)?;
+ D2::migrate(connection)
+ }
+}
+
+impl<D1: Domain, D2: Domain, D3: Domain> Migrator for (D1, D2, D3) {
+ fn migrate(connection: &Connection) -> anyhow::Result<()> {
+ D1::migrate(connection)?;
+ D2::migrate(connection)?;
+ D3::migrate(connection)
+ }
+}
+
+impl<D1: Domain, D2: Domain, D3: Domain, D4: Domain> Migrator for (D1, D2, D3, D4) {
+ fn migrate(connection: &Connection) -> anyhow::Result<()> {
+ D1::migrate(connection)?;
+ D2::migrate(connection)?;
+ D3::migrate(connection)?;
+ D4::migrate(connection)
+ }
+}
+
+impl<D1: Domain, D2: Domain, D3: Domain, D4: Domain, D5: Domain> Migrator for (D1, D2, D3, D4, D5) {
+ fn migrate(connection: &Connection) -> anyhow::Result<()> {
+ D1::migrate(connection)?;
+ D2::migrate(connection)?;
+ D3::migrate(connection)?;
+ D4::migrate(connection)?;
+ D5::migrate(connection)
+ }
+}
@@ -0,0 +1,11 @@
+pub mod bindable;
+pub mod connection;
+pub mod domain;
+pub mod migrations;
+pub mod savepoint;
+pub mod statement;
+pub mod thread_safe_connection;
+pub mod typed_statements;
+mod util;
+
+pub use anyhow;
@@ -0,0 +1,260 @@
+// Migrations are constructed by domain, and stored in a table in the connection db with domain name,
+// effected tables, actual query text, and order.
+// If a migration is run and any of the query texts don't match, the app panics on startup (maybe fallback
+// to creating a new db?)
+// Otherwise any missing migrations are run on the connection
+
+use anyhow::{anyhow, Result};
+use indoc::{formatdoc, indoc};
+
+use crate::connection::Connection;
+
+impl Connection {
+ pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
+ self.with_savepoint("migrating", || {
+ // Setup the migrations table unconditionally
+ self.exec(indoc! {"
+ CREATE TABLE IF NOT EXISTS migrations (
+ domain TEXT,
+ step INTEGER,
+ migration TEXT
+ )"})?()?;
+
+ let completed_migrations =
+ self.select_bound::<&str, (String, usize, String)>(indoc! {"
+ SELECT domain, step, migration FROM migrations
+ WHERE domain = ?
+ ORDER BY step
+ "})?(domain)?;
+
+ let mut store_completed_migration = self
+ .exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
+
+ for (index, migration) in migrations.iter().enumerate() {
+ if let Some((_, _, completed_migration)) = completed_migrations.get(index) {
+ if completed_migration != migration {
+ return Err(anyhow!(formatdoc! {"
+ Migration changed for {} at step {}
+
+ Stored migration:
+ {}
+
+ Proposed migration:
+ {}", domain, index, completed_migration, migration}));
+ } else {
+ // Migration already run. Continue
+ continue;
+ }
+ }
+
+ self.exec(migration)?()?;
+ store_completed_migration((domain, index, *migration))?;
+ }
+
+ Ok(())
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use indoc::indoc;
+
+ use crate::connection::Connection;
+
+ #[test]
+ fn test_migrations_are_added_to_table() {
+ let connection = Connection::open_memory(Some("migrations_are_added_to_table"));
+
+ // Create first migration with a single step and run it
+ connection
+ .migrate(
+ "test",
+ &[indoc! {"
+ CREATE TABLE test1 (
+ a TEXT,
+ b TEXT
+ )"}],
+ )
+ .unwrap();
+
+ // Verify it got added to the migrations table
+ assert_eq!(
+ &connection
+ .select::<String>("SELECT (migration) FROM migrations")
+ .unwrap()()
+ .unwrap()[..],
+ &[indoc! {"
+ CREATE TABLE test1 (
+ a TEXT,
+ b TEXT
+ )"}],
+ );
+
+ // Add another step to the migration and run it again
+ connection
+ .migrate(
+ "test",
+ &[
+ indoc! {"
+ CREATE TABLE test1 (
+ a TEXT,
+ b TEXT
+ )"},
+ indoc! {"
+ CREATE TABLE test2 (
+ c TEXT,
+ d TEXT
+ )"},
+ ],
+ )
+ .unwrap();
+
+ // Verify it is also added to the migrations table
+ assert_eq!(
+ &connection
+ .select::<String>("SELECT (migration) FROM migrations")
+ .unwrap()()
+ .unwrap()[..],
+ &[
+ indoc! {"
+ CREATE TABLE test1 (
+ a TEXT,
+ b TEXT
+ )"},
+ indoc! {"
+ CREATE TABLE test2 (
+ c TEXT,
+ d TEXT
+ )"},
+ ],
+ );
+ }
+
+ #[test]
+ fn test_migration_setup_works() {
+ let connection = Connection::open_memory(Some("migration_setup_works"));
+
+ connection
+ .exec(indoc! {"
+ CREATE TABLE IF NOT EXISTS migrations (
+ domain TEXT,
+ step INTEGER,
+ migration TEXT
+ );"})
+ .unwrap()()
+ .unwrap();
+
+ let mut store_completed_migration = connection
+ .exec_bound::<(&str, usize, String)>(indoc! {"
+ INSERT INTO migrations (domain, step, migration)
+ VALUES (?, ?, ?)"})
+ .unwrap();
+
+ let domain = "test_domain";
+ for i in 0..5 {
+ // Create a table forcing a schema change
+ connection
+ .exec(&format!("CREATE TABLE table{} ( test TEXT );", i))
+ .unwrap()()
+ .unwrap();
+
+ store_completed_migration((domain, i, i.to_string())).unwrap();
+ }
+ }
+
+ #[test]
+ fn migrations_dont_rerun() {
+ let connection = Connection::open_memory(Some("migrations_dont_rerun"));
+
+ // Create migration which clears a tabl
+
+ // Manually create the table for that migration with a row
+ connection
+ .exec(indoc! {"
+ CREATE TABLE test_table (
+ test_column INTEGER
+ );"})
+ .unwrap()()
+ .unwrap();
+ connection
+ .exec(indoc! {"
+ INSERT INTO test_table (test_column) VALUES (1);"})
+ .unwrap()()
+ .unwrap();
+
+ assert_eq!(
+ connection
+ .select_row::<usize>("SELECT * FROM test_table")
+ .unwrap()()
+ .unwrap(),
+ Some(1)
+ );
+
+ // Run the migration verifying that the row got dropped
+ connection
+ .migrate("test", &["DELETE FROM test_table"])
+ .unwrap();
+ assert_eq!(
+ connection
+ .select_row::<usize>("SELECT * FROM test_table")
+ .unwrap()()
+ .unwrap(),
+ None
+ );
+
+ // Recreate the dropped row
+ connection
+ .exec("INSERT INTO test_table (test_column) VALUES (2)")
+ .unwrap()()
+ .unwrap();
+
+ // Run the same migration again and verify that the table was left unchanged
+ connection
+ .migrate("test", &["DELETE FROM test_table"])
+ .unwrap();
+ assert_eq!(
+ connection
+ .select_row::<usize>("SELECT * FROM test_table")
+ .unwrap()()
+ .unwrap(),
+ Some(2)
+ );
+ }
+
+ #[test]
+ fn changed_migration_fails() {
+ let connection = Connection::open_memory(Some("changed_migration_fails"));
+
+ // Create a migration with two steps and run it
+ connection
+ .migrate(
+ "test migration",
+ &[
+ indoc! {"
+ CREATE TABLE test (
+ col INTEGER
+ )"},
+ indoc! {"
+ INSERT INTO test (col) VALUES (1)"},
+ ],
+ )
+ .unwrap();
+
+ // Create another migration with the same domain but different steps
+ let second_migration_result = connection.migrate(
+ "test migration",
+ &[
+ indoc! {"
+ CREATE TABLE test (
+ color INTEGER
+ )"},
+ indoc! {"
+ INSERT INTO test (color) VALUES (1)"},
+ ],
+ );
+
+ // Verify new migration returns error when run
+ assert!(second_migration_result.is_err())
+ }
+}
@@ -0,0 +1,148 @@
+use anyhow::Result;
+use indoc::formatdoc;
+
+use crate::connection::Connection;
+
+impl Connection {
+ // Run a set of commands within the context of a `SAVEPOINT name`. If the callback
+ // returns Err(_), the savepoint will be rolled back. Otherwise, the save
+ // point is released.
+ pub fn with_savepoint<R, F>(&self, name: impl AsRef<str>, f: F) -> Result<R>
+ where
+ F: FnOnce() -> Result<R>,
+ {
+ let name = name.as_ref();
+ self.exec(&format!("SAVEPOINT {name}"))?()?;
+ let result = f();
+ match result {
+ Ok(_) => {
+ self.exec(&format!("RELEASE {name}"))?()?;
+ }
+ Err(_) => {
+ self.exec(&formatdoc! {"
+ ROLLBACK TO {name};
+ RELEASE {name}"})?()?;
+ }
+ }
+ result
+ }
+
+ // Run a set of commands within the context of a `SAVEPOINT name`. If the callback
+ // returns Ok(None) or Err(_), the savepoint will be rolled back. Otherwise, the save
+ // point is released.
+ pub fn with_savepoint_rollback<R, F>(&self, name: impl AsRef<str>, f: F) -> Result<Option<R>>
+ where
+ F: FnOnce() -> Result<Option<R>>,
+ {
+ let name = name.as_ref();
+ self.exec(&format!("SAVEPOINT {name}"))?()?;
+ let result = f();
+ match result {
+ Ok(Some(_)) => {
+ self.exec(&format!("RELEASE {name}"))?()?;
+ }
+ Ok(None) | Err(_) => {
+ self.exec(&formatdoc! {"
+ ROLLBACK TO {name};
+ RELEASE {name}"})?()?;
+ }
+ }
+ result
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::connection::Connection;
+ use anyhow::Result;
+ use indoc::indoc;
+
+ #[test]
+ fn test_nested_savepoints() -> Result<()> {
+ let connection = Connection::open_memory(Some("nested_savepoints"));
+
+ connection
+ .exec(indoc! {"
+ CREATE TABLE text (
+ text TEXT,
+ idx INTEGER
+ );"})
+ .unwrap()()
+ .unwrap();
+
+ let save1_text = "test save1";
+ let save2_text = "test save2";
+
+ connection.with_savepoint("first", || {
+ connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((save1_text, 1))?;
+
+ assert!(connection
+ .with_savepoint("second", || -> Result<Option<()>, anyhow::Error> {
+ connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
+ save2_text, 2,
+ ))?;
+
+ assert_eq!(
+ connection
+ .select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?(
+ )?,
+ vec![save1_text, save2_text],
+ );
+
+ anyhow::bail!("Failed second save point :(")
+ })
+ .err()
+ .is_some());
+
+ assert_eq!(
+ connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
+ vec![save1_text],
+ );
+
+ connection.with_savepoint_rollback::<(), _>("second", || {
+ connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
+ save2_text, 2,
+ ))?;
+
+ assert_eq!(
+ connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
+ vec![save1_text, save2_text],
+ );
+
+ Ok(None)
+ })?;
+
+ assert_eq!(
+ connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
+ vec![save1_text],
+ );
+
+ connection.with_savepoint_rollback("second", || {
+ connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
+ save2_text, 2,
+ ))?;
+
+ assert_eq!(
+ connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
+ vec![save1_text, save2_text],
+ );
+
+ Ok(Some(()))
+ })?;
+
+ assert_eq!(
+ connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
+ vec![save1_text, save2_text],
+ );
+
+ Ok(())
+ })?;
+
+ assert_eq!(
+ connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
+ vec![save1_text, save2_text],
+ );
+
+ Ok(())
+ }
+}
@@ -0,0 +1,491 @@
+use std::ffi::{c_int, CStr, CString};
+use std::marker::PhantomData;
+use std::{ptr, slice, str};
+
+use anyhow::{anyhow, bail, Context, Result};
+use libsqlite3_sys::*;
+
+use crate::bindable::{Bind, Column};
+use crate::connection::Connection;
+
+pub struct Statement<'a> {
+ raw_statements: Vec<*mut sqlite3_stmt>,
+ current_statement: usize,
+ connection: &'a Connection,
+ phantom: PhantomData<sqlite3_stmt>,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum StepResult {
+ Row,
+ Done,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum SqlType {
+ Text,
+ Integer,
+ Blob,
+ Float,
+ Null,
+}
+
+impl<'a> Statement<'a> {
+ pub fn prepare<T: AsRef<str>>(connection: &'a Connection, query: T) -> Result<Self> {
+ let mut statement = Self {
+ raw_statements: Default::default(),
+ current_statement: 0,
+ connection,
+ phantom: PhantomData,
+ };
+ unsafe {
+ let sql = CString::new(query.as_ref()).context("Error creating cstr")?;
+ let mut remaining_sql = sql.as_c_str();
+ while {
+ let remaining_sql_str = remaining_sql
+ .to_str()
+ .context("Parsing remaining sql")?
+ .trim();
+ remaining_sql_str != ";" && !remaining_sql_str.is_empty()
+ } {
+ let mut raw_statement = 0 as *mut sqlite3_stmt;
+ let mut remaining_sql_ptr = ptr::null();
+ sqlite3_prepare_v2(
+ connection.sqlite3,
+ remaining_sql.as_ptr(),
+ -1,
+ &mut raw_statement,
+ &mut remaining_sql_ptr,
+ );
+
+ remaining_sql = CStr::from_ptr(remaining_sql_ptr);
+ statement.raw_statements.push(raw_statement);
+
+ connection.last_error().with_context(|| {
+ format!("Prepare call failed for query:\n{}", query.as_ref())
+ })?;
+
+ if !connection.can_write() && sqlite3_stmt_readonly(raw_statement) == 0 {
+ let sql = CStr::from_ptr(sqlite3_sql(raw_statement));
+
+ bail!(
+ "Write statement prepared with connection that is not write capable. SQL:\n{} ",
+ sql.to_str()?)
+ }
+ }
+ }
+
+ Ok(statement)
+ }
+
+ fn current_statement(&self) -> *mut sqlite3_stmt {
+ *self.raw_statements.get(self.current_statement).unwrap()
+ }
+
+ pub fn reset(&mut self) {
+ unsafe {
+ for raw_statement in self.raw_statements.iter() {
+ sqlite3_reset(*raw_statement);
+ }
+ }
+ self.current_statement = 0;
+ }
+
+ pub fn parameter_count(&self) -> i32 {
+ unsafe {
+ self.raw_statements
+ .iter()
+ .map(|raw_statement| sqlite3_bind_parameter_count(*raw_statement))
+ .max()
+ .unwrap_or(0)
+ }
+ }
+
+ fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt) -> ()) -> Result<()> {
+ let mut any_succeed = false;
+ unsafe {
+ for raw_statement in self.raw_statements.iter() {
+ if index <= sqlite3_bind_parameter_count(*raw_statement) {
+ bind(raw_statement);
+ self.connection
+ .last_error()
+ .with_context(|| format!("Failed to bind value at index {index}"))?;
+ any_succeed = true;
+ } else {
+ continue;
+ }
+ }
+ }
+ if any_succeed {
+ Ok(())
+ } else {
+ Err(anyhow!("Failed to bind parameters"))
+ }
+ }
+
+ pub fn bind_blob(&self, index: i32, blob: &[u8]) -> Result<()> {
+ let index = index as c_int;
+ let blob_pointer = blob.as_ptr() as *const _;
+ let len = blob.len() as c_int;
+
+ self.bind_index_with(index, |raw_statement| unsafe {
+ sqlite3_bind_blob(*raw_statement, index, blob_pointer, len, SQLITE_TRANSIENT());
+ })
+ }
+
+ pub fn column_blob<'b>(&'b mut self, index: i32) -> Result<&'b [u8]> {
+ let index = index as c_int;
+ let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) };
+
+ self.connection
+ .last_error()
+ .with_context(|| format!("Failed to read blob at index {index}"))?;
+ if pointer.is_null() {
+ return Ok(&[]);
+ }
+ let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize };
+ self.connection
+ .last_error()
+ .with_context(|| format!("Failed to read length of blob at index {index}"))?;
+
+ unsafe { Ok(slice::from_raw_parts(pointer as *const u8, len)) }
+ }
+
+ pub fn bind_double(&self, index: i32, double: f64) -> Result<()> {
+ let index = index as c_int;
+
+ self.bind_index_with(index, |raw_statement| unsafe {
+ sqlite3_bind_double(*raw_statement, index, double);
+ })
+ }
+
+ pub fn column_double(&self, index: i32) -> Result<f64> {
+ let index = index as c_int;
+ let result = unsafe { sqlite3_column_double(self.current_statement(), index) };
+ self.connection
+ .last_error()
+ .with_context(|| format!("Failed to read double at index {index}"))?;
+ Ok(result)
+ }
+
+ pub fn bind_int(&self, index: i32, int: i32) -> Result<()> {
+ let index = index as c_int;
+ self.bind_index_with(index, |raw_statement| unsafe {
+ sqlite3_bind_int(*raw_statement, index, int);
+ })
+ }
+
+ pub fn column_int(&self, index: i32) -> Result<i32> {
+ let index = index as c_int;
+ let result = unsafe { sqlite3_column_int(self.current_statement(), index) };
+ self.connection
+ .last_error()
+ .with_context(|| format!("Failed to read int at index {index}"))?;
+ Ok(result)
+ }
+
+ pub fn bind_int64(&self, index: i32, int: i64) -> Result<()> {
+ let index = index as c_int;
+ self.bind_index_with(index, |raw_statement| unsafe {
+ sqlite3_bind_int64(*raw_statement, index, int);
+ })
+ }
+
+ pub fn column_int64(&self, index: i32) -> Result<i64> {
+ let index = index as c_int;
+ let result = unsafe { sqlite3_column_int64(self.current_statement(), index) };
+ self.connection
+ .last_error()
+ .with_context(|| format!("Failed to read i64 at index {index}"))?;
+ Ok(result)
+ }
+
+ pub fn bind_null(&self, index: i32) -> Result<()> {
+ let index = index as c_int;
+ self.bind_index_with(index, |raw_statement| unsafe {
+ sqlite3_bind_null(*raw_statement, index);
+ })
+ }
+
+ pub fn bind_text(&self, index: i32, text: &str) -> Result<()> {
+ let index = index as c_int;
+ let text_pointer = text.as_ptr() as *const _;
+ let len = text.len() as c_int;
+
+ self.bind_index_with(index, |raw_statement| unsafe {
+ sqlite3_bind_text(*raw_statement, index, text_pointer, len, SQLITE_TRANSIENT());
+ })
+ }
+
+ pub fn column_text<'b>(&'b mut self, index: i32) -> Result<&'b str> {
+ let index = index as c_int;
+ let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) };
+
+ self.connection
+ .last_error()
+ .with_context(|| format!("Failed to read text from column {index}"))?;
+ if pointer.is_null() {
+ return Ok("");
+ }
+ let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize };
+ self.connection
+ .last_error()
+ .with_context(|| format!("Failed to read text length at {index}"))?;
+
+ let slice = unsafe { slice::from_raw_parts(pointer as *const u8, len) };
+ Ok(str::from_utf8(slice)?)
+ }
+
+ pub fn bind<T: Bind>(&self, value: T, index: i32) -> Result<i32> {
+ debug_assert!(index > 0);
+ value.bind(self, index)
+ }
+
+ pub fn column<T: Column>(&mut self) -> Result<T> {
+ let (result, _) = T::column(self, 0)?;
+ Ok(result)
+ }
+
+ pub fn column_type(&mut self, index: i32) -> Result<SqlType> {
+ let result = unsafe { sqlite3_column_type(self.current_statement(), index) };
+ self.connection.last_error()?;
+ match result {
+ SQLITE_INTEGER => Ok(SqlType::Integer),
+ SQLITE_FLOAT => Ok(SqlType::Float),
+ SQLITE_TEXT => Ok(SqlType::Text),
+ SQLITE_BLOB => Ok(SqlType::Blob),
+ SQLITE_NULL => Ok(SqlType::Null),
+ _ => Err(anyhow!("Column type returned was incorrect ")),
+ }
+ }
+
+ pub fn with_bindings(&mut self, bindings: impl Bind) -> Result<&mut Self> {
+ self.bind(bindings, 1)?;
+ Ok(self)
+ }
+
+ fn step(&mut self) -> Result<StepResult> {
+ unsafe {
+ match sqlite3_step(self.current_statement()) {
+ SQLITE_ROW => Ok(StepResult::Row),
+ SQLITE_DONE => {
+ if self.current_statement >= self.raw_statements.len() - 1 {
+ Ok(StepResult::Done)
+ } else {
+ self.current_statement += 1;
+ self.step()
+ }
+ }
+ SQLITE_MISUSE => Err(anyhow!("Statement step returned SQLITE_MISUSE")),
+ _other_error => {
+ self.connection.last_error()?;
+ unreachable!("Step returned error code and last error failed to catch it");
+ }
+ }
+ }
+ }
+
+ pub fn exec(&mut self) -> Result<()> {
+ fn logic(this: &mut Statement) -> Result<()> {
+ while this.step()? == StepResult::Row {}
+ Ok(())
+ }
+ let result = logic(self);
+ self.reset();
+ result
+ }
+
+ pub fn map<R>(&mut self, callback: impl FnMut(&mut Statement) -> Result<R>) -> Result<Vec<R>> {
+ fn logic<R>(
+ this: &mut Statement,
+ mut callback: impl FnMut(&mut Statement) -> Result<R>,
+ ) -> Result<Vec<R>> {
+ let mut mapped_rows = Vec::new();
+ while this.step()? == StepResult::Row {
+ mapped_rows.push(callback(this)?);
+ }
+ Ok(mapped_rows)
+ }
+
+ let result = logic(self, callback);
+ self.reset();
+ result
+ }
+
+ pub fn rows<R: Column>(&mut self) -> Result<Vec<R>> {
+ self.map(|s| s.column::<R>())
+ }
+
+ pub fn single<R>(&mut self, callback: impl FnOnce(&mut Statement) -> Result<R>) -> Result<R> {
+ fn logic<R>(
+ this: &mut Statement,
+ callback: impl FnOnce(&mut Statement) -> Result<R>,
+ ) -> Result<R> {
+ if this.step()? != StepResult::Row {
+ return Err(anyhow!("single called with query that returns no rows."));
+ }
+ let result = callback(this)?;
+
+ if this.step()? != StepResult::Done {
+ return Err(anyhow!(
+ "single called with a query that returns more than one row."
+ ));
+ }
+
+ Ok(result)
+ }
+ let result = logic(self, callback);
+ self.reset();
+ result
+ }
+
+ pub fn row<R: Column>(&mut self) -> Result<R> {
+ self.single(|this| this.column::<R>())
+ }
+
+ pub fn maybe<R>(
+ &mut self,
+ callback: impl FnOnce(&mut Statement) -> Result<R>,
+ ) -> Result<Option<R>> {
+ fn logic<R>(
+ this: &mut Statement,
+ callback: impl FnOnce(&mut Statement) -> Result<R>,
+ ) -> Result<Option<R>> {
+ if this.step().context("Failed on step call")? != StepResult::Row {
+ return Ok(None);
+ }
+
+ let result = callback(this)
+ .map(|r| Some(r))
+ .context("Failed to parse row result")?;
+
+ if this.step().context("Second step call")? != StepResult::Done {
+ return Err(anyhow!(
+ "maybe called with a query that returns more than one row."
+ ));
+ }
+
+ Ok(result)
+ }
+ let result = logic(self, callback);
+ self.reset();
+ result
+ }
+
+ pub fn maybe_row<R: Column>(&mut self) -> Result<Option<R>> {
+ self.maybe(|this| this.column::<R>())
+ }
+}
+
+impl<'a> Drop for Statement<'a> {
+ fn drop(&mut self) {
+ unsafe {
+ for raw_statement in self.raw_statements.iter() {
+ sqlite3_finalize(*raw_statement);
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use indoc::indoc;
+
+ use crate::{
+ connection::Connection,
+ statement::{Statement, StepResult},
+ };
+
+ #[test]
+ fn binding_multiple_statements_with_parameter_gaps() {
+ let connection =
+ Connection::open_memory(Some("binding_multiple_statements_with_parameter_gaps"));
+
+ connection
+ .exec(indoc! {"
+ CREATE TABLE test (
+ col INTEGER
+ )"})
+ .unwrap()()
+ .unwrap();
+
+ let statement = Statement::prepare(
+ &connection,
+ indoc! {"
+ INSERT INTO test(col) VALUES (?3);
+ SELECT * FROM test WHERE col = ?1"},
+ )
+ .unwrap();
+
+ statement
+ .bind_int(1, 1)
+ .expect("Could not bind parameter to first index");
+ statement
+ .bind_int(2, 2)
+ .expect("Could not bind parameter to second index");
+ statement
+ .bind_int(3, 3)
+ .expect("Could not bind parameter to third index");
+ }
+
+ #[test]
+ fn blob_round_trips() {
+ let connection1 = Connection::open_memory(Some("blob_round_trips"));
+ connection1
+ .exec(indoc! {"
+ CREATE TABLE blobs (
+ data BLOB
+ )"})
+ .unwrap()()
+ .unwrap();
+
+ let blob = &[0, 1, 2, 4, 8, 16, 32, 64];
+
+ let mut write =
+ Statement::prepare(&connection1, "INSERT INTO blobs (data) VALUES (?)").unwrap();
+ write.bind_blob(1, blob).unwrap();
+ assert_eq!(write.step().unwrap(), StepResult::Done);
+
+ // Read the blob from the
+ let connection2 = Connection::open_memory(Some("blob_round_trips"));
+ let mut read = Statement::prepare(&connection2, "SELECT * FROM blobs").unwrap();
+ assert_eq!(read.step().unwrap(), StepResult::Row);
+ assert_eq!(read.column_blob(0).unwrap(), blob);
+ assert_eq!(read.step().unwrap(), StepResult::Done);
+
+ // Delete the added blob and verify its deleted on the other side
+ connection2.exec("DELETE FROM blobs").unwrap()().unwrap();
+ let mut read = Statement::prepare(&connection1, "SELECT * FROM blobs").unwrap();
+ assert_eq!(read.step().unwrap(), StepResult::Done);
+ }
+
+ #[test]
+ pub fn maybe_returns_options() {
+ let connection = Connection::open_memory(Some("maybe_returns_options"));
+ connection
+ .exec(indoc! {"
+ CREATE TABLE texts (
+ text TEXT
+ )"})
+ .unwrap()()
+ .unwrap();
+
+ assert!(connection
+ .select_row::<String>("SELECT text FROM texts")
+ .unwrap()()
+ .unwrap()
+ .is_none());
+
+ let text_to_insert = "This is a test";
+
+ connection
+ .exec_bound("INSERT INTO texts VALUES (?)")
+ .unwrap()(text_to_insert)
+ .unwrap();
+
+ assert_eq!(
+ connection.select_row("SELECT text FROM texts").unwrap()().unwrap(),
+ Some(text_to_insert.to_string())
+ );
+ }
+}
@@ -0,0 +1,359 @@
+use anyhow::Context;
+use futures::{channel::oneshot, Future, FutureExt};
+use lazy_static::lazy_static;
+use parking_lot::{Mutex, RwLock};
+use std::{collections::HashMap, marker::PhantomData, ops::Deref, sync::Arc, thread};
+use thread_local::ThreadLocal;
+
+use crate::{connection::Connection, domain::Migrator, util::UnboundedSyncSender};
+
+const MIGRATION_RETRIES: usize = 10;
+
+type QueuedWrite = Box<dyn 'static + Send + FnOnce()>;
+type WriteQueueConstructor =
+ Box<dyn 'static + Send + FnMut() -> Box<dyn 'static + Send + Sync + Fn(QueuedWrite)>>;
+lazy_static! {
+ /// List of queues of tasks by database uri. This lets us serialize writes to the database
+ /// and have a single worker thread per db file. This means many thread safe connections
+ /// (possibly with different migrations) could all be communicating with the same background
+ /// thread.
+ static ref QUEUES: RwLock<HashMap<Arc<str>, Box<dyn 'static + Send + Sync + Fn(QueuedWrite)>>> =
+ Default::default();
+}
+
+/// Thread safe connection to a given database file or in memory db. This can be cloned, shared, static,
+/// whatever. It derefs to a synchronous connection by thread that is read only. A write capable connection
+/// may be accessed by passing a callback to the `write` function which will queue the callback
+pub struct ThreadSafeConnection<M: Migrator + 'static = ()> {
+ uri: Arc<str>,
+ persistent: bool,
+ connection_initialize_query: Option<&'static str>,
+ connections: Arc<ThreadLocal<Connection>>,
+ _migrator: PhantomData<*mut M>,
+}
+
+unsafe impl<M: Migrator> Send for ThreadSafeConnection<M> {}
+unsafe impl<M: Migrator> Sync for ThreadSafeConnection<M> {}
+
+pub struct ThreadSafeConnectionBuilder<M: Migrator + 'static = ()> {
+ db_initialize_query: Option<&'static str>,
+ write_queue_constructor: Option<WriteQueueConstructor>,
+ connection: ThreadSafeConnection<M>,
+}
+
+impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
+ /// Sets the query to run every time a connection is opened. This must
+ /// be infallible (EG only use pragma statements) and not cause writes.
+ /// to the db or it will panic.
+ pub fn with_connection_initialize_query(mut self, initialize_query: &'static str) -> Self {
+ self.connection.connection_initialize_query = Some(initialize_query);
+ self
+ }
+
+ /// Queues an initialization query for the database file. This must be infallible
+ /// but may cause changes to the database file such as with `PRAGMA journal_mode`
+ pub fn with_db_initialization_query(mut self, initialize_query: &'static str) -> Self {
+ self.db_initialize_query = Some(initialize_query);
+ self
+ }
+
+ /// Specifies how the thread safe connection should serialize writes. If provided
+ /// the connection will call the write_queue_constructor for each database file in
+ /// this process. The constructor is responsible for setting up a background thread or
+ /// async task which handles queued writes with the provided connection.
+ pub fn with_write_queue_constructor(
+ mut self,
+ write_queue_constructor: WriteQueueConstructor,
+ ) -> Self {
+ self.write_queue_constructor = Some(write_queue_constructor);
+ self
+ }
+
+ pub async fn build(self) -> anyhow::Result<ThreadSafeConnection<M>> {
+ self.connection
+ .initialize_queues(self.write_queue_constructor);
+
+ let db_initialize_query = self.db_initialize_query;
+
+ self.connection
+ .write(move |connection| {
+ if let Some(db_initialize_query) = db_initialize_query {
+ connection.exec(db_initialize_query).with_context(|| {
+ format!(
+ "Db initialize query failed to execute: {}",
+ db_initialize_query
+ )
+ })?()?;
+ }
+
+ // Retry failed migrations in case they were run in parallel from different
+ // processes. This gives a best attempt at migrating before bailing
+ let mut migration_result =
+ anyhow::Result::<()>::Err(anyhow::anyhow!("Migration never run"));
+
+ for _ in 0..MIGRATION_RETRIES {
+ migration_result = connection
+ .with_savepoint("thread_safe_multi_migration", || M::migrate(connection));
+
+ if migration_result.is_ok() {
+ break;
+ }
+ }
+
+ migration_result
+ })
+ .await?;
+
+ Ok(self.connection)
+ }
+}
+
+impl<M: Migrator> ThreadSafeConnection<M> {
+ fn initialize_queues(&self, write_queue_constructor: Option<WriteQueueConstructor>) -> bool {
+ if !QUEUES.read().contains_key(&self.uri) {
+ let mut queues = QUEUES.write();
+ if !queues.contains_key(&self.uri) {
+ let mut write_queue_constructor =
+ write_queue_constructor.unwrap_or(background_thread_queue());
+ queues.insert(self.uri.clone(), write_queue_constructor());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ pub fn builder(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder<M> {
+ ThreadSafeConnectionBuilder::<M> {
+ db_initialize_query: None,
+ write_queue_constructor: None,
+ connection: Self {
+ uri: Arc::from(uri),
+ persistent,
+ connection_initialize_query: None,
+ connections: Default::default(),
+ _migrator: PhantomData,
+ },
+ }
+ }
+
+ /// Opens a new db connection with the initialized file path. This is internal and only
+ /// called from the deref function.
+ fn open_file(uri: &str) -> Connection {
+ Connection::open_file(uri)
+ }
+
+ /// Opens a shared memory connection using the file path as the identifier. This is internal
+ /// and only called from the deref function.
+ fn open_shared_memory(uri: &str) -> Connection {
+ Connection::open_memory(Some(uri))
+ }
+
+ pub fn write<T: 'static + Send + Sync>(
+ &self,
+ callback: impl 'static + Send + FnOnce(&Connection) -> T,
+ ) -> impl Future<Output = T> {
+ // Check and invalidate queue and maybe recreate queue
+ let queues = QUEUES.read();
+ let write_channel = queues
+ .get(&self.uri)
+ .expect("Queues are inserted when build is called. This should always succeed");
+
+ // Create a one shot channel for the result of the queued write
+ // so we can await on the result
+ let (sender, reciever) = oneshot::channel();
+
+ let thread_safe_connection = (*self).clone();
+ write_channel(Box::new(move || {
+ let connection = thread_safe_connection.deref();
+ let result = connection.with_write(|connection| callback(connection));
+ sender.send(result).ok();
+ }));
+ reciever.map(|response| response.expect("Write queue unexpectedly closed"))
+ }
+
+ pub(crate) fn create_connection(
+ persistent: bool,
+ uri: &str,
+ connection_initialize_query: Option<&'static str>,
+ ) -> Connection {
+ let mut connection = if persistent {
+ Self::open_file(uri)
+ } else {
+ Self::open_shared_memory(uri)
+ };
+
+ // Disallow writes on the connection. The only writes allowed for thread safe connections
+ // are from the background thread that can serialize them.
+ *connection.write.get_mut() = false;
+
+ if let Some(initialize_query) = connection_initialize_query {
+ connection.exec(initialize_query).expect(&format!(
+ "Initialize query failed to execute: {}",
+ initialize_query
+ ))()
+ .unwrap()
+ }
+
+ connection
+ }
+}
+
+impl ThreadSafeConnection<()> {
+ /// Special constructor for ThreadSafeConnection which disallows db initialization and migrations.
+ /// This allows construction to be infallible and not write to the db.
+ pub fn new(
+ uri: &str,
+ persistent: bool,
+ connection_initialize_query: Option<&'static str>,
+ write_queue_constructor: Option<WriteQueueConstructor>,
+ ) -> Self {
+ let connection = Self {
+ uri: Arc::from(uri),
+ persistent,
+ connection_initialize_query,
+ connections: Default::default(),
+ _migrator: PhantomData,
+ };
+
+ connection.initialize_queues(write_queue_constructor);
+ connection
+ }
+}
+
+impl<M: Migrator> Clone for ThreadSafeConnection<M> {
+ fn clone(&self) -> Self {
+ Self {
+ uri: self.uri.clone(),
+ persistent: self.persistent,
+ connection_initialize_query: self.connection_initialize_query.clone(),
+ connections: self.connections.clone(),
+ _migrator: PhantomData,
+ }
+ }
+}
+
+impl<M: Migrator> Deref for ThreadSafeConnection<M> {
+ type Target = Connection;
+
+ fn deref(&self) -> &Self::Target {
+ self.connections.get_or(|| {
+ Self::create_connection(self.persistent, &self.uri, self.connection_initialize_query)
+ })
+ }
+}
+
+pub fn background_thread_queue() -> WriteQueueConstructor {
+ use std::sync::mpsc::channel;
+
+ Box::new(|| {
+ let (sender, reciever) = channel::<QueuedWrite>();
+
+ thread::spawn(move || {
+ while let Ok(write) = reciever.recv() {
+ write()
+ }
+ });
+
+ let sender = UnboundedSyncSender::new(sender);
+ Box::new(move |queued_write| {
+ sender
+ .send(queued_write)
+ .expect("Could not send write action to background thread");
+ })
+ })
+}
+
+pub fn locking_queue() -> WriteQueueConstructor {
+ Box::new(|| {
+ let write_mutex = Mutex::new(());
+ Box::new(move |queued_write| {
+ let _lock = write_mutex.lock();
+ queued_write();
+ })
+ })
+}
+
+#[cfg(test)]
+mod test {
+ use indoc::indoc;
+ use lazy_static::__Deref;
+
+ use std::thread;
+
+ use crate::{domain::Domain, thread_safe_connection::ThreadSafeConnection};
+
+ #[test]
+ fn many_initialize_and_migrate_queries_at_once() {
+ let mut handles = vec![];
+
+ enum TestDomain {}
+ impl Domain for TestDomain {
+ fn name() -> &'static str {
+ "test"
+ }
+ fn migrations() -> &'static [&'static str] {
+ &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]
+ }
+ }
+
+ for _ in 0..100 {
+ handles.push(thread::spawn(|| {
+ let builder =
+ ThreadSafeConnection::<TestDomain>::builder("annoying-test.db", false)
+ .with_db_initialization_query("PRAGMA journal_mode=WAL")
+ .with_connection_initialize_query(indoc! {"
+ PRAGMA synchronous=NORMAL;
+ PRAGMA busy_timeout=1;
+ PRAGMA foreign_keys=TRUE;
+ PRAGMA case_sensitive_like=TRUE;
+ "});
+
+ let _ = smol::block_on(builder.build()).unwrap().deref();
+ }));
+ }
+
+ for handle in handles {
+ let _ = handle.join();
+ }
+ }
+
+ #[test]
+ #[should_panic]
+ fn wild_zed_lost_failure() {
+ enum TestWorkspace {}
+ impl Domain for TestWorkspace {
+ fn name() -> &'static str {
+ "workspace"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &["
+ CREATE TABLE workspaces(
+ workspace_id INTEGER PRIMARY KEY,
+ dock_visible INTEGER, -- Boolean
+ dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
+ dock_pane INTEGER, -- NULL indicates that we don't have a dock pane yet
+ timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ FOREIGN KEY(dock_pane) REFERENCES panes(pane_id),
+ FOREIGN KEY(active_pane) REFERENCES panes(pane_id)
+ ) STRICT;
+
+ CREATE TABLE panes(
+ pane_id INTEGER PRIMARY KEY,
+ workspace_id INTEGER NOT NULL,
+ active INTEGER NOT NULL, -- Boolean
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+ ) STRICT;
+ "]
+ }
+ }
+
+ let builder =
+ ThreadSafeConnection::<TestWorkspace>::builder("wild_zed_lost_failure", false)
+ .with_connection_initialize_query("PRAGMA FOREIGN_KEYS=true");
+
+ smol::block_on(builder.build()).unwrap();
+ }
+}
@@ -0,0 +1,60 @@
+use anyhow::{Context, Result};
+
+use crate::{
+ bindable::{Bind, Column},
+ connection::Connection,
+ statement::Statement,
+};
+
+impl Connection {
+ pub fn exec<'a>(&'a self, query: &str) -> Result<impl 'a + FnMut() -> Result<()>> {
+ let mut statement = Statement::prepare(&self, query)?;
+ Ok(move || statement.exec())
+ }
+
+ pub fn exec_bound<'a, B: Bind>(
+ &'a self,
+ query: &str,
+ ) -> Result<impl 'a + FnMut(B) -> Result<()>> {
+ let mut statement = Statement::prepare(&self, query)?;
+ Ok(move |bindings| statement.with_bindings(bindings)?.exec())
+ }
+
+ pub fn select<'a, C: Column>(
+ &'a self,
+ query: &str,
+ ) -> Result<impl 'a + FnMut() -> Result<Vec<C>>> {
+ let mut statement = Statement::prepare(&self, query)?;
+ Ok(move || statement.rows::<C>())
+ }
+
+ pub fn select_bound<'a, B: Bind, C: Column>(
+ &'a self,
+ query: &str,
+ ) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
+ let mut statement = Statement::prepare(&self, query)?;
+ Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
+ }
+
+ pub fn select_row<'a, C: Column>(
+ &'a self,
+ query: &str,
+ ) -> Result<impl 'a + FnMut() -> Result<Option<C>>> {
+ let mut statement = Statement::prepare(&self, query)?;
+ Ok(move || statement.maybe_row::<C>())
+ }
+
+ pub fn select_row_bound<'a, B: Bind, C: Column>(
+ &'a self,
+ query: &str,
+ ) -> Result<impl 'a + FnMut(B) -> Result<Option<C>>> {
+ let mut statement = Statement::prepare(&self, query)?;
+ Ok(move |bindings| {
+ statement
+ .with_bindings(bindings)
+ .context("Bindings failed")?
+ .maybe_row::<C>()
+ .context("Maybe row failed")
+ })
+ }
+}
@@ -0,0 +1,32 @@
+use std::ops::Deref;
+use std::sync::mpsc::Sender;
+
+use parking_lot::Mutex;
+use thread_local::ThreadLocal;
+
+/// Unbounded standard library sender which is stored per thread to get around
+/// the lack of sync on the standard library version while still being unbounded
+/// Note: this locks on the cloneable sender, but its done once per thread, so it
+/// shouldn't result in too much contention
+pub struct UnboundedSyncSender<T: Send> {
+ clonable_sender: Mutex<Sender<T>>,
+ local_senders: ThreadLocal<Sender<T>>,
+}
+
+impl<T: Send> UnboundedSyncSender<T> {
+ pub fn new(sender: Sender<T>) -> Self {
+ Self {
+ clonable_sender: Mutex::new(sender),
+ local_senders: ThreadLocal::new(),
+ }
+ }
+}
+
+impl<T: Send> Deref for UnboundedSyncSender<T> {
+ type Target = Sender<T>;
+
+ fn deref(&self) -> &Self::Target {
+ self.local_senders
+ .get_or(|| self.clonable_sender.lock().clone())
+ }
+}
@@ -0,0 +1,17 @@
+[package]
+name = "sqlez_macros"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/sqlez_macros.rs"
+proc-macro = true
+doctest = false
+
+[dependencies]
+syn = "1.0"
+quote = "1.0"
+proc-macro2 = "1.0"
+lazy_static = "1.4"
+sqlez = { path = "../sqlez" }
+sqlformat = "0.2"
@@ -0,0 +1,93 @@
+use proc_macro::{Delimiter, Span, TokenStream, TokenTree};
+use sqlez::thread_safe_connection::{locking_queue, ThreadSafeConnection};
+use syn::Error;
+
+lazy_static::lazy_static! {
+ static ref SQLITE: ThreadSafeConnection = {
+ ThreadSafeConnection::new(":memory:", false, None, Some(locking_queue()))
+ };
+}
+
+#[proc_macro]
+pub fn sql(tokens: TokenStream) -> TokenStream {
+ let (spans, sql) = make_sql(tokens);
+
+ let error = SQLITE.sql_has_syntax_error(sql.trim());
+ let formatted_sql = sqlformat::format(&sql, &sqlformat::QueryParams::None, Default::default());
+
+ if let Some((error, error_offset)) = error {
+ create_error(spans, error_offset, error, &formatted_sql)
+ } else {
+ format!("r#\"{}\"#", &formatted_sql).parse().unwrap()
+ }
+}
+
+fn create_error(
+ spans: Vec<(usize, Span)>,
+ error_offset: usize,
+ error: String,
+ formatted_sql: &String,
+) -> TokenStream {
+ let error_span = spans
+ .into_iter()
+ .skip_while(|(offset, _)| offset <= &error_offset)
+ .map(|(_, span)| span)
+ .next()
+ .unwrap_or(Span::call_site());
+ let error_text = format!("Sql Error: {}\nFor Query: {}", error, formatted_sql);
+ TokenStream::from(Error::new(error_span.into(), error_text).into_compile_error())
+}
+
+fn make_sql(tokens: TokenStream) -> (Vec<(usize, Span)>, String) {
+ let mut sql_tokens = vec![];
+ flatten_stream(tokens.clone(), &mut sql_tokens);
+ // Lookup of spans by offset at the end of the token
+ let mut spans: Vec<(usize, Span)> = Vec::new();
+ let mut sql = String::new();
+ for (token_text, span) in sql_tokens {
+ sql.push_str(&token_text);
+ spans.push((sql.len(), span));
+ }
+ (spans, sql)
+}
+
+/// This method exists to normalize the representation of groups
+/// to always include spaces between tokens. This is why we don't use the usual .to_string().
+/// This allows our token search in token_at_offset to resolve
+/// ambiguity of '(tokens)' vs. '( token )', due to sqlite requiring byte offsets
+fn flatten_stream(tokens: TokenStream, result: &mut Vec<(String, Span)>) {
+ for token_tree in tokens.into_iter() {
+ match token_tree {
+ TokenTree::Group(group) => {
+ // push open delimiter
+ result.push((open_delimiter(group.delimiter()), group.span()));
+ // recurse
+ flatten_stream(group.stream(), result);
+ // push close delimiter
+ result.push((close_delimiter(group.delimiter()), group.span()));
+ }
+ TokenTree::Ident(ident) => {
+ result.push((format!("{} ", ident.to_string()), ident.span()));
+ }
+ leaf_tree => result.push((leaf_tree.to_string(), leaf_tree.span())),
+ }
+ }
+}
+
+fn open_delimiter(delimiter: Delimiter) -> String {
+ match delimiter {
+ Delimiter::Parenthesis => "( ".to_string(),
+ Delimiter::Brace => "[ ".to_string(),
+ Delimiter::Bracket => "{ ".to_string(),
+ Delimiter::None => "".to_string(),
+ }
+}
+
+fn close_delimiter(delimiter: Delimiter) -> String {
+ match delimiter {
+ Delimiter::Parenthesis => " ) ".to_string(),
+ Delimiter::Brace => " ] ".to_string(),
+ Delimiter::Bracket => " } ".to_string(),
+ Delimiter::None => "".to_string(),
+ }
+}
@@ -17,6 +17,7 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
+db = { path = "../db" }
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
smallvec = { version = "1.6", features = ["union"] }
@@ -0,0 +1,52 @@
+use std::path::PathBuf;
+
+use db::{define_connection, query, sqlez_macros::sql};
+
+use workspace::{ItemId, WorkspaceDb, WorkspaceId};
+
+define_connection! {
+ pub static ref TERMINAL_CONNECTION: TerminalDb<WorkspaceDb> =
+ &[sql!(
+ CREATE TABLE terminals (
+ workspace_id INTEGER,
+ item_id INTEGER UNIQUE,
+ working_directory BLOB,
+ PRIMARY KEY(workspace_id, item_id),
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ) STRICT;
+ )];
+}
+
+impl TerminalDb {
+ query! {
+ pub async fn update_workspace_id(
+ new_id: WorkspaceId,
+ old_id: WorkspaceId,
+ item_id: ItemId
+ ) -> Result<()> {
+ UPDATE terminals
+ SET workspace_id = ?
+ WHERE workspace_id = ? AND item_id = ?
+ }
+ }
+
+ query! {
+ pub async fn save_working_directory(
+ item_id: ItemId,
+ workspace_id: WorkspaceId,
+ working_directory: PathBuf
+ ) -> Result<()> {
+ INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory)
+ VALUES (?, ?, ?)
+ }
+ }
+
+ query! {
+ pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
+ SELECT working_directory
+ FROM terminals
+ WHERE item_id = ? AND workspace_id = ?
+ }
+ }
+}
@@ -1,4 +1,5 @@
pub mod mappings;
+mod persistence;
pub mod terminal_container_view;
pub mod terminal_element;
pub mod terminal_view;
@@ -32,9 +33,11 @@ use mappings::mouse::{
alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
};
+use persistence::TERMINAL_CONNECTION;
use procinfo::LocalProcessInfo;
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use util::ResultExt;
+use workspace::{ItemId, WorkspaceId};
use std::{
cmp::min,
@@ -54,7 +57,8 @@ use gpui::{
geometry::vector::{vec2f, Vector2F},
keymap::Keystroke,
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
- ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
+ AppContext, ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent,
+ MutableAppContext, Task,
};
use crate::mappings::{
@@ -281,6 +285,8 @@ impl TerminalBuilder {
blink_settings: Option<TerminalBlink>,
alternate_scroll: &AlternateScroll,
window_id: usize,
+ item_id: ItemId,
+ workspace_id: WorkspaceId,
) -> Result<TerminalBuilder> {
let pty_config = {
let alac_shell = shell.clone().and_then(|shell| match shell {
@@ -385,6 +391,8 @@ impl TerminalBuilder {
last_mouse_position: None,
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
+ workspace_id,
+ item_id,
};
Ok(TerminalBuilder {
@@ -528,6 +536,8 @@ pub struct Terminal {
scroll_px: f32,
next_link_id: usize,
selection_phase: SelectionPhase,
+ workspace_id: WorkspaceId,
+ item_id: ItemId,
}
impl Terminal {
@@ -567,7 +577,21 @@ impl Terminal {
cx.emit(Event::Wakeup);
if self.update_process_info() {
- cx.emit(Event::TitleChanged)
+ cx.emit(Event::TitleChanged);
+
+ if let Some(foreground_info) = &self.foreground_process_info {
+ let cwd = foreground_info.cwd.clone();
+ let item_id = self.item_id;
+ let workspace_id = self.workspace_id;
+ cx.background()
+ .spawn(async move {
+ TERMINAL_CONNECTION
+ .save_working_directory(item_id, workspace_id, cwd)
+ .await
+ .log_err();
+ })
+ .detach();
+ }
}
}
AlacTermEvent::ColorRequest(idx, fun_ptr) => {
@@ -875,10 +899,6 @@ impl Terminal {
return;
};
- if self.update_process_info() {
- cx.emit(Event::TitleChanged);
- }
-
//Note that the ordering of events matters for event processing
while let Some(e) = self.events.pop_front() {
self.process_terminal_event(&e, &mut terminal, cx)
@@ -1174,6 +1194,21 @@ impl Terminal {
}
}
+ pub fn set_workspace_id(&mut self, id: WorkspaceId, cx: &AppContext) {
+ let old_workspace_id = self.workspace_id;
+ let item_id = self.item_id;
+ cx.background()
+ .spawn(async move {
+ TERMINAL_CONNECTION
+ .update_workspace_id(id, old_workspace_id, item_id)
+ .await
+ .log_err()
+ })
+ .detach();
+
+ self.workspace_id = id;
+ }
+
pub fn find_matches(
&mut self,
query: project::search::SearchQuery,
@@ -1,15 +1,20 @@
+use crate::persistence::TERMINAL_CONNECTION;
use crate::terminal_view::TerminalView;
-use crate::{Event, Terminal, TerminalBuilder, TerminalError};
+use crate::{Event, TerminalBuilder, TerminalError};
use alacritty_terminal::index::Point;
use dirs::home_dir;
use gpui::{
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task,
- View, ViewContext, ViewHandle,
+ View, ViewContext, ViewHandle, WeakViewHandle,
};
-use util::truncate_and_trailoff;
+use util::{truncate_and_trailoff, ResultExt};
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
-use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
+use workspace::{
+ item::{Item, ItemEvent},
+ ToolbarItemLocation, Workspace,
+};
+use workspace::{register_deserializable_item, Pane, WorkspaceId};
use project::{LocalWorktree, Project, ProjectPath};
use settings::{AlternateScroll, Settings, WorkingDirectory};
@@ -23,6 +28,8 @@ actions!(terminal, [DeployModal]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(TerminalContainer::deploy);
+
+ register_deserializable_item::<TerminalContainer>(cx);
}
//Make terminal view an enum, that can give you views for the error and non-error states
@@ -75,7 +82,9 @@ impl TerminalContainer {
.unwrap_or(WorkingDirectory::CurrentProjectDirectory);
let working_directory = get_working_directory(workspace, cx, strategy);
- let view = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
+ let view = cx.add_view(|cx| {
+ TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
+ });
workspace.add_item(Box::new(view), cx);
}
@@ -83,6 +92,7 @@ impl TerminalContainer {
pub fn new(
working_directory: Option<PathBuf>,
modal: bool,
+ workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Self {
let settings = cx.global::<Settings>();
@@ -109,10 +119,13 @@ impl TerminalContainer {
settings.terminal_overrides.blinking.clone(),
scroll,
cx.window_id(),
+ cx.view_id(),
+ workspace_id,
) {
Ok(terminal) => {
let terminal = cx.add_model(|cx| terminal.subscribe(cx));
let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
+
cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event))
.detach();
TerminalContainerContent::Connected(view)
@@ -124,7 +137,6 @@ impl TerminalContainer {
TerminalContainerContent::Error(view)
}
};
- cx.focus(content.handle());
TerminalContainer {
content,
@@ -132,18 +144,6 @@ impl TerminalContainer {
}
}
- pub fn from_terminal(
- terminal: ModelHandle<Terminal>,
- modal: bool,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let connected_view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
- TerminalContainer {
- content: TerminalContainerContent::Connected(connected_view),
- associated_directory: None,
- }
- }
-
fn connected(&self) -> Option<ViewHandle<TerminalView>> {
match &self.content {
TerminalContainerContent::Connected(vh) => Some(vh.clone()),
@@ -271,13 +271,18 @@ impl Item for TerminalContainer {
.boxed()
}
- fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
+ fn clone_on_split(
+ &self,
+ workspace_id: WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Self> {
//From what I can tell, there's no way to tell the current working
//Directory of the terminal from outside the shell. There might be
//solutions to this, but they are non-trivial and require more IPC
Some(TerminalContainer::new(
self.associated_directory.clone(),
false,
+ workspace_id,
cx,
))
}
@@ -372,6 +377,36 @@ impl Item for TerminalContainer {
)
.boxed()])
}
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("Terminal")
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ workspace_id: workspace::WorkspaceId,
+ item_id: workspace::ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, workspace_id);
+ Task::ready(Ok(cx.add_view(|cx| {
+ TerminalContainer::new(
+ working_directory.log_err().flatten(),
+ false,
+ workspace_id,
+ cx,
+ )
+ })))
+ }
+
+ fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+ if let Some(connected) = self.connected() {
+ let id = workspace.database_id();
+ let terminal_handle = connected.read(cx).terminal().clone();
+ terminal_handle.update(cx, |terminal, cx| terminal.set_workspace_id(id, cx))
+ }
+ }
}
impl SearchableItem for TerminalContainer {
@@ -28,9 +28,15 @@ impl<'a> TerminalTestContext<'a> {
let params = self.cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], self.cx).await;
- let (_, workspace) = self
- .cx
- .add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
+ let (_, workspace) = self.cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
(project, workspace)
}
@@ -31,6 +31,7 @@ pub struct Theme {
pub shared_screen: ContainerStyle,
pub contact_notification: ContactNotification,
pub update_notification: UpdateNotification,
+ pub simple_message_notification: MessageNotification,
pub project_shared_notification: ProjectSharedNotification,
pub incoming_call_notification: IncomingCallNotification,
pub tooltip: TooltipStyle,
@@ -478,6 +479,13 @@ pub struct UpdateNotification {
pub dismiss_button: Interactive<IconButton>,
}
+#[derive(Deserialize, Default)]
+pub struct MessageNotification {
+ pub message: ContainedText,
+ pub action_message: Interactive<ContainedText>,
+ pub dismiss_button: Interactive<IconButton>,
+}
+
#[derive(Deserialize, Default)]
pub struct ProjectSharedNotification {
pub window_height: f32,
@@ -6,18 +6,24 @@ use gpui::{
Padding, ParentElement,
},
fonts::TextStyle,
- Border, Element, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
+ Border, Element, Entity, ModelHandle, MutableAppContext, Quad, RenderContext, Task, View,
+ ViewContext, ViewHandle, WeakViewHandle,
};
use project::{Project, ProjectEntryId, ProjectPath};
use settings::Settings;
use smallvec::SmallVec;
use theme::{ColorScheme, Layer, Style, StyleSet};
-use workspace::{Item, Workspace};
+use workspace::{
+ item::{Item, ItemEvent},
+ register_deserializable_item, Pane, Workspace,
+};
actions!(theme, [DeployThemeTestbench]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ThemeTestbench::deploy);
+
+ register_deserializable_item::<ThemeTestbench>(cx)
}
pub struct ThemeTestbench {}
@@ -351,7 +357,21 @@ impl Item for ThemeTestbench {
gpui::Task::ready(Ok(()))
}
- fn to_item_events(_: &Self::Event) -> Vec<workspace::ItemEvent> {
+ fn to_item_events(_: &Self::Event) -> Vec<ItemEvent> {
Vec::new()
}
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("ThemeTestBench")
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: workspace::WorkspaceId,
+ _item_id: workspace::ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<gpui::anyhow::Result<ViewHandle<Self>>> {
+ Task::ready(Ok(cx.add_view(|_| Self {})))
+ }
}
@@ -19,6 +19,7 @@ rand = { workspace = true }
tempdir = { version = "0.3.7", optional = true }
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
git2 = { version = "0.15", default-features = false, optional = true }
+dirs = "3.0"
[dev-dependencies]
@@ -0,0 +1,40 @@
+use std::env;
+
+use lazy_static::lazy_static;
+
+lazy_static! {
+ pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
+ .unwrap_or(include_str!("../../zed/RELEASE_CHANNEL").to_string());
+ pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
+ "dev" => ReleaseChannel::Dev,
+ "preview" => ReleaseChannel::Preview,
+ "stable" => ReleaseChannel::Stable,
+ _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME),
+ };
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Default)]
+pub enum ReleaseChannel {
+ #[default]
+ Dev,
+ Preview,
+ Stable,
+}
+
+impl ReleaseChannel {
+ pub fn display_name(&self) -> &'static str {
+ match self {
+ ReleaseChannel::Dev => "Zed Dev",
+ ReleaseChannel::Preview => "Zed Preview",
+ ReleaseChannel::Stable => "Zed",
+ }
+ }
+
+ pub fn dev_name(&self) -> &'static str {
+ match self {
+ ReleaseChannel::Dev => "dev",
+ ReleaseChannel::Preview => "preview",
+ ReleaseChannel::Stable => "stable",
+ }
+ }
+}
@@ -1,3 +1,5 @@
+pub mod channel;
+pub mod paths;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
@@ -204,6 +206,30 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
}
}
+// 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
+pub fn unzip_option<T, U>(option: Option<(T, U)>) -> (Option<T>, Option<U>) {
+ match option {
+ Some((a, b)) => (Some(a), Some(b)),
+ None => (None, None),
+ }
+}
+
+#[macro_export]
+macro_rules! iife {
+ ($block:block) => {
+ (|| $block)()
+ };
+}
+
+#[macro_export]
+macro_rules! async_iife {
+ ($block:block) => {
+ (|| async move { $block })()
+ };
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -221,4 +247,18 @@ mod tests {
extend_sorted(&mut vec, vec![1000, 19, 17, 9, 5], 8, |a, b| b.cmp(a));
assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]);
}
+
+ #[test]
+ fn test_iife() {
+ fn option_returning_function() -> Option<()> {
+ None
+ }
+
+ let foo = iife!({
+ option_returning_function()?;
+ Some(())
+ });
+
+ assert_eq!(foo, None);
+ }
}
@@ -41,8 +41,15 @@ impl<'a> VimTestContext<'a> {
.insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
.await;
- let (window_id, workspace) =
- cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
+ let (window_id, workspace) = cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
// Setup search toolbars
workspace.update(cx, |workspace, cx| {
@@ -18,6 +18,7 @@ test-support = [
]
[dependencies]
+db = { path = "../db" }
call = { path = "../call" }
client = { path = "../client" }
collections = { path = "../collections" }
@@ -31,14 +32,19 @@ project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
+async-recursion = "1.0.0"
+bincode = "1.2.1"
anyhow = "1.0.38"
futures = "0.3"
+lazy_static = "1.4"
+env_logger = "0.9.1"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
smallvec = { version = "1.6", features = ["union"] }
+indoc = "1.0.4"
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }
@@ -47,3 +53,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
+db = { path = "../db", features = ["test-support"] }
@@ -98,14 +98,14 @@ pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
}
impl DockPosition {
- fn is_visible(&self) -> bool {
+ pub fn is_visible(&self) -> bool {
match self {
DockPosition::Shown(_) => true,
DockPosition::Hidden(_) => false,
}
}
- fn anchor(&self) -> DockAnchor {
+ pub fn anchor(&self) -> DockAnchor {
match self {
DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
}
@@ -137,9 +137,10 @@ pub struct Dock {
}
impl Dock {
- pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
- let anchor = cx.global::<Settings>().default_dock_anchor;
- let pane = cx.add_view(|cx| Pane::new(Some(anchor), cx));
+ pub fn new(default_item_factory: DefaultItemFactory, cx: &mut ViewContext<Workspace>) -> Self {
+ let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
+
+ let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx));
pane.update(cx, |pane, cx| {
pane.set_active(false, cx);
});
@@ -152,7 +153,7 @@ impl Dock {
Self {
pane,
panel_sizes: Default::default(),
- position: DockPosition::Hidden(anchor),
+ position,
default_item_factory,
}
}
@@ -169,21 +170,26 @@ impl Dock {
self.position.is_visible() && self.position.anchor() == anchor
}
- fn set_dock_position(
+ pub(crate) fn set_dock_position(
workspace: &mut Workspace,
new_position: DockPosition,
cx: &mut ViewContext<Workspace>,
) {
+ dbg!("starting", &new_position);
workspace.dock.position = new_position;
// Tell the pane about the new anchor position
workspace.dock.pane.update(cx, |pane, cx| {
+ dbg!("setting docked");
pane.set_docked(Some(new_position.anchor()), cx)
});
if workspace.dock.position.is_visible() {
+ dbg!("dock is visible");
// Close the right sidebar if the dock is on the right side and the right sidebar is open
if workspace.dock.position.anchor() == DockAnchor::Right {
+ dbg!("dock anchor is right");
if workspace.right_sidebar().read(cx).is_open() {
+ dbg!("Toggling right sidebar");
workspace.toggle_sidebar(SidebarSide::Right, cx);
}
}
@@ -193,8 +199,10 @@ impl Dock {
if pane.read(cx).items().next().is_none() {
let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
// Adding the item focuses the pane by default
+ dbg!("Adding item to dock");
Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
} else {
+ dbg!("just focusing dock");
cx.focus(pane);
}
} else if let Some(last_active_center_pane) = workspace
@@ -205,6 +213,8 @@ impl Dock {
cx.focus(last_active_center_pane);
}
cx.emit(crate::Event::DockAnchorChanged);
+ workspace.serialize_workspace(cx);
+ dbg!("Serializing workspace after dock position changed");
cx.notify();
}
@@ -341,6 +351,10 @@ impl Dock {
}
})
}
+
+ pub fn position(&self) -> DockPosition {
+ self.position
+ }
}
pub struct ToggleDockButton {
@@ -454,7 +468,7 @@ mod tests {
use settings::Settings;
use super::*;
- use crate::{sidebar::Sidebar, tests::TestItem, ItemHandle, Workspace};
+ use crate::{item::test::TestItem, sidebar::Sidebar, ItemHandle, Workspace};
pub fn default_item_factory(
_workspace: &mut Workspace,
@@ -568,8 +582,9 @@ mod tests {
cx.update(|cx| init(cx));
let project = Project::test(fs, [], cx).await;
- let (window_id, workspace) =
- cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
+ let (window_id, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, default_item_factory, cx)
+ });
workspace.update(cx, |workspace, cx| {
let left_panel = cx.add_view(|_| TestItem::new());
@@ -0,0 +1,905 @@
+use std::{
+ any::{Any, TypeId},
+ borrow::Cow,
+ cell::RefCell,
+ fmt,
+ path::PathBuf,
+ rc::Rc,
+ sync::atomic::{AtomicBool, Ordering},
+ time::Duration,
+};
+
+use anyhow::Result;
+use client::proto;
+use gpui::{
+ AnyViewHandle, AppContext, ElementBox, ModelHandle, MutableAppContext, Task, View, ViewContext,
+ ViewHandle, WeakViewHandle,
+};
+use project::{Project, ProjectEntryId, ProjectPath};
+use settings::{Autosave, Settings};
+use smallvec::SmallVec;
+use theme::Theme;
+use util::ResultExt;
+
+use crate::{
+ pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
+ FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+};
+
+#[derive(Eq, PartialEq, Hash)]
+pub enum ItemEvent {
+ CloseItem,
+ UpdateTab,
+ UpdateBreadcrumbs,
+ Edit,
+}
+
+pub trait Item: View {
+ fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
+ fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
+ fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+ false
+ }
+ fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+ None
+ }
+ fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
+ -> ElementBox;
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+ fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
+ fn is_singleton(&self, cx: &AppContext) -> bool;
+ fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
+ fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ None
+ }
+ fn is_dirty(&self, _: &AppContext) -> bool {
+ false
+ }
+ fn has_conflict(&self, _: &AppContext) -> bool {
+ false
+ }
+ fn can_save(&self, cx: &AppContext) -> bool;
+ fn save(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>>;
+ fn save_as(
+ &mut self,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>>;
+ fn reload(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>>;
+ fn git_diff_recalc(
+ &mut self,
+ _project: ModelHandle<Project>,
+ _cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ Task::ready(Ok(()))
+ }
+ fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
+ fn should_close_item_on_event(_: &Self::Event) -> bool {
+ false
+ }
+ fn should_update_tab_on_event(_: &Self::Event) -> bool {
+ false
+ }
+ fn is_edit_event(_: &Self::Event) -> bool {
+ false
+ }
+ fn act_as_type(
+ &self,
+ type_id: TypeId,
+ self_handle: &ViewHandle<Self>,
+ _: &AppContext,
+ ) -> Option<AnyViewHandle> {
+ if TypeId::of::<Self>() == type_id {
+ Some(self_handle.into())
+ } else {
+ None
+ }
+ }
+ fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ None
+ }
+
+ fn breadcrumb_location(&self) -> ToolbarItemLocation {
+ ToolbarItemLocation::Hidden
+ }
+
+ fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<ElementBox>> {
+ None
+ }
+
+ fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
+
+ fn serialized_item_kind() -> Option<&'static str>;
+
+ fn deserialize(
+ project: ModelHandle<Project>,
+ workspace: WeakViewHandle<Workspace>,
+ workspace_id: WorkspaceId,
+ item_id: ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<Result<ViewHandle<Self>>>;
+}
+
+pub trait ItemHandle: 'static + fmt::Debug {
+ fn subscribe_to_item_events(
+ &self,
+ cx: &mut MutableAppContext,
+ handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
+ ) -> gpui::Subscription;
+ fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
+ fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
+ -> ElementBox;
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+ fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
+ fn is_singleton(&self, cx: &AppContext) -> bool;
+ fn boxed_clone(&self) -> Box<dyn ItemHandle>;
+ fn clone_on_split(
+ &self,
+ workspace_id: WorkspaceId,
+ cx: &mut MutableAppContext,
+ ) -> Option<Box<dyn ItemHandle>>;
+ fn added_to_pane(
+ &self,
+ workspace: &mut Workspace,
+ pane: ViewHandle<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ );
+ fn deactivated(&self, cx: &mut MutableAppContext);
+ fn workspace_deactivated(&self, cx: &mut MutableAppContext);
+ fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
+ fn id(&self) -> usize;
+ fn window_id(&self) -> usize;
+ fn to_any(&self) -> AnyViewHandle;
+ fn is_dirty(&self, cx: &AppContext) -> bool;
+ fn has_conflict(&self, cx: &AppContext) -> bool;
+ fn can_save(&self, cx: &AppContext) -> bool;
+ fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
+ fn save_as(
+ &self,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
+ cx: &mut MutableAppContext,
+ ) -> Task<Result<()>>;
+ fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
+ -> Task<Result<()>>;
+ fn git_diff_recalc(
+ &self,
+ project: ModelHandle<Project>,
+ cx: &mut MutableAppContext,
+ ) -> Task<Result<()>>;
+ fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
+ fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
+ fn on_release(
+ &self,
+ cx: &mut MutableAppContext,
+ callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+ ) -> gpui::Subscription;
+ fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
+ fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
+ fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
+ fn serialized_item_kind(&self) -> Option<&'static str>;
+}
+
+pub trait WeakItemHandle {
+ fn id(&self) -> usize;
+ fn window_id(&self) -> usize;
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
+}
+
+impl dyn ItemHandle {
+ pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
+ self.to_any().downcast()
+ }
+
+ pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
+ self.act_as_type(TypeId::of::<T>(), cx)
+ .and_then(|t| t.downcast())
+ }
+}
+
+impl<T: Item> ItemHandle for ViewHandle<T> {
+ fn subscribe_to_item_events(
+ &self,
+ cx: &mut MutableAppContext,
+ handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
+ ) -> gpui::Subscription {
+ cx.subscribe(self, move |_, event, cx| {
+ for item_event in T::to_item_events(event) {
+ handler(item_event, cx)
+ }
+ })
+ }
+
+ fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+ self.read(cx).tab_description(detail, cx)
+ }
+
+ fn tab_content(
+ &self,
+ detail: Option<usize>,
+ style: &theme::Tab,
+ cx: &AppContext,
+ ) -> ElementBox {
+ self.read(cx).tab_content(detail, style, cx)
+ }
+
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
+ self.read(cx).project_path(cx)
+ }
+
+ fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
+ self.read(cx).project_entry_ids(cx)
+ }
+
+ fn is_singleton(&self, cx: &AppContext) -> bool {
+ self.read(cx).is_singleton(cx)
+ }
+
+ fn boxed_clone(&self) -> Box<dyn ItemHandle> {
+ Box::new(self.clone())
+ }
+
+ fn clone_on_split(
+ &self,
+ workspace_id: WorkspaceId,
+ cx: &mut MutableAppContext,
+ ) -> Option<Box<dyn ItemHandle>> {
+ self.update(cx, |item, cx| {
+ cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
+ })
+ .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
+ }
+
+ fn added_to_pane(
+ &self,
+ workspace: &mut Workspace,
+ pane: ViewHandle<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let history = pane.read(cx).nav_history_for_item(self);
+ self.update(cx, |this, cx| {
+ this.set_nav_history(history, cx);
+ this.added_to_workspace(workspace, cx);
+ });
+
+ if let Some(followed_item) = self.to_followable_item_handle(cx) {
+ if let Some(message) = followed_item.to_state_proto(cx) {
+ workspace.update_followers(
+ proto::update_followers::Variant::CreateView(proto::View {
+ id: followed_item.id() as u64,
+ variant: Some(message),
+ leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
+ }),
+ cx,
+ );
+ }
+ }
+
+ if workspace
+ .panes_by_item
+ .insert(self.id(), pane.downgrade())
+ .is_none()
+ {
+ let mut pending_autosave = DelayedDebouncedEditAction::new();
+ let mut pending_git_update = DelayedDebouncedEditAction::new();
+ let pending_update = Rc::new(RefCell::new(None));
+ let pending_update_scheduled = Rc::new(AtomicBool::new(false));
+
+ let mut event_subscription =
+ Some(cx.subscribe(self, move |workspace, item, event, cx| {
+ let pane = if let Some(pane) = workspace
+ .panes_by_item
+ .get(&item.id())
+ .and_then(|pane| pane.upgrade(cx))
+ {
+ pane
+ } else {
+ log::error!("unexpected item event after pane was dropped");
+ return;
+ };
+
+ if let Some(item) = item.to_followable_item_handle(cx) {
+ let leader_id = workspace.leader_for_pane(&pane);
+
+ if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
+ workspace.unfollow(&pane, cx);
+ }
+
+ if item.add_event_to_update_proto(
+ event,
+ &mut *pending_update.borrow_mut(),
+ cx,
+ ) && !pending_update_scheduled.load(Ordering::SeqCst)
+ {
+ pending_update_scheduled.store(true, Ordering::SeqCst);
+ cx.after_window_update({
+ let pending_update = pending_update.clone();
+ let pending_update_scheduled = pending_update_scheduled.clone();
+ move |this, cx| {
+ pending_update_scheduled.store(false, Ordering::SeqCst);
+ this.update_followers(
+ proto::update_followers::Variant::UpdateView(
+ proto::UpdateView {
+ id: item.id() as u64,
+ variant: pending_update.borrow_mut().take(),
+ leader_id: leader_id.map(|id| id.0),
+ },
+ ),
+ cx,
+ );
+ }
+ });
+ }
+ }
+
+ for item_event in T::to_item_events(event).into_iter() {
+ match item_event {
+ ItemEvent::CloseItem => {
+ Pane::close_item(workspace, pane, item.id(), cx)
+ .detach_and_log_err(cx);
+ return;
+ }
+
+ ItemEvent::UpdateTab => {
+ pane.update(cx, |_, cx| {
+ cx.emit(pane::Event::ChangeItemTitle);
+ cx.notify();
+ });
+ }
+
+ ItemEvent::Edit => {
+ if let Autosave::AfterDelay { milliseconds } =
+ cx.global::<Settings>().autosave
+ {
+ let delay = Duration::from_millis(milliseconds);
+ let item = item.clone();
+ pending_autosave.fire_new(
+ delay,
+ workspace,
+ cx,
+ |project, mut cx| async move {
+ cx.update(|cx| Pane::autosave_item(&item, project, cx))
+ .await
+ .log_err();
+ },
+ );
+ }
+
+ let settings = cx.global::<Settings>();
+ let debounce_delay = settings.git_overrides.gutter_debounce;
+
+ let item = item.clone();
+
+ if let Some(delay) = debounce_delay {
+ const MIN_GIT_DELAY: u64 = 50;
+
+ let delay = delay.max(MIN_GIT_DELAY);
+ let duration = Duration::from_millis(delay);
+
+ pending_git_update.fire_new(
+ duration,
+ workspace,
+ cx,
+ |project, mut cx| async move {
+ cx.update(|cx| item.git_diff_recalc(project, cx))
+ .await
+ .log_err();
+ },
+ );
+ } else {
+ let project = workspace.project().downgrade();
+ cx.spawn_weak(|_, mut cx| async move {
+ if let Some(project) = project.upgrade(&cx) {
+ cx.update(|cx| item.git_diff_recalc(project, cx))
+ .await
+ .log_err();
+ }
+ })
+ .detach();
+ }
+ }
+
+ _ => {}
+ }
+ }
+ }));
+
+ cx.observe_focus(self, move |workspace, item, focused, cx| {
+ if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
+ Pane::autosave_item(&item, workspace.project.clone(), cx)
+ .detach_and_log_err(cx);
+ }
+ })
+ .detach();
+
+ let item_id = self.id();
+ cx.observe_release(self, move |workspace, _, _| {
+ workspace.panes_by_item.remove(&item_id);
+ event_subscription.take();
+ })
+ .detach();
+ }
+
+ cx.defer(|workspace, cx| {
+ workspace.serialize_workspace(cx);
+ });
+ }
+
+ fn deactivated(&self, cx: &mut MutableAppContext) {
+ self.update(cx, |this, cx| this.deactivated(cx));
+ }
+
+ fn workspace_deactivated(&self, cx: &mut MutableAppContext) {
+ self.update(cx, |this, cx| this.workspace_deactivated(cx));
+ }
+
+ fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
+ self.update(cx, |this, cx| this.navigate(data, cx))
+ }
+
+ fn id(&self) -> usize {
+ self.id()
+ }
+
+ fn window_id(&self) -> usize {
+ self.window_id()
+ }
+
+ fn to_any(&self) -> AnyViewHandle {
+ self.into()
+ }
+
+ fn is_dirty(&self, cx: &AppContext) -> bool {
+ self.read(cx).is_dirty(cx)
+ }
+
+ fn has_conflict(&self, cx: &AppContext) -> bool {
+ self.read(cx).has_conflict(cx)
+ }
+
+ fn can_save(&self, cx: &AppContext) -> bool {
+ self.read(cx).can_save(cx)
+ }
+
+ fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
+ self.update(cx, |item, cx| item.save(project, cx))
+ }
+
+ fn save_as(
+ &self,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
+ cx: &mut MutableAppContext,
+ ) -> Task<anyhow::Result<()>> {
+ self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
+ }
+
+ fn reload(
+ &self,
+ project: ModelHandle<Project>,
+ cx: &mut MutableAppContext,
+ ) -> Task<Result<()>> {
+ self.update(cx, |item, cx| item.reload(project, cx))
+ }
+
+ fn git_diff_recalc(
+ &self,
+ project: ModelHandle<Project>,
+ cx: &mut MutableAppContext,
+ ) -> Task<Result<()>> {
+ self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
+ }
+
+ fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
+ self.read(cx).act_as_type(type_id, self, cx)
+ }
+
+ fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
+ if cx.has_global::<FollowableItemBuilders>() {
+ let builders = cx.global::<FollowableItemBuilders>();
+ let item = self.to_any();
+ Some(builders.get(&item.view_type())?.1(item))
+ } else {
+ None
+ }
+ }
+
+ fn on_release(
+ &self,
+ cx: &mut MutableAppContext,
+ callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+ ) -> gpui::Subscription {
+ cx.observe_release(self, move |_, cx| callback(cx))
+ }
+
+ fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+ self.read(cx).as_searchable(self)
+ }
+
+ fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
+ self.read(cx).breadcrumb_location()
+ }
+
+ fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
+ self.read(cx).breadcrumbs(theme, cx)
+ }
+
+ fn serialized_item_kind(&self) -> Option<&'static str> {
+ T::serialized_item_kind()
+ }
+}
+
+impl From<Box<dyn ItemHandle>> for AnyViewHandle {
+ fn from(val: Box<dyn ItemHandle>) -> Self {
+ val.to_any()
+ }
+}
+
+impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
+ fn from(val: &Box<dyn ItemHandle>) -> Self {
+ val.to_any()
+ }
+}
+
+impl Clone for Box<dyn ItemHandle> {
+ fn clone(&self) -> Box<dyn ItemHandle> {
+ self.boxed_clone()
+ }
+}
+
+impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
+ fn id(&self) -> usize {
+ self.id()
+ }
+
+ fn window_id(&self) -> usize {
+ self.window_id()
+ }
+
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
+ self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
+ }
+}
+
+pub trait ProjectItem: Item {
+ type Item: project::Item;
+
+ fn for_project_item(
+ project: ModelHandle<Project>,
+ item: ModelHandle<Self::Item>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self;
+}
+
+pub trait FollowableItem: Item {
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn from_state_proto(
+ pane: ViewHandle<Pane>,
+ project: ModelHandle<Project>,
+ state: &mut Option<proto::view::Variant>,
+ cx: &mut MutableAppContext,
+ ) -> Option<Task<Result<ViewHandle<Self>>>>;
+ fn add_event_to_update_proto(
+ &self,
+ event: &Self::Event,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool;
+ fn apply_update_proto(
+ &mut self,
+ message: proto::update_view::Variant,
+ cx: &mut ViewContext<Self>,
+ ) -> Result<()>;
+
+ fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
+ fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
+}
+
+pub trait FollowableItemHandle: ItemHandle {
+ fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn add_event_to_update_proto(
+ &self,
+ event: &dyn Any,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool;
+ fn apply_update_proto(
+ &self,
+ message: proto::update_view::Variant,
+ cx: &mut MutableAppContext,
+ ) -> Result<()>;
+ fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
+}
+
+impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
+ fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
+ self.update(cx, |this, cx| {
+ this.set_leader_replica_id(leader_replica_id, cx)
+ })
+ }
+
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+ self.read(cx).to_state_proto(cx)
+ }
+
+ fn add_event_to_update_proto(
+ &self,
+ event: &dyn Any,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool {
+ if let Some(event) = event.downcast_ref() {
+ self.read(cx).add_event_to_update_proto(event, update, cx)
+ } else {
+ false
+ }
+ }
+
+ fn apply_update_proto(
+ &self,
+ message: proto::update_view::Variant,
+ cx: &mut MutableAppContext,
+ ) -> Result<()> {
+ self.update(cx, |this, cx| this.apply_update_proto(message, cx))
+ }
+
+ fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
+ if let Some(event) = event.downcast_ref() {
+ T::should_unfollow_on_event(event, cx)
+ } else {
+ false
+ }
+ }
+}
+
+#[cfg(test)]
+pub(crate) mod test {
+ use std::{any::Any, borrow::Cow, cell::Cell};
+
+ use gpui::{
+ elements::Empty, AppContext, Element, ElementBox, Entity, ModelHandle, RenderContext, Task,
+ View, ViewContext, ViewHandle, WeakViewHandle,
+ };
+ use project::{Project, ProjectEntryId, ProjectPath};
+ use smallvec::SmallVec;
+
+ use crate::{sidebar::SidebarItem, ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
+
+ use super::{Item, ItemEvent};
+
+ pub struct TestItem {
+ pub state: String,
+ pub label: String,
+ pub save_count: usize,
+ pub save_as_count: usize,
+ pub reload_count: usize,
+ pub is_dirty: bool,
+ pub is_singleton: bool,
+ pub has_conflict: bool,
+ pub project_entry_ids: Vec<ProjectEntryId>,
+ pub project_path: Option<ProjectPath>,
+ pub nav_history: Option<ItemNavHistory>,
+ pub tab_descriptions: Option<Vec<&'static str>>,
+ pub tab_detail: Cell<Option<usize>>,
+ }
+
+ pub enum TestItemEvent {
+ Edit,
+ }
+
+ impl Clone for TestItem {
+ fn clone(&self) -> Self {
+ Self {
+ state: self.state.clone(),
+ label: self.label.clone(),
+ save_count: self.save_count,
+ save_as_count: self.save_as_count,
+ reload_count: self.reload_count,
+ is_dirty: self.is_dirty,
+ is_singleton: self.is_singleton,
+ has_conflict: self.has_conflict,
+ project_entry_ids: self.project_entry_ids.clone(),
+ project_path: self.project_path.clone(),
+ nav_history: None,
+ tab_descriptions: None,
+ tab_detail: Default::default(),
+ }
+ }
+ }
+
+ impl TestItem {
+ pub fn new() -> Self {
+ Self {
+ state: String::new(),
+ label: String::new(),
+ save_count: 0,
+ save_as_count: 0,
+ reload_count: 0,
+ is_dirty: false,
+ has_conflict: false,
+ project_entry_ids: Vec::new(),
+ project_path: None,
+ is_singleton: true,
+ nav_history: None,
+ tab_descriptions: None,
+ tab_detail: Default::default(),
+ }
+ }
+
+ pub fn with_label(mut self, state: &str) -> Self {
+ self.label = state.to_string();
+ self
+ }
+
+ pub fn with_singleton(mut self, singleton: bool) -> Self {
+ self.is_singleton = singleton;
+ self
+ }
+
+ pub fn with_project_entry_ids(mut self, project_entry_ids: &[u64]) -> Self {
+ self.project_entry_ids.extend(
+ project_entry_ids
+ .iter()
+ .copied()
+ .map(ProjectEntryId::from_proto),
+ );
+ self
+ }
+
+ pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
+ self.push_to_nav_history(cx);
+ self.state = state;
+ }
+
+ fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(history) = &mut self.nav_history {
+ history.push(Some(Box::new(self.state.clone())), cx);
+ }
+ }
+ }
+
+ impl Entity for TestItem {
+ type Event = TestItemEvent;
+ }
+
+ impl View for TestItem {
+ fn ui_name() -> &'static str {
+ "TestItem"
+ }
+
+ fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+ Empty::new().boxed()
+ }
+ }
+
+ impl Item for TestItem {
+ fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+ self.tab_descriptions.as_ref().and_then(|descriptions| {
+ let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
+ Some(description.into())
+ })
+ }
+
+ fn tab_content(&self, detail: Option<usize>, _: &theme::Tab, _: &AppContext) -> ElementBox {
+ self.tab_detail.set(detail);
+ Empty::new().boxed()
+ }
+
+ fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
+ self.project_path.clone()
+ }
+
+ fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
+ self.project_entry_ids.iter().copied().collect()
+ }
+
+ fn is_singleton(&self, _: &AppContext) -> bool {
+ self.is_singleton
+ }
+
+ fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+ self.nav_history = Some(history);
+ }
+
+ fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+ let state = *state.downcast::<String>().unwrap_or_default();
+ if state != self.state {
+ self.state = state;
+ true
+ } else {
+ false
+ }
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ self.push_to_nav_history(cx);
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ _: &mut ViewContext<Self>,
+ ) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ Some(self.clone())
+ }
+
+ fn is_dirty(&self, _: &AppContext) -> bool {
+ self.is_dirty
+ }
+
+ fn has_conflict(&self, _: &AppContext) -> bool {
+ self.has_conflict
+ }
+
+ fn can_save(&self, _: &AppContext) -> bool {
+ !self.project_entry_ids.is_empty()
+ }
+
+ fn save(
+ &mut self,
+ _: ModelHandle<Project>,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.save_count += 1;
+ self.is_dirty = false;
+ Task::ready(Ok(()))
+ }
+
+ fn save_as(
+ &mut self,
+ _: ModelHandle<Project>,
+ _: std::path::PathBuf,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.save_as_count += 1;
+ self.is_dirty = false;
+ Task::ready(Ok(()))
+ }
+
+ fn reload(
+ &mut self,
+ _: ModelHandle<Project>,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.reload_count += 1;
+ self.is_dirty = false;
+ Task::ready(Ok(()))
+ }
+
+ fn to_item_events(_: &Self::Event) -> Vec<ItemEvent> {
+ vec![ItemEvent::UpdateTab, ItemEvent::Edit]
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ None
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: WorkspaceId,
+ _item_id: ItemId,
+ _cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ unreachable!("Cannot deserialize test item")
+ }
+ }
+
+ impl SidebarItem for TestItem {}
+}
@@ -0,0 +1,280 @@
+use std::{any::TypeId, ops::DerefMut};
+
+use collections::HashSet;
+use gpui::{AnyViewHandle, Entity, MutableAppContext, View, ViewContext, ViewHandle};
+
+use crate::Workspace;
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.set_global(NotificationTracker::new());
+ simple_message_notification::init(cx);
+}
+
+pub trait Notification: View {
+ fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
+}
+
+pub trait NotificationHandle {
+ fn id(&self) -> usize;
+ fn to_any(&self) -> AnyViewHandle;
+}
+
+impl<T: Notification> NotificationHandle for ViewHandle<T> {
+ fn id(&self) -> usize {
+ self.id()
+ }
+
+ fn to_any(&self) -> AnyViewHandle {
+ self.into()
+ }
+}
+
+impl From<&dyn NotificationHandle> for AnyViewHandle {
+ fn from(val: &dyn NotificationHandle) -> Self {
+ val.to_any()
+ }
+}
+
+struct NotificationTracker {
+ notifications_sent: HashSet<TypeId>,
+}
+
+impl std::ops::Deref for NotificationTracker {
+ type Target = HashSet<TypeId>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.notifications_sent
+ }
+}
+
+impl DerefMut for NotificationTracker {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.notifications_sent
+ }
+}
+
+impl NotificationTracker {
+ fn new() -> Self {
+ Self {
+ notifications_sent: HashSet::default(),
+ }
+ }
+}
+
+impl Workspace {
+ pub fn show_notification_once<V: Notification>(
+ &mut self,
+ id: usize,
+ cx: &mut ViewContext<Self>,
+ build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
+ ) {
+ if !cx
+ .global::<NotificationTracker>()
+ .contains(&TypeId::of::<V>())
+ {
+ cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
+ tracker.insert(TypeId::of::<V>())
+ });
+
+ self.show_notification::<V>(id, cx, build_notification)
+ }
+ }
+
+ pub fn show_notification<V: Notification>(
+ &mut self,
+ id: usize,
+ cx: &mut ViewContext<Self>,
+ build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
+ ) {
+ let type_id = TypeId::of::<V>();
+ if self
+ .notifications
+ .iter()
+ .all(|(existing_type_id, existing_id, _)| {
+ (*existing_type_id, *existing_id) != (type_id, id)
+ })
+ {
+ let notification = build_notification(cx);
+ cx.subscribe(¬ification, move |this, handle, event, cx| {
+ if handle.read(cx).should_dismiss_notification_on_event(event) {
+ this.dismiss_notification(type_id, id, cx);
+ }
+ })
+ .detach();
+ self.notifications
+ .push((type_id, id, Box::new(notification)));
+ cx.notify();
+ }
+ }
+
+ fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
+ self.notifications
+ .retain(|(existing_type_id, existing_id, _)| {
+ if (*existing_type_id, *existing_id) == (type_id, id) {
+ cx.notify();
+ false
+ } else {
+ true
+ }
+ });
+ }
+}
+
+pub mod simple_message_notification {
+ use std::process::Command;
+
+ use gpui::{
+ actions,
+ elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
+ impl_actions, Action, CursorStyle, Element, Entity, MouseButton, MutableAppContext, View,
+ ViewContext,
+ };
+ use menu::Cancel;
+ use serde::Deserialize;
+ use settings::Settings;
+
+ use crate::Workspace;
+
+ use super::Notification;
+
+ actions!(message_notifications, [CancelMessageNotification]);
+
+ #[derive(Clone, Default, Deserialize, PartialEq)]
+ pub struct OsOpen(pub String);
+
+ impl_actions!(message_notifications, [OsOpen]);
+
+ pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(MessageNotification::dismiss);
+ cx.add_action(
+ |_workspace: &mut Workspace, open_action: &OsOpen, _cx: &mut ViewContext<Workspace>| {
+ #[cfg(target_os = "macos")]
+ {
+ let mut command = Command::new("open");
+ command.arg(open_action.0.clone());
+
+ command.spawn().ok();
+ }
+ },
+ )
+ }
+
+ pub struct MessageNotification {
+ message: String,
+ click_action: Box<dyn Action>,
+ click_message: String,
+ }
+
+ pub enum MessageNotificationEvent {
+ Dismiss,
+ }
+
+ impl Entity for MessageNotification {
+ type Event = MessageNotificationEvent;
+ }
+
+ impl MessageNotification {
+ pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
+ message: S1,
+ click_action: A,
+ click_message: S2,
+ ) -> Self {
+ Self {
+ message: message.as_ref().to_string(),
+ click_action: Box::new(click_action) as Box<dyn Action>,
+ click_message: click_message.as_ref().to_string(),
+ }
+ }
+
+ pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
+ cx.emit(MessageNotificationEvent::Dismiss);
+ }
+ }
+
+ impl View for MessageNotification {
+ fn ui_name() -> &'static str {
+ "MessageNotification"
+ }
+
+ fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+ let theme = cx.global::<Settings>().theme.clone();
+ let theme = &theme.update_notification;
+
+ enum MessageNotificationTag {}
+
+ let click_action = self.click_action.boxed_clone();
+ let click_message = self.click_message.clone();
+ let message = self.message.clone();
+
+ MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
+ Flex::column()
+ .with_child(
+ Flex::row()
+ .with_child(
+ Text::new(message, theme.message.text.clone())
+ .contained()
+ .with_style(theme.message.container)
+ .aligned()
+ .top()
+ .left()
+ .flex(1., true)
+ .boxed(),
+ )
+ .with_child(
+ MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
+ let style = theme.dismiss_button.style_for(state, false);
+ Svg::new("icons/x_mark_8.svg")
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_width(style.button_width)
+ .with_height(style.button_width)
+ .boxed()
+ })
+ .with_padding(Padding::uniform(5.))
+ .on_click(MouseButton::Left, move |_, cx| {
+ cx.dispatch_action(CancelMessageNotification)
+ })
+ .aligned()
+ .constrained()
+ .with_height(
+ cx.font_cache().line_height(theme.message.text.font_size),
+ )
+ .aligned()
+ .top()
+ .flex_float()
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_child({
+ let style = theme.action_message.style_for(state, false);
+
+ Text::new(click_message, style.text.clone())
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ })
+ .contained()
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, cx| {
+ cx.dispatch_any_action(click_action.boxed_clone())
+ })
+ .boxed()
+ }
+ }
+
+ impl Notification for MessageNotification {
+ fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
+ match event {
+ MessageNotificationEvent::Dismiss => true,
+ }
+ }
+ }
+}
@@ -3,8 +3,9 @@ mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection};
use crate::{
dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
+ item::WeakItemHandle,
toolbar::Toolbar,
- Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
+ Item, NewFile, NewSearch, NewTerminal, Workspace,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
@@ -1634,7 +1635,7 @@ mod tests {
use std::sync::Arc;
use super::*;
- use crate::tests::TestItem;
+ use crate::item::test::TestItem;
use gpui::{executor::Deterministic, TestAppContext};
use project::FakeFs;
@@ -1645,8 +1646,9 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// 1. Add with a destination index
@@ -1734,8 +1736,9 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// 1. Add with a destination index
@@ -1811,8 +1814,9 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// singleton view
@@ -1922,7 +1926,7 @@ mod tests {
let project = Project::test(fs, None, cx).await;
let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ cx.add_window(|cx| Workspace::new(None, 0, project, |_, _| unimplemented!(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
add_labled_item(&workspace, &pane, "A", cx);
@@ -13,10 +13,14 @@ use theme::Theme;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PaneGroup {
- root: Member,
+ pub(crate) root: Member,
}
impl PaneGroup {
+ pub(crate) fn with_root(root: Member) -> Self {
+ Self { root }
+ }
+
pub fn new(pane: ViewHandle<Pane>) -> Self {
Self {
root: Member::Pane(pane),
@@ -85,7 +89,7 @@ impl PaneGroup {
}
#[derive(Clone, Debug, Eq, PartialEq)]
-enum Member {
+pub(crate) enum Member {
Axis(PaneAxis),
Pane(ViewHandle<Pane>),
}
@@ -276,9 +280,9 @@ impl Member {
}
#[derive(Clone, Debug, Eq, PartialEq)]
-struct PaneAxis {
- axis: Axis,
- members: Vec<Member>,
+pub(crate) struct PaneAxis {
+ pub axis: Axis,
+ pub members: Vec<Member>,
}
impl PaneAxis {
@@ -0,0 +1,836 @@
+#![allow(dead_code)]
+
+pub mod model;
+
+use std::path::Path;
+
+use anyhow::{anyhow, bail, Context, Result};
+use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
+use gpui::Axis;
+
+use util::{iife, unzip_option, ResultExt};
+
+use crate::dock::DockPosition;
+use crate::WorkspaceId;
+
+use model::{
+ GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
+ WorkspaceLocation,
+};
+
+define_connection! {
+ pub static ref DB: WorkspaceDb<()> =
+ &[sql!(
+ CREATE TABLE workspaces(
+ workspace_id INTEGER PRIMARY KEY,
+ workspace_location BLOB UNIQUE,
+ dock_visible INTEGER, // Boolean
+ dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded'
+ dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet
+ left_sidebar_open INTEGER, //Boolean
+ timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
+ ) STRICT;
+
+ CREATE TABLE pane_groups(
+ group_id INTEGER PRIMARY KEY,
+ workspace_id INTEGER NOT NULL,
+ parent_group_id INTEGER, // NULL indicates that this is a root node
+ position INTEGER, // NULL indicates that this is a root node
+ axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
+ ) STRICT;
+
+ CREATE TABLE panes(
+ pane_id INTEGER PRIMARY KEY,
+ workspace_id INTEGER NOT NULL,
+ active INTEGER NOT NULL, // Boolean
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+ ) STRICT;
+
+ CREATE TABLE center_panes(
+ pane_id INTEGER PRIMARY KEY,
+ parent_group_id INTEGER, // NULL means that this is a root pane
+ position INTEGER, // NULL means that this is a root pane
+ FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
+ ON DELETE CASCADE,
+ FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
+ ) STRICT;
+
+ CREATE TABLE items(
+ item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
+ workspace_id INTEGER NOT NULL,
+ pane_id INTEGER NOT NULL,
+ kind TEXT NOT NULL,
+ position INTEGER NOT NULL,
+ active INTEGER NOT NULL,
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
+ ON DELETE CASCADE,
+ PRIMARY KEY(item_id, workspace_id)
+ ) STRICT;
+ )];
+}
+
+impl WorkspaceDb {
+ /// Returns a serialized workspace for the given worktree_roots. If the passed array
+ /// is empty, the most recent workspace is returned instead. If no workspace for the
+ /// passed roots is stored, returns none.
+ pub fn workspace_for_roots<P: AsRef<Path>>(
+ &self,
+ worktree_roots: &[P],
+ ) -> Option<SerializedWorkspace> {
+ let workspace_location: WorkspaceLocation = worktree_roots.into();
+
+ // Note that we re-assign the workspace_id here in case it's empty
+ // and we've grabbed the most recent workspace
+ let (workspace_id, workspace_location, left_sidebar_open, dock_position): (
+ WorkspaceId,
+ WorkspaceLocation,
+ bool,
+ DockPosition,
+ ) = iife!({
+ if worktree_roots.len() == 0 {
+ self.select_row(sql!(
+ SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
+ FROM workspaces
+ ORDER BY timestamp DESC LIMIT 1))?()?
+ } else {
+ self.select_row_bound(sql!(
+ SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
+ FROM workspaces
+ WHERE workspace_location = ?))?(&workspace_location)?
+ }
+ .context("No workspaces found")
+ })
+ .warn_on_err()
+ .flatten()?;
+
+ Some(SerializedWorkspace {
+ id: workspace_id,
+ location: workspace_location.clone(),
+ dock_pane: self
+ .get_dock_pane(workspace_id)
+ .context("Getting dock pane")
+ .log_err()?,
+ center_group: self
+ .get_center_pane_group(workspace_id)
+ .context("Getting center group")
+ .log_err()?,
+ dock_position,
+ left_sidebar_open
+ })
+ }
+
+ /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
+ /// that used this workspace previously
+ pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
+ self.write(move |conn| {
+ conn.with_savepoint("update_worktrees", || {
+ // Clear out panes and pane_groups
+ conn.exec_bound(sql!(
+ UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
+ DELETE FROM pane_groups WHERE workspace_id = ?1;
+ DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
+ .expect("Clearing old panes");
+
+ conn.exec_bound(sql!(
+ DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
+ ))?((&workspace.location, workspace.id.clone()))
+ .context("clearing out old locations")?;
+
+ // Upsert
+ conn.exec_bound(sql!(
+ INSERT INTO workspaces(
+ workspace_id,
+ workspace_location,
+ left_sidebar_open,
+ dock_visible,
+ dock_anchor,
+ timestamp
+ )
+ VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP)
+ ON CONFLICT DO
+ UPDATE SET
+ workspace_location = ?2,
+ left_sidebar_open = ?3,
+ dock_visible = ?4,
+ dock_anchor = ?5,
+ timestamp = CURRENT_TIMESTAMP
+ ))?((workspace.id, &workspace.location, workspace.left_sidebar_open, workspace.dock_position))
+ .context("Updating workspace")?;
+
+ // Save center pane group and dock pane
+ Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
+ .context("save pane group in save workspace")?;
+
+ let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true)
+ .context("save pane in save workspace")?;
+
+ // Complete workspace initialization
+ conn.exec_bound(sql!(
+ UPDATE workspaces
+ SET dock_pane = ?
+ WHERE workspace_id = ?
+ ))?((dock_id, workspace.id))
+ .context("Finishing initialization with dock pane")?;
+
+ Ok(())
+ })
+ .log_err();
+ })
+ .await;
+ }
+
+ query! {
+ pub async fn next_id() -> Result<WorkspaceId> {
+ INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
+ }
+ }
+
+ query! {
+ pub fn recent_workspaces(limit: usize) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
+ SELECT workspace_id, workspace_location
+ FROM workspaces
+ WHERE workspace_location IS NOT NULL
+ ORDER BY timestamp DESC
+ LIMIT ?
+ }
+ }
+
+ fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
+ self.get_pane_group(workspace_id, None)?
+ .into_iter()
+ .next()
+ .context("No center pane group")
+ }
+
+ fn get_pane_group(
+ &self,
+ workspace_id: WorkspaceId,
+ group_id: Option<GroupId>,
+ ) -> Result<Vec<SerializedPaneGroup>> {
+ type GroupKey = (Option<GroupId>, WorkspaceId);
+ type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
+ self.select_bound::<GroupKey, GroupOrPane>(sql!(
+ SELECT group_id, axis, pane_id, active
+ FROM (SELECT
+ group_id,
+ axis,
+ NULL as pane_id,
+ NULL as active,
+ position,
+ parent_group_id,
+ workspace_id
+ FROM pane_groups
+ UNION
+ SELECT
+ NULL,
+ NULL,
+ center_panes.pane_id,
+ panes.active as active,
+ position,
+ parent_group_id,
+ panes.workspace_id as workspace_id
+ FROM center_panes
+ JOIN panes ON center_panes.pane_id = panes.pane_id)
+ WHERE parent_group_id IS ? AND workspace_id = ?
+ ORDER BY position
+ ))?((group_id, workspace_id))?
+ .into_iter()
+ .map(|(group_id, axis, pane_id, active)| {
+ if let Some((group_id, axis)) = group_id.zip(axis) {
+ Ok(SerializedPaneGroup::Group {
+ axis,
+ children: self.get_pane_group(workspace_id, Some(group_id))?,
+ })
+ } else if let Some((pane_id, active)) = pane_id.zip(active) {
+ Ok(SerializedPaneGroup::Pane(SerializedPane::new(
+ self.get_items(pane_id)?,
+ active,
+ )))
+ } else {
+ bail!("Pane Group Child was neither a pane group or a pane");
+ }
+ })
+ // Filter out panes and pane groups which don't have any children or items
+ .filter(|pane_group| match pane_group {
+ Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
+ Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
+ _ => true,
+ })
+ .collect::<Result<_>>()
+ }
+
+
+ fn save_pane_group(
+ conn: &Connection,
+ workspace_id: WorkspaceId,
+ pane_group: &SerializedPaneGroup,
+ parent: Option<(GroupId, usize)>,
+ ) -> Result<()> {
+ match pane_group {
+ SerializedPaneGroup::Group { axis, children } => {
+ let (parent_id, position) = unzip_option(parent);
+
+ let group_id = conn.select_row_bound::<_, i64>(sql!(
+ INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
+ VALUES (?, ?, ?, ?)
+ RETURNING group_id
+ ))?((
+ workspace_id,
+ parent_id,
+ position,
+ *axis,
+ ))?
+ .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
+
+ for (position, group) in children.iter().enumerate() {
+ Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
+ }
+
+ Ok(())
+ }
+ SerializedPaneGroup::Pane(pane) => {
+ Self::save_pane(conn, workspace_id, &pane, parent, false)?;
+ Ok(())
+ }
+ }
+ }
+
+ fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
+ let (pane_id, active) = self.select_row_bound(sql!(
+ SELECT pane_id, active
+ FROM panes
+ WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
+ ))?(
+ workspace_id,
+ )?
+ .context("No dock pane for workspace")?;
+
+ Ok(SerializedPane::new(
+ self.get_items(pane_id).context("Reading items")?,
+ active,
+ ))
+ }
+
+ fn save_pane(
+ conn: &Connection,
+ workspace_id: WorkspaceId,
+ pane: &SerializedPane,
+ parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
+ dock: bool,
+ ) -> Result<PaneId> {
+ let pane_id = conn.select_row_bound::<_, i64>(sql!(
+ INSERT INTO panes(workspace_id, active)
+ VALUES (?, ?)
+ RETURNING pane_id
+ ))?((workspace_id, pane.active))?
+ .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
+
+ if !dock {
+ let (parent_id, order) = unzip_option(parent);
+ conn.exec_bound(sql!(
+ INSERT INTO center_panes(pane_id, parent_group_id, position)
+ VALUES (?, ?, ?)
+ ))?((pane_id, parent_id, order))?;
+ }
+
+ Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
+
+ Ok(pane_id)
+ }
+
+ fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
+ Ok(self.select_bound(sql!(
+ SELECT kind, item_id, active FROM items
+ WHERE pane_id = ?
+ ORDER BY position
+ ))?(pane_id)?)
+ }
+
+ fn save_items(
+ conn: &Connection,
+ workspace_id: WorkspaceId,
+ pane_id: PaneId,
+ items: &[SerializedItem],
+ ) -> Result<()> {
+ let mut insert = conn.exec_bound(sql!(
+ INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
+ )).context("Preparing insertion")?;
+ for (position, item) in items.iter().enumerate() {
+ insert((workspace_id, pane_id, position, item))?;
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use std::sync::Arc;
+
+ use db::open_test_db;
+ use settings::DockAnchor;
+
+ use super::*;
+
+ #[gpui::test]
+ async fn test_next_id_stability() {
+ env_logger::try_init().ok();
+
+ let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
+
+ db.write(|conn| {
+ conn.migrate(
+ "test_table",
+ &[sql!(
+ CREATE TABLE test_table(
+ text TEXT,
+ workspace_id INTEGER,
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ) STRICT;
+ )],
+ )
+ .unwrap();
+ })
+ .await;
+
+ let id = db.next_id().await.unwrap();
+ // Assert the empty row got inserted
+ assert_eq!(
+ Some(id),
+ db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
+ SELECT workspace_id FROM workspaces WHERE workspace_id = ?
+ ))
+ .unwrap()(id)
+ .unwrap()
+ );
+
+ db.write(move |conn| {
+ conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
+ .unwrap()(("test-text-1", id))
+ .unwrap()
+ })
+ .await;
+
+ let test_text_1 = db
+ .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
+ .unwrap()(1)
+ .unwrap()
+ .unwrap();
+ assert_eq!(test_text_1, "test-text-1");
+ }
+
+ #[gpui::test]
+ async fn test_workspace_id_stability() {
+ env_logger::try_init().ok();
+
+ let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
+
+ db.write(|conn| {
+ conn.migrate(
+ "test_table",
+ &[sql!(
+ CREATE TABLE test_table(
+ text TEXT,
+ workspace_id INTEGER,
+ FOREIGN KEY(workspace_id)
+ REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ) STRICT;)],
+ )
+ })
+ .await
+ .unwrap();
+
+ let mut workspace_1 = SerializedWorkspace {
+ id: 1,
+ location: (["/tmp", "/tmp2"]).into(),
+ dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
+ center_group: Default::default(),
+ dock_pane: Default::default(),
+ left_sidebar_open: true
+ };
+
+ let mut workspace_2 = SerializedWorkspace {
+ id: 2,
+ location: (["/tmp"]).into(),
+ dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
+ center_group: Default::default(),
+ dock_pane: Default::default(),
+ left_sidebar_open: false
+ };
+
+ db.save_workspace(workspace_1.clone()).await;
+
+ db.write(|conn| {
+ conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
+ .unwrap()(("test-text-1", 1))
+ .unwrap();
+ })
+ .await;
+
+ db.save_workspace(workspace_2.clone()).await;
+
+ db.write(|conn| {
+ conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
+ .unwrap()(("test-text-2", 2))
+ .unwrap();
+ })
+ .await;
+
+ workspace_1.location = (["/tmp", "/tmp3"]).into();
+ db.save_workspace(workspace_1.clone()).await;
+ db.save_workspace(workspace_1).await;
+
+ workspace_2.dock_pane.children.push(SerializedItem {
+ kind: Arc::from("Test"),
+ item_id: 10,
+ active: true,
+ });
+ db.save_workspace(workspace_2).await;
+
+ let test_text_2 = db
+ .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
+ .unwrap()(2)
+ .unwrap()
+ .unwrap();
+ assert_eq!(test_text_2, "test-text-2");
+
+ let test_text_1 = db
+ .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
+ .unwrap()(1)
+ .unwrap()
+ .unwrap();
+ assert_eq!(test_text_1, "test-text-1");
+ }
+
+ #[gpui::test]
+ async fn test_full_workspace_serialization() {
+ env_logger::try_init().ok();
+
+ let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
+
+ let dock_pane = crate::persistence::model::SerializedPane {
+ children: vec![
+ SerializedItem::new("Terminal", 1, false),
+ SerializedItem::new("Terminal", 2, false),
+ SerializedItem::new("Terminal", 3, true),
+ SerializedItem::new("Terminal", 4, false),
+ ],
+ active: false,
+ };
+
+ // -----------------
+ // | 1,2 | 5,6 |
+ // | - - - | |
+ // | 3,4 | |
+ // -----------------
+ let center_group = SerializedPaneGroup::Group {
+ axis: gpui::Axis::Horizontal,
+ children: vec![
+ SerializedPaneGroup::Group {
+ axis: gpui::Axis::Vertical,
+ children: vec![
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 5, false),
+ SerializedItem::new("Terminal", 6, true),
+ ],
+ false,
+ )),
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 7, true),
+ SerializedItem::new("Terminal", 8, false),
+ ],
+ false,
+ )),
+ ],
+ },
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 9, false),
+ SerializedItem::new("Terminal", 10, true),
+ ],
+ false,
+ )),
+ ],
+ };
+
+ let workspace = SerializedWorkspace {
+ id: 5,
+ location: (["/tmp", "/tmp2"]).into(),
+ dock_position: DockPosition::Shown(DockAnchor::Bottom),
+ center_group,
+ dock_pane,
+ left_sidebar_open: true
+ };
+
+ db.save_workspace(workspace.clone()).await;
+ let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
+
+ assert_eq!(workspace, round_trip_workspace.unwrap());
+
+ // Test guaranteed duplicate IDs
+ db.save_workspace(workspace.clone()).await;
+ db.save_workspace(workspace.clone()).await;
+
+ let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
+ assert_eq!(workspace, round_trip_workspace.unwrap());
+ }
+
+ #[gpui::test]
+ async fn test_workspace_assignment() {
+ env_logger::try_init().ok();
+
+ let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
+
+ let workspace_1 = SerializedWorkspace {
+ id: 1,
+ location: (["/tmp", "/tmp2"]).into(),
+ dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
+ center_group: Default::default(),
+ dock_pane: Default::default(),
+ left_sidebar_open: true,
+ };
+
+ let mut workspace_2 = SerializedWorkspace {
+ id: 2,
+ location: (["/tmp"]).into(),
+ dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
+ center_group: Default::default(),
+ dock_pane: Default::default(),
+ left_sidebar_open: false,
+ };
+
+ db.save_workspace(workspace_1.clone()).await;
+ db.save_workspace(workspace_2.clone()).await;
+
+ // Test that paths are treated as a set
+ assert_eq!(
+ db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
+ workspace_1
+ );
+ assert_eq!(
+ db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
+ workspace_1
+ );
+
+ // Make sure that other keys work
+ assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
+ assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
+
+ // Test 'mutate' case of updating a pre-existing id
+ workspace_2.location = (["/tmp", "/tmp2"]).into();
+
+ db.save_workspace(workspace_2.clone()).await;
+ assert_eq!(
+ db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
+ workspace_2
+ );
+
+ // Test other mechanism for mutating
+ let mut workspace_3 = SerializedWorkspace {
+ id: 3,
+ location: (&["/tmp", "/tmp2"]).into(),
+ dock_position: DockPosition::Shown(DockAnchor::Right),
+ center_group: Default::default(),
+ dock_pane: Default::default(),
+ left_sidebar_open: false
+ };
+
+ db.save_workspace(workspace_3.clone()).await;
+ assert_eq!(
+ db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
+ workspace_3
+ );
+
+ // Make sure that updating paths differently also works
+ workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
+ db.save_workspace(workspace_3.clone()).await;
+ assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
+ assert_eq!(
+ db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
+ .unwrap(),
+ workspace_3
+ );
+ }
+
+ use crate::dock::DockPosition;
+ use crate::persistence::model::SerializedWorkspace;
+ use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
+
+ fn default_workspace<P: AsRef<Path>>(
+ workspace_id: &[P],
+ dock_pane: SerializedPane,
+ center_group: &SerializedPaneGroup,
+ ) -> SerializedWorkspace {
+ SerializedWorkspace {
+ id: 4,
+ location: workspace_id.into(),
+ dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
+ center_group: center_group.clone(),
+ dock_pane,
+ left_sidebar_open: true
+ }
+ }
+
+ #[gpui::test]
+ async fn test_basic_dock_pane() {
+ env_logger::try_init().ok();
+
+ let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
+
+ let dock_pane = crate::persistence::model::SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 1, false),
+ SerializedItem::new("Terminal", 4, false),
+ SerializedItem::new("Terminal", 2, false),
+ SerializedItem::new("Terminal", 3, true),
+ ],
+ false,
+ );
+
+ let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
+
+ db.save_workspace(workspace.clone()).await;
+
+ let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
+
+ assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
+ }
+
+ #[gpui::test]
+ async fn test_simple_split() {
+ env_logger::try_init().ok();
+
+ let db = WorkspaceDb(open_test_db("simple_split").await);
+
+ // -----------------
+ // | 1,2 | 5,6 |
+ // | - - - | |
+ // | 3,4 | |
+ // -----------------
+ let center_pane = SerializedPaneGroup::Group {
+ axis: gpui::Axis::Horizontal,
+ children: vec![
+ SerializedPaneGroup::Group {
+ axis: gpui::Axis::Vertical,
+ children: vec![
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 1, false),
+ SerializedItem::new("Terminal", 2, true),
+ ],
+ false,
+ )),
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 4, false),
+ SerializedItem::new("Terminal", 3, true),
+ ],
+ true,
+ )),
+ ],
+ },
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 5, true),
+ SerializedItem::new("Terminal", 6, false),
+ ],
+ false,
+ )),
+ ],
+ };
+
+ let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
+
+ db.save_workspace(workspace.clone()).await;
+
+ let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
+
+ assert_eq!(workspace.center_group, new_workspace.center_group);
+ }
+
+ #[gpui::test]
+ async fn test_cleanup_panes() {
+ env_logger::try_init().ok();
+
+ let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
+
+ let center_pane = SerializedPaneGroup::Group {
+ axis: gpui::Axis::Horizontal,
+ children: vec![
+ SerializedPaneGroup::Group {
+ axis: gpui::Axis::Vertical,
+ children: vec![
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 1, false),
+ SerializedItem::new("Terminal", 2, true),
+ ],
+ false,
+ )),
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 4, false),
+ SerializedItem::new("Terminal", 3, true),
+ ],
+ true,
+ )),
+ ],
+ },
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 5, false),
+ SerializedItem::new("Terminal", 6, true),
+ ],
+ false,
+ )),
+ ],
+ };
+
+ let id = &["/tmp"];
+
+ let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
+
+ db.save_workspace(workspace.clone()).await;
+
+ workspace.center_group = SerializedPaneGroup::Group {
+ axis: gpui::Axis::Vertical,
+ children: vec![
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 1, false),
+ SerializedItem::new("Terminal", 2, true),
+ ],
+ false,
+ )),
+ SerializedPaneGroup::Pane(SerializedPane::new(
+ vec![
+ SerializedItem::new("Terminal", 4, true),
+ SerializedItem::new("Terminal", 3, false),
+ ],
+ true,
+ )),
+ ],
+ };
+
+ db.save_workspace(workspace.clone()).await;
+
+ let new_workspace = db.workspace_for_roots(id).unwrap();
+
+ assert_eq!(workspace.center_group, new_workspace.center_group);
+ }
+}
@@ -0,0 +1,315 @@
+use std::{
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+use anyhow::{Context, Result};
+
+use async_recursion::async_recursion;
+use gpui::{AsyncAppContext, Axis, ModelHandle, Task, ViewHandle};
+
+use db::sqlez::{
+ bindable::{Bind, Column},
+ statement::Statement,
+};
+use project::Project;
+use settings::DockAnchor;
+use util::ResultExt;
+
+use crate::{
+ dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
+
+impl WorkspaceLocation {
+ pub fn paths(&self) -> Arc<Vec<PathBuf>> {
+ self.0.clone()
+ }
+}
+
+impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
+ fn from(iterator: T) -> Self {
+ let mut roots = iterator
+ .into_iter()
+ .map(|p| p.as_ref().to_path_buf())
+ .collect::<Vec<_>>();
+ roots.sort();
+ Self(Arc::new(roots))
+ }
+}
+
+impl Bind for &WorkspaceLocation {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ bincode::serialize(&self.0)
+ .expect("Bincode serialization of paths should not fail")
+ .bind(statement, start_index)
+ }
+}
+
+impl Column for WorkspaceLocation {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let blob = statement.column_blob(start_index)?;
+ Ok((
+ WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
+ start_index + 1,
+ ))
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct SerializedWorkspace {
+ pub id: WorkspaceId,
+ pub location: WorkspaceLocation,
+ pub dock_position: DockPosition,
+ pub center_group: SerializedPaneGroup,
+ pub dock_pane: SerializedPane,
+ pub left_sidebar_open: bool,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum SerializedPaneGroup {
+ Group {
+ axis: Axis,
+ children: Vec<SerializedPaneGroup>,
+ },
+ Pane(SerializedPane),
+}
+
+#[cfg(test)]
+impl Default for SerializedPaneGroup {
+ fn default() -> Self {
+ Self::Pane(SerializedPane {
+ children: vec![SerializedItem::default()],
+ active: false,
+ })
+ }
+}
+
+impl SerializedPaneGroup {
+ #[async_recursion(?Send)]
+ pub(crate) async fn deserialize(
+ &self,
+ project: &ModelHandle<Project>,
+ workspace_id: WorkspaceId,
+ workspace: &ViewHandle<Workspace>,
+ cx: &mut AsyncAppContext,
+ ) -> Option<(Member, Option<ViewHandle<Pane>>)> {
+ match self {
+ SerializedPaneGroup::Group { axis, children } => {
+ let mut current_active_pane = None;
+ let mut members = Vec::new();
+ for child in children {
+ if let Some((new_member, active_pane)) = child
+ .deserialize(project, workspace_id, workspace, cx)
+ .await
+ {
+ members.push(new_member);
+
+ current_active_pane = current_active_pane.or(active_pane);
+ }
+ }
+
+ if members.is_empty() {
+ return None;
+ }
+
+ Some((
+ Member::Axis(PaneAxis {
+ axis: *axis,
+ members,
+ }),
+ current_active_pane,
+ ))
+ }
+ SerializedPaneGroup::Pane(serialized_pane) => {
+ let pane = workspace.update(cx, |workspace, cx| workspace.add_pane(cx));
+ let active = serialized_pane.active;
+ serialized_pane
+ .deserialize_to(project, &pane, workspace_id, workspace, cx)
+ .await;
+
+ if pane.read_with(cx, |pane, _| pane.items().next().is_some()) {
+ Some((Member::Pane(pane.clone()), active.then(|| pane)))
+ } else {
+ None
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Default, Clone)]
+pub struct SerializedPane {
+ pub(crate) active: bool,
+ pub(crate) children: Vec<SerializedItem>,
+}
+
+impl SerializedPane {
+ pub fn new(children: Vec<SerializedItem>, active: bool) -> Self {
+ SerializedPane { children, active }
+ }
+
+ pub async fn deserialize_to(
+ &self,
+ project: &ModelHandle<Project>,
+ pane_handle: &ViewHandle<Pane>,
+ workspace_id: WorkspaceId,
+ workspace: &ViewHandle<Workspace>,
+ cx: &mut AsyncAppContext,
+ ) {
+ let mut active_item_index = None;
+ for (index, item) in self.children.iter().enumerate() {
+ let project = project.clone();
+ let item_handle = pane_handle
+ .update(cx, |_, cx| {
+ if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
+ deserializer(
+ project,
+ workspace.downgrade(),
+ workspace_id,
+ item.item_id,
+ cx,
+ )
+ } else {
+ Task::ready(Err(anyhow::anyhow!(
+ "Deserializer does not exist for item kind: {}",
+ item.kind
+ )))
+ }
+ })
+ .await
+ .log_err();
+
+ if let Some(item_handle) = item_handle {
+ workspace.update(cx, |workspace, cx| {
+ Pane::add_item(workspace, &pane_handle, item_handle, false, false, None, cx);
+ })
+ }
+
+ if item.active {
+ active_item_index = Some(index);
+ }
+ }
+
+ if let Some(active_item_index) = active_item_index {
+ pane_handle.update(cx, |pane, cx| {
+ pane.activate_item(active_item_index, false, false, cx);
+ })
+ }
+ }
+}
+
+pub type GroupId = i64;
+pub type PaneId = i64;
+pub type ItemId = usize;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct SerializedItem {
+ pub kind: Arc<str>,
+ pub item_id: ItemId,
+ pub active: bool,
+}
+
+impl SerializedItem {
+ pub fn new(kind: impl AsRef<str>, item_id: ItemId, active: bool) -> Self {
+ Self {
+ kind: Arc::from(kind.as_ref()),
+ item_id,
+ active,
+ }
+ }
+}
+
+#[cfg(test)]
+impl Default for SerializedItem {
+ fn default() -> Self {
+ SerializedItem {
+ kind: Arc::from("Terminal"),
+ item_id: 100000,
+ active: false,
+ }
+ }
+}
+
+impl Bind for &SerializedItem {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = statement.bind(self.kind.clone(), start_index)?;
+ let next_index = statement.bind(self.item_id, next_index)?;
+ statement.bind(self.active, next_index)
+ }
+}
+
+impl Column for SerializedItem {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (kind, next_index) = Arc::<str>::column(statement, start_index)?;
+ let (item_id, next_index) = ItemId::column(statement, next_index)?;
+ let (active, next_index) = bool::column(statement, next_index)?;
+ Ok((
+ SerializedItem {
+ kind,
+ item_id,
+ active,
+ },
+ next_index,
+ ))
+ }
+}
+
+impl Bind for DockPosition {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = statement.bind(self.is_visible(), start_index)?;
+ statement.bind(self.anchor(), next_index)
+ }
+}
+
+impl Column for DockPosition {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (visible, next_index) = bool::column(statement, start_index)?;
+ let (dock_anchor, next_index) = DockAnchor::column(statement, next_index)?;
+ let position = if visible {
+ DockPosition::Shown(dock_anchor)
+ } else {
+ DockPosition::Hidden(dock_anchor)
+ };
+ Ok((position, next_index))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use db::sqlez::connection::Connection;
+ use settings::DockAnchor;
+
+ use super::WorkspaceLocation;
+
+ #[test]
+ fn test_workspace_round_trips() {
+ let db = Connection::open_memory(Some("workspace_id_round_trips"));
+
+ db.exec(indoc::indoc! {"
+ CREATE TABLE workspace_id_test(
+ workspace_id INTEGER,
+ dock_anchor TEXT
+ );"})
+ .unwrap()()
+ .unwrap();
+
+ let workspace_id: WorkspaceLocation = WorkspaceLocation::from(&["\test2", "\test1"]);
+
+ db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)")
+ .unwrap()((&workspace_id, DockAnchor::Bottom))
+ .unwrap();
+
+ assert_eq!(
+ db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
+ .unwrap()()
+ .unwrap(),
+ Some((
+ WorkspaceLocation::from(&["\test1", "\test2"]),
+ DockAnchor::Bottom
+ ))
+ );
+ }
+}
@@ -6,7 +6,7 @@ use gpui::{
};
use project::search::SearchQuery;
-use crate::{Item, ItemHandle, WeakItemHandle};
+use crate::{item::WeakItemHandle, Item, ItemHandle};
#[derive(Debug)]
pub enum SearchEvent {
@@ -1,4 +1,6 @@
-use crate::{Item, ItemNavHistory};
+use crate::{
+ item::ItemEvent, persistence::model::ItemId, Item, ItemNavHistory, Pane, Workspace, WorkspaceId,
+};
use anyhow::{anyhow, Result};
use call::participant::{Frame, RemoteVideoTrack};
use client::{PeerId, User};
@@ -6,8 +8,10 @@ use futures::StreamExt;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
- Entity, ModelHandle, MouseButton, RenderContext, Task, View, ViewContext,
+ Entity, ModelHandle, MouseButton, RenderContext, Task, View, ViewContext, ViewHandle,
+ WeakViewHandle,
};
+use project::Project;
use settings::Settings;
use smallvec::SmallVec;
use std::{
@@ -142,7 +146,11 @@ impl Item for SharedScreen {
self.nav_history = Some(history);
}
- fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Self> {
let track = self.track.upgrade()?;
Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
}
@@ -176,9 +184,23 @@ impl Item for SharedScreen {
Task::ready(Err(anyhow!("Item::reload called on SharedScreen")))
}
- fn to_item_events(event: &Self::Event) -> Vec<crate::ItemEvent> {
+ fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
match event {
- Event::Close => vec![crate::ItemEvent::CloseItem],
+ Event::Close => vec![ItemEvent::CloseItem],
}
}
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ None
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: WorkspaceId,
+ _item_id: ItemId,
+ _cx: &mut ViewContext<Pane>,
+ ) -> Task<Result<ViewHandle<Self>>> {
+ unreachable!("Shared screen can not be deserialized")
+ }
}
@@ -3,14 +3,26 @@
/// This may cause issues when you're trying to write tests that use workspace focus to add items at
/// specific locations.
pub mod dock;
+pub mod item;
+pub mod notifications;
pub mod pane;
pub mod pane_group;
+mod persistence;
pub mod searchable;
pub mod shared_screen;
pub mod sidebar;
mod status_bar;
mod toolbar;
+use std::{
+ any::TypeId,
+ borrow::Cow,
+ future::Future,
+ path::{Path, PathBuf},
+ sync::Arc,
+ time::Duration,
+};
+
use anyhow::{anyhow, Context, Result};
use call::ActiveCall;
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
@@ -28,56 +40,34 @@ use gpui::{
MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
+use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
use language::LanguageRegistry;
+
use log::{error, warn};
+use notifications::NotificationHandle;
pub use pane::*;
pub use pane_group::*;
+use persistence::{model::SerializedItem, DB};
+pub use persistence::{
+ model::{ItemId, WorkspaceLocation},
+ WorkspaceDb,
+};
use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
-use searchable::SearchableItemHandle;
use serde::Deserialize;
use settings::{Autosave, DockAnchor, Settings};
use shared_screen::SharedScreen;
use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
-use smallvec::SmallVec;
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
-use std::{
- any::{Any, TypeId},
- borrow::Cow,
- cell::RefCell,
- fmt,
- future::Future,
- path::{Path, PathBuf},
- rc::Rc,
- sync::{
- atomic::{AtomicBool, Ordering::SeqCst},
- Arc,
- },
- time::Duration,
-};
use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::ResultExt;
-type ProjectItemBuilders = HashMap<
- TypeId,
- fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
->;
-
-type FollowableItemBuilder = fn(
- ViewHandle<Pane>,
- ModelHandle<Project>,
- &mut Option<proto::view::Variant>,
- &mut MutableAppContext,
-) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
-type FollowableItemBuilders = HashMap<
- TypeId,
- (
- FollowableItemBuilder,
- fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
- ),
->;
+use crate::{
+ notifications::simple_message_notification::{MessageNotification, OsOpen},
+ persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
+};
#[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId);
@@ -147,6 +137,8 @@ pub struct OpenProjectEntryInPane {
project_entry: ProjectEntryId,
}
+pub type WorkspaceId = i64;
+
impl_internal_actions!(
workspace,
[
@@ -165,6 +157,7 @@ impl_actions!(workspace, [ActivatePane]);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
pane::init(cx);
dock::init(cx);
+ notifications::init(cx);
cx.add_global_action(open);
cx.add_global_action({
@@ -179,7 +172,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
let app_state = Arc::downgrade(&app_state);
move |_: &NewFile, cx: &mut MutableAppContext| {
if let Some(app_state) = app_state.upgrade() {
- open_new(&app_state, cx)
+ open_new(&app_state, cx).detach();
}
}
});
@@ -187,7 +180,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
let app_state = Arc::downgrade(&app_state);
move |_: &NewWindow, cx: &mut MutableAppContext| {
if let Some(app_state) = app_state.upgrade() {
- open_new(&app_state, cx)
+ open_new(&app_state, cx).detach();
}
}
});
@@ -306,6 +299,10 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
client.add_view_message_handler(Workspace::handle_update_followers);
}
+type ProjectItemBuilders = HashMap<
+ TypeId,
+ fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
+>;
pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
@@ -315,6 +312,19 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
});
}
+type FollowableItemBuilder = fn(
+ ViewHandle<Pane>,
+ ModelHandle<Project>,
+ &mut Option<proto::view::Variant>,
+ &mut MutableAppContext,
+) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
+type FollowableItemBuilders = HashMap<
+ TypeId,
+ (
+ FollowableItemBuilder,
+ fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
+ ),
+>;
pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
builders.insert(
@@ -332,6 +342,31 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
});
}
+type ItemDeserializers = HashMap<
+ Arc<str>,
+ fn(
+ ModelHandle<Project>,
+ WeakViewHandle<Workspace>,
+ WorkspaceId,
+ ItemId,
+ &mut ViewContext<Pane>,
+ ) -> Task<Result<Box<dyn ItemHandle>>>,
+>;
+pub fn register_deserializable_item<I: Item>(cx: &mut MutableAppContext) {
+ cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
+ if let Some(serialized_item_kind) = I::serialized_item_kind() {
+ deserializers.insert(
+ Arc::from(serialized_item_kind),
+ |project, workspace, workspace_id, item_id, cx| {
+ let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
+ cx.foreground()
+ .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
+ },
+ );
+ }
+ });
+}
+
pub struct AppState {
pub languages: Arc<LanguageRegistry>,
pub themes: Arc<ThemeRegistry>,
@@ -343,189 +378,32 @@ pub struct AppState {
pub default_item_factory: DefaultItemFactory,
}
-#[derive(Eq, PartialEq, Hash)]
-pub enum ItemEvent {
- CloseItem,
- UpdateTab,
- UpdateBreadcrumbs,
- Edit,
-}
-
-pub trait Item: View {
- fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
- fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
- fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
- false
- }
- fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
- None
- }
- fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
- -> ElementBox;
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
- fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
- fn is_singleton(&self, cx: &AppContext) -> bool;
- fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
- fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
- where
- Self: Sized,
- {
- None
- }
- fn is_dirty(&self, _: &AppContext) -> bool {
- false
- }
- fn has_conflict(&self, _: &AppContext) -> bool {
- false
- }
- fn can_save(&self, cx: &AppContext) -> bool;
- fn save(
- &mut self,
- project: ModelHandle<Project>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>>;
- fn save_as(
- &mut self,
- project: ModelHandle<Project>,
- abs_path: PathBuf,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>>;
- fn reload(
- &mut self,
- project: ModelHandle<Project>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>>;
- fn git_diff_recalc(
- &mut self,
- _project: ModelHandle<Project>,
- _cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- Task::ready(Ok(()))
- }
- fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
- fn should_close_item_on_event(_: &Self::Event) -> bool {
- false
- }
- fn should_update_tab_on_event(_: &Self::Event) -> bool {
- false
- }
- fn is_edit_event(_: &Self::Event) -> bool {
- false
- }
- fn act_as_type(
- &self,
- type_id: TypeId,
- self_handle: &ViewHandle<Self>,
- _: &AppContext,
- ) -> Option<AnyViewHandle> {
- if TypeId::of::<Self>() == type_id {
- Some(self_handle.into())
- } else {
- None
- }
- }
- fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- None
- }
-
- fn breadcrumb_location(&self) -> ToolbarItemLocation {
- ToolbarItemLocation::Hidden
- }
- fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<ElementBox>> {
- None
- }
-}
-
-pub trait ProjectItem: Item {
- type Item: project::Item;
-
- fn for_project_item(
- project: ModelHandle<Project>,
- item: ModelHandle<Self::Item>,
- cx: &mut ViewContext<Self>,
- ) -> Self;
-}
-
-pub trait FollowableItem: Item {
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
- fn from_state_proto(
- pane: ViewHandle<Pane>,
- project: ModelHandle<Project>,
- state: &mut Option<proto::view::Variant>,
- cx: &mut MutableAppContext,
- ) -> Option<Task<Result<ViewHandle<Self>>>>;
- fn add_event_to_update_proto(
- &self,
- event: &Self::Event,
- update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
- ) -> bool;
- fn apply_update_proto(
- &mut self,
- message: proto::update_view::Variant,
- cx: &mut ViewContext<Self>,
- ) -> Result<()>;
-
- fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
- fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
-}
+impl AppState {
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
+ use fs::HomeDir;
-pub trait FollowableItemHandle: ItemHandle {
- fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
- fn add_event_to_update_proto(
- &self,
- event: &dyn Any,
- update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
- ) -> bool;
- fn apply_update_proto(
- &self,
- message: proto::update_view::Variant,
- cx: &mut MutableAppContext,
- ) -> Result<()>;
- fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
-}
+ cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
+ let settings = Settings::test(cx);
+ cx.set_global(settings);
-impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
- fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
- self.update(cx, |this, cx| {
- this.set_leader_replica_id(leader_replica_id, cx)
+ let fs = fs::FakeFs::new(cx.background().clone());
+ let languages = Arc::new(LanguageRegistry::test());
+ let http_client = client::test::FakeHttpClient::with_404_response();
+ let client = Client::new(http_client.clone(), cx);
+ let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+ let themes = ThemeRegistry::new((), cx.font_cache().clone());
+ Arc::new(Self {
+ client,
+ themes,
+ fs,
+ languages,
+ user_store,
+ initialize_workspace: |_, _, _| {},
+ build_window_options: Default::default,
+ default_item_factory: |_, _| unimplemented!(),
})
}
-
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
- self.read(cx).to_state_proto(cx)
- }
-
- fn add_event_to_update_proto(
- &self,
- event: &dyn Any,
- update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
- ) -> bool {
- if let Some(event) = event.downcast_ref() {
- self.read(cx).add_event_to_update_proto(event, update, cx)
- } else {
- false
- }
- }
-
- fn apply_update_proto(
- &self,
- message: proto::update_view::Variant,
- cx: &mut MutableAppContext,
- ) -> Result<()> {
- self.update(cx, |this, cx| this.apply_update_proto(message, cx))
- }
-
- fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
- if let Some(event) = event.downcast_ref() {
- T::should_unfollow_on_event(event, cx)
- } else {
- false
- }
- }
}
struct DelayedDebouncedEditAction {
@@ -569,7 +447,7 @@ impl DelayedDebouncedEditAction {
futures::select_biased! {
_ = receiver => return,
- _ = timer => {}
+ _ = timer => {}
}
if let Some(project) = project.upgrade(&cx) {
@@ -579,478 +457,23 @@ impl DelayedDebouncedEditAction {
}
}
-pub trait ItemHandle: 'static + fmt::Debug {
- fn subscribe_to_item_events(
- &self,
- cx: &mut MutableAppContext,
- handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
- ) -> gpui::Subscription;
- fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
- fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
- -> ElementBox;
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
- fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
- fn is_singleton(&self, cx: &AppContext) -> bool;
- fn boxed_clone(&self) -> Box<dyn ItemHandle>;
- fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
- fn added_to_pane(
- &self,
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
- cx: &mut ViewContext<Workspace>,
- );
- fn deactivated(&self, cx: &mut MutableAppContext);
- fn workspace_deactivated(&self, cx: &mut MutableAppContext);
- fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
- fn id(&self) -> usize;
- fn window_id(&self) -> usize;
- fn to_any(&self) -> AnyViewHandle;
- fn is_dirty(&self, cx: &AppContext) -> bool;
- fn has_conflict(&self, cx: &AppContext) -> bool;
- fn can_save(&self, cx: &AppContext) -> bool;
- fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
- fn save_as(
- &self,
- project: ModelHandle<Project>,
- abs_path: PathBuf,
- cx: &mut MutableAppContext,
- ) -> Task<Result<()>>;
- fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
- -> Task<Result<()>>;
- fn git_diff_recalc(
- &self,
- project: ModelHandle<Project>,
- cx: &mut MutableAppContext,
- ) -> Task<Result<()>>;
- fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
- fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
- fn on_release(
- &self,
- cx: &mut MutableAppContext,
- callback: Box<dyn FnOnce(&mut MutableAppContext)>,
- ) -> gpui::Subscription;
- fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
- fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
- fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
-}
-
-pub trait WeakItemHandle {
- fn id(&self) -> usize;
- fn window_id(&self) -> usize;
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
-}
-
-impl dyn ItemHandle {
- pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
- self.to_any().downcast()
- }
-
- pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
- self.act_as_type(TypeId::of::<T>(), cx)
- .and_then(|t| t.downcast())
- }
-}
-
-impl<T: Item> ItemHandle for ViewHandle<T> {
- fn subscribe_to_item_events(
- &self,
- cx: &mut MutableAppContext,
- handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
- ) -> gpui::Subscription {
- cx.subscribe(self, move |_, event, cx| {
- for item_event in T::to_item_events(event) {
- handler(item_event, cx)
- }
- })
- }
-
- fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
- self.read(cx).tab_description(detail, cx)
- }
-
- fn tab_content(
- &self,
- detail: Option<usize>,
- style: &theme::Tab,
- cx: &AppContext,
- ) -> ElementBox {
- self.read(cx).tab_content(detail, style, cx)
- }
-
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
- self.read(cx).project_path(cx)
- }
-
- fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
- self.read(cx).project_entry_ids(cx)
- }
-
- fn is_singleton(&self, cx: &AppContext) -> bool {
- self.read(cx).is_singleton(cx)
- }
-
- fn boxed_clone(&self) -> Box<dyn ItemHandle> {
- Box::new(self.clone())
- }
-
- fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
- self.update(cx, |item, cx| {
- cx.add_option_view(|cx| item.clone_on_split(cx))
- })
- .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
- }
-
- fn added_to_pane(
- &self,
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) {
- let history = pane.read(cx).nav_history_for_item(self);
- self.update(cx, |this, cx| this.set_nav_history(history, cx));
-
- if let Some(followed_item) = self.to_followable_item_handle(cx) {
- if let Some(message) = followed_item.to_state_proto(cx) {
- workspace.update_followers(
- proto::update_followers::Variant::CreateView(proto::View {
- id: followed_item.id() as u64,
- variant: Some(message),
- leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
- }),
- cx,
- );
- }
- }
-
- if workspace
- .panes_by_item
- .insert(self.id(), pane.downgrade())
- .is_none()
- {
- let mut pending_autosave = DelayedDebouncedEditAction::new();
- let mut pending_git_update = DelayedDebouncedEditAction::new();
- let pending_update = Rc::new(RefCell::new(None));
- let pending_update_scheduled = Rc::new(AtomicBool::new(false));
-
- let mut event_subscription =
- Some(cx.subscribe(self, move |workspace, item, event, cx| {
- let pane = if let Some(pane) = workspace
- .panes_by_item
- .get(&item.id())
- .and_then(|pane| pane.upgrade(cx))
- {
- pane
- } else {
- log::error!("unexpected item event after pane was dropped");
- return;
- };
-
- if let Some(item) = item.to_followable_item_handle(cx) {
- let leader_id = workspace.leader_for_pane(&pane);
-
- if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
- workspace.unfollow(&pane, cx);
- }
-
- if item.add_event_to_update_proto(
- event,
- &mut *pending_update.borrow_mut(),
- cx,
- ) && !pending_update_scheduled.load(SeqCst)
- {
- pending_update_scheduled.store(true, SeqCst);
- cx.after_window_update({
- let pending_update = pending_update.clone();
- let pending_update_scheduled = pending_update_scheduled.clone();
- move |this, cx| {
- pending_update_scheduled.store(false, SeqCst);
- this.update_followers(
- proto::update_followers::Variant::UpdateView(
- proto::UpdateView {
- id: item.id() as u64,
- variant: pending_update.borrow_mut().take(),
- leader_id: leader_id.map(|id| id.0),
- },
- ),
- cx,
- );
- }
- });
- }
- }
-
- for item_event in T::to_item_events(event).into_iter() {
- match item_event {
- ItemEvent::CloseItem => {
- Pane::close_item(workspace, pane, item.id(), cx)
- .detach_and_log_err(cx);
- return;
- }
-
- ItemEvent::UpdateTab => {
- pane.update(cx, |_, cx| {
- cx.emit(pane::Event::ChangeItemTitle);
- cx.notify();
- });
- }
-
- ItemEvent::Edit => {
- if let Autosave::AfterDelay { milliseconds } =
- cx.global::<Settings>().autosave
- {
- let delay = Duration::from_millis(milliseconds);
- let item = item.clone();
- pending_autosave.fire_new(
- delay,
- workspace,
- cx,
- |project, mut cx| async move {
- cx.update(|cx| Pane::autosave_item(&item, project, cx))
- .await
- .log_err();
- },
- );
- }
-
- let settings = cx.global::<Settings>();
- let debounce_delay = settings.git_overrides.gutter_debounce;
-
- let item = item.clone();
-
- if let Some(delay) = debounce_delay {
- const MIN_GIT_DELAY: u64 = 50;
-
- let delay = delay.max(MIN_GIT_DELAY);
- let duration = Duration::from_millis(delay);
-
- pending_git_update.fire_new(
- duration,
- workspace,
- cx,
- |project, mut cx| async move {
- cx.update(|cx| item.git_diff_recalc(project, cx))
- .await
- .log_err();
- },
- );
- } else {
- let project = workspace.project().downgrade();
- cx.spawn_weak(|_, mut cx| async move {
- if let Some(project) = project.upgrade(&cx) {
- cx.update(|cx| item.git_diff_recalc(project, cx))
- .await
- .log_err();
- }
- })
- .detach();
- }
- }
-
- _ => {}
- }
- }
- }));
-
- cx.observe_focus(self, move |workspace, item, focused, cx| {
- if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
- Pane::autosave_item(&item, workspace.project.clone(), cx)
- .detach_and_log_err(cx);
- }
- })
- .detach();
-
- let item_id = self.id();
- cx.observe_release(self, move |workspace, _, _| {
- workspace.panes_by_item.remove(&item_id);
- event_subscription.take();
- })
- .detach();
- }
- }
-
- fn deactivated(&self, cx: &mut MutableAppContext) {
- self.update(cx, |this, cx| this.deactivated(cx));
- }
-
- fn workspace_deactivated(&self, cx: &mut MutableAppContext) {
- self.update(cx, |this, cx| this.workspace_deactivated(cx));
- }
-
- fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
- self.update(cx, |this, cx| this.navigate(data, cx))
- }
-
- fn id(&self) -> usize {
- self.id()
- }
-
- fn window_id(&self) -> usize {
- self.window_id()
- }
-
- fn to_any(&self) -> AnyViewHandle {
- self.into()
- }
-
- fn is_dirty(&self, cx: &AppContext) -> bool {
- self.read(cx).is_dirty(cx)
- }
-
- fn has_conflict(&self, cx: &AppContext) -> bool {
- self.read(cx).has_conflict(cx)
- }
-
- fn can_save(&self, cx: &AppContext) -> bool {
- self.read(cx).can_save(cx)
- }
-
- fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
- self.update(cx, |item, cx| item.save(project, cx))
- }
-
- fn save_as(
- &self,
- project: ModelHandle<Project>,
- abs_path: PathBuf,
- cx: &mut MutableAppContext,
- ) -> Task<anyhow::Result<()>> {
- self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
- }
-
- fn reload(
- &self,
- project: ModelHandle<Project>,
- cx: &mut MutableAppContext,
- ) -> Task<Result<()>> {
- self.update(cx, |item, cx| item.reload(project, cx))
- }
-
- fn git_diff_recalc(
- &self,
- project: ModelHandle<Project>,
- cx: &mut MutableAppContext,
- ) -> Task<Result<()>> {
- self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
- }
-
- fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
- self.read(cx).act_as_type(type_id, self, cx)
- }
-
- fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
- if cx.has_global::<FollowableItemBuilders>() {
- let builders = cx.global::<FollowableItemBuilders>();
- let item = self.to_any();
- Some(builders.get(&item.view_type())?.1(item))
- } else {
- None
- }
- }
-
- fn on_release(
- &self,
- cx: &mut MutableAppContext,
- callback: Box<dyn FnOnce(&mut MutableAppContext)>,
- ) -> gpui::Subscription {
- cx.observe_release(self, move |_, cx| callback(cx))
- }
-
- fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
- self.read(cx).as_searchable(self)
- }
-
- fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
- self.read(cx).breadcrumb_location()
- }
-
- fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
- self.read(cx).breadcrumbs(theme, cx)
- }
-}
-
-impl From<Box<dyn ItemHandle>> for AnyViewHandle {
- fn from(val: Box<dyn ItemHandle>) -> Self {
- val.to_any()
- }
-}
-
-impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
- fn from(val: &Box<dyn ItemHandle>) -> Self {
- val.to_any()
- }
-}
-
-impl Clone for Box<dyn ItemHandle> {
- fn clone(&self) -> Box<dyn ItemHandle> {
- self.boxed_clone()
- }
-}
-
-impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
- fn id(&self) -> usize {
- self.id()
- }
-
- fn window_id(&self) -> usize {
- self.window_id()
- }
-
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
- self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
- }
-}
-
-pub trait Notification: View {
- fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
-}
-
-pub trait NotificationHandle {
- fn id(&self) -> usize;
- fn to_any(&self) -> AnyViewHandle;
+#[derive(Default)]
+struct LeaderState {
+ followers: HashSet<PeerId>,
}
-impl<T: Notification> NotificationHandle for ViewHandle<T> {
- fn id(&self) -> usize {
- self.id()
- }
-
- fn to_any(&self) -> AnyViewHandle {
- self.into()
- }
-}
+type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
-impl From<&dyn NotificationHandle> for AnyViewHandle {
- fn from(val: &dyn NotificationHandle) -> Self {
- val.to_any()
- }
+#[derive(Default)]
+struct FollowerState {
+ active_view_id: Option<u64>,
+ items_by_leader_view_id: HashMap<u64, FollowerItem>,
}
-impl AppState {
- #[cfg(any(test, feature = "test-support"))]
- pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
- use fs::HomeDir;
-
- cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
- let settings = Settings::test(cx);
- cx.set_global(settings);
-
- let fs = fs::FakeFs::new(cx.background().clone());
- let languages = Arc::new(LanguageRegistry::test());
- let http_client = client::test::FakeHttpClient::with_404_response();
- let client = Client::new(http_client.clone(), cx);
- let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
- let themes = ThemeRegistry::new((), cx.font_cache().clone());
- Arc::new(Self {
- client,
- themes,
- fs,
- languages,
- user_store,
- initialize_workspace: |_, _, _| {},
- build_window_options: Default::default,
- default_item_factory: |_, _| unimplemented!(),
- })
- }
+#[derive(Debug)]
+enum FollowerItem {
+ Loading(Vec<proto::update_view::Variant>),
+ Loaded(Box<dyn FollowableItemHandle>),
}
pub enum Event {
@@ -1083,30 +506,14 @@ pub struct Workspace {
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
+ database_id: WorkspaceId,
_observe_current_user: Task<()>,
}
-#[derive(Default)]
-struct LeaderState {
- followers: HashSet<PeerId>,
-}
-
-type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
-
-#[derive(Default)]
-struct FollowerState {
- active_view_id: Option<u64>,
- items_by_leader_view_id: HashMap<u64, FollowerItem>,
-}
-
-#[derive(Debug)]
-enum FollowerItem {
- Loading(Vec<proto::update_view::Variant>),
- Loaded(Box<dyn FollowableItemHandle>),
-}
-
impl Workspace {
pub fn new(
+ serialized_workspace: Option<SerializedWorkspace>,
+ workspace_id: WorkspaceId,
project: ModelHandle<Project>,
dock_default_factory: DefaultItemFactory,
cx: &mut ViewContext<Self>,
@@ -1126,6 +533,7 @@ impl Workspace {
}
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
this.update_window_title(cx);
+ this.serialize_workspace(cx);
}
project::Event::DisconnectedFromHost => {
this.update_window_edited(cx);
@@ -1145,6 +553,8 @@ impl Workspace {
.detach();
cx.focus(¢er_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
+ let dock = Dock::new(dock_default_factory, cx);
+ let dock_pane = dock.pane().clone();
let fs = project.read(cx).fs().clone();
let user_store = project.read(cx).user_store();
@@ -1171,9 +581,6 @@ impl Workspace {
cx.emit_global(WorkspaceCreated(weak_handle.clone()));
- let dock = Dock::new(cx, dock_default_factory);
- let dock_pane = dock.pane().clone();
-
let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
@@ -1200,39 +607,143 @@ impl Workspace {
active_call = Some((call, subscriptions));
}
- let mut this = Workspace {
- modal: None,
- weak_self: weak_handle,
- center: PaneGroup::new(center_pane.clone()),
- dock,
- // When removing an item, the last element remaining in this array
- // is used to find where focus should fallback to. As such, the order
- // of these two variables is important.
- panes: vec![dock_pane, center_pane.clone()],
- panes_by_item: Default::default(),
- active_pane: center_pane.clone(),
- last_active_center_pane: Some(center_pane.downgrade()),
- status_bar,
- titlebar_item: None,
- notifications: Default::default(),
- client,
- remote_entity_subscription: None,
- user_store,
- fs,
- left_sidebar,
- right_sidebar,
- project,
- leader_state: Default::default(),
- follower_states_by_leader: Default::default(),
- last_leaders_by_pane: Default::default(),
- window_edited: false,
- active_call,
- _observe_current_user,
- };
- this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
- cx.defer(|this, cx| this.update_window_title(cx));
+ let mut this = Workspace {
+ modal: None,
+ weak_self: weak_handle.clone(),
+ center: PaneGroup::new(center_pane.clone()),
+ dock,
+ // When removing an item, the last element remaining in this array
+ // is used to find where focus should fallback to. As such, the order
+ // of these two variables is important.
+ panes: vec![dock_pane.clone(), center_pane.clone()],
+ panes_by_item: Default::default(),
+ active_pane: center_pane.clone(),
+ last_active_center_pane: Some(center_pane.downgrade()),
+ status_bar,
+ titlebar_item: None,
+ notifications: Default::default(),
+ client,
+ remote_entity_subscription: None,
+ user_store,
+ fs,
+ left_sidebar,
+ right_sidebar,
+ project: project.clone(),
+ leader_state: Default::default(),
+ follower_states_by_leader: Default::default(),
+ last_leaders_by_pane: Default::default(),
+ window_edited: false,
+ active_call,
+ database_id: workspace_id,
+ _observe_current_user,
+ };
+ this.project_remote_id_changed(project.read(cx).remote_id(), cx);
+ cx.defer(|this, cx| this.update_window_title(cx));
+
+ if let Some(serialized_workspace) = serialized_workspace {
+ cx.defer(move |_, cx| {
+ Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
+ });
+ }
+
+ this
+ }
+
+ fn new_local(
+ abs_paths: Vec<PathBuf>,
+ app_state: Arc<AppState>,
+ cx: &mut MutableAppContext,
+ ) -> Task<(
+ ViewHandle<Workspace>,
+ Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
+ )> {
+ let project_handle = Project::local(
+ app_state.client.clone(),
+ app_state.user_store.clone(),
+ app_state.languages.clone(),
+ app_state.fs.clone(),
+ cx,
+ );
+
+ cx.spawn(|mut cx| async move {
+ let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
+
+ let paths_to_open = serialized_workspace
+ .as_ref()
+ .map(|workspace| workspace.location.paths())
+ .unwrap_or(Arc::new(abs_paths));
+
+ // Get project paths for all of the abs_paths
+ let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
+ let mut project_paths = Vec::new();
+ for path in paths_to_open.iter() {
+ if let Some((worktree, project_entry)) = cx
+ .update(|cx| {
+ Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
+ })
+ .await
+ .log_err()
+ {
+ worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
+ project_paths.push(Some(project_entry));
+ } else {
+ project_paths.push(None);
+ }
+ }
+
+ let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
+ serialized_workspace.id
+ } else {
+ DB.next_id().await.unwrap_or(0)
+ };
+
+ // Use the serialized workspace to construct the new window
+ let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
+ let mut workspace = Workspace::new(
+ serialized_workspace,
+ workspace_id,
+ project_handle,
+ app_state.default_item_factory,
+ cx,
+ );
+ (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
+ workspace
+ });
+
+ notify_if_database_failed(&workspace, &mut cx);
+
+ // Call open path for each of the project paths
+ // (this will bring them to the front if they were in the serialized workspace)
+ debug_assert!(paths_to_open.len() == project_paths.len());
+ let tasks = paths_to_open
+ .iter()
+ .cloned()
+ .zip(project_paths.into_iter())
+ .map(|(abs_path, project_path)| {
+ let workspace = workspace.clone();
+ cx.spawn(|mut cx| {
+ let fs = app_state.fs.clone();
+ async move {
+ let project_path = project_path?;
+ if fs.is_file(&abs_path).await {
+ Some(
+ workspace
+ .update(&mut cx, |workspace, cx| {
+ workspace.open_path(project_path, None, true, cx)
+ })
+ .await,
+ )
+ } else {
+ None
+ }
+ }
+ })
+ });
+
+ let opened_items = futures::future::join_all(tasks.into_iter()).await;
- this
+ (workspace, opened_items)
+ })
}
pub fn weak_handle(&self) -> WeakViewHandle<Self> {
@@ -62,7 +62,6 @@ async-trait = "0.1"
backtrace = "0.3"
chrono = "0.4"
ctor = "0.1.20"
-dirs = "3.0"
easy-parallel = "3.1.0"
env_logger = "0.9"
futures = "0.3"
@@ -5,7 +5,7 @@ use gpui::{
Element, Entity, MouseButton, RenderContext, View,
};
use settings::Settings;
-use workspace::StatusItemView;
+use workspace::{item::ItemHandle, StatusItemView};
pub const NEW_ISSUE_URL: &str = "https://github.com/zed-industries/feedback/issues/new/choose";
@@ -43,7 +43,7 @@ impl View for FeedbackLink {
impl StatusItemView for FeedbackLink {
fn set_active_pane_item(
&mut self,
- _: Option<&dyn workspace::ItemHandle>,
+ _: Option<&dyn ItemHandle>,
_: &mut gpui::ViewContext<Self>,
) {
}
@@ -37,12 +37,9 @@ use terminal::terminal_container_view::{get_working_directory, TerminalContainer
use fs::RealFs;
use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile};
use theme::ThemeRegistry;
-use util::{ResultExt, TryFutureExt};
-use workspace::{self, AppState, ItemHandle, NewFile, OpenPaths, Workspace};
-use zed::{
- self, build_window_options, initialize_workspace, languages, menus, RELEASE_CHANNEL,
- RELEASE_CHANNEL_NAME,
-};
+use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
+use workspace::{self, item::ItemHandle, AppState, NewFile, OpenPaths, Workspace};
+use zed::{self, build_window_options, initialize_workspace, languages, menus};
fn main() {
let http = http::client();
@@ -56,10 +53,6 @@ fn main() {
.map_or("dev".to_string(), |v| v.to_string());
init_panic_hook(app_version, http.clone(), app.background());
- let db = app.background().spawn(async move {
- project::Db::open(&*zed::paths::DB_DIR, RELEASE_CHANNEL_NAME.as_str())
- });
-
load_embedded_fonts(&app);
let fs = Arc::new(RealFs);
@@ -91,11 +84,11 @@ fn main() {
app.run(move |cx| {
cx.set_global(*RELEASE_CHANNEL);
- cx.set_global(HomeDir(zed::paths::HOME.to_path_buf()));
+ cx.set_global(HomeDir(paths::HOME.to_path_buf()));
let client = client::Client::new(http.clone(), cx);
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
- languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone());
+ languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
let languages = Arc::new(languages);
let init_languages = cx
.background()
@@ -106,7 +99,7 @@ fn main() {
//Setup settings global before binding actions
cx.set_global(SettingsFile::new(
- &*zed::paths::SETTINGS,
+ &*paths::SETTINGS,
settings_file_content.clone(),
fs.clone(),
));
@@ -146,8 +139,7 @@ fn main() {
})
.detach();
- let db = cx.background().block(db);
- client.start_telemetry(db.clone());
+ client.start_telemetry();
client.report_event("start app", Default::default());
let app_state = Arc::new(AppState {
@@ -160,8 +152,10 @@ fn main() {
initialize_workspace,
default_item_factory,
});
- auto_update::init(db, http, cx);
+ auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
+
workspace::init(app_state.clone(), cx);
+
journal::init(app_state.clone(), cx);
theme_selector::init(app_state.clone(), cx);
zed::init(&app_state, cx);
@@ -207,10 +201,10 @@ fn main() {
}
fn init_paths() {
- std::fs::create_dir_all(&*zed::paths::CONFIG_DIR).expect("could not create config path");
- std::fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path");
- std::fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path");
- std::fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path");
+ std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path");
+ std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path");
+ std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path");
+ std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path");
}
fn init_logger() {
@@ -223,16 +217,15 @@ fn init_logger() {
const KIB: u64 = 1024;
const MIB: u64 = 1024 * KIB;
const MAX_LOG_BYTES: u64 = MIB;
- if std::fs::metadata(&*zed::paths::LOG)
- .map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES)
+ if std::fs::metadata(&*paths::LOG).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES)
{
- let _ = std::fs::rename(&*zed::paths::LOG, &*zed::paths::OLD_LOG);
+ let _ = std::fs::rename(&*paths::LOG, &*paths::OLD_LOG);
}
let log_file = OpenOptions::new()
.create(true)
.append(true)
- .open(&*zed::paths::LOG)
+ .open(&*paths::LOG)
.expect("could not open logfile");
simplelog::WriteLogger::init(level, simplelog::Config::default(), log_file)
.expect("could not initialize logger");
@@ -244,7 +237,7 @@ fn init_panic_hook(app_version: String, http: Arc<dyn HttpClient>, background: A
.spawn({
async move {
let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
- let mut children = smol::fs::read_dir(&*zed::paths::LOGS_DIR).await?;
+ let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?;
while let Some(child) = children.next().await {
let child = child?;
let child_path = child.path();
@@ -332,7 +325,7 @@ fn init_panic_hook(app_version: String, http: Arc<dyn HttpClient>, background: A
let panic_filename = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
std::fs::write(
- zed::paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, panic_filename)),
+ paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, panic_filename)),
&message,
)
.context("error writing panic to disk")
@@ -466,8 +459,8 @@ fn load_config_files(
.clone()
.spawn(async move {
let settings_file =
- WatchedJsonFile::new(fs.clone(), &executor, zed::paths::SETTINGS.clone()).await;
- let keymap_file = WatchedJsonFile::new(fs, &executor, zed::paths::KEYMAP.clone()).await;
+ WatchedJsonFile::new(fs.clone(), &executor, paths::SETTINGS.clone()).await;
+ let keymap_file = WatchedJsonFile::new(fs, &executor, paths::KEYMAP.clone()).await;
tx.send((settings_file, keymap_file)).ok()
})
.detach();
@@ -601,6 +594,8 @@ pub fn default_item_factory(
let working_directory = get_working_directory(workspace, cx, strategy);
- let terminal_handle = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
+ let terminal_handle = cx.add_view(|cx| {
+ TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
+ });
Box::new(terminal_handle)
}
@@ -1,7 +1,6 @@
mod feedback;
pub mod languages;
pub mod menus;
-pub mod paths;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
@@ -13,7 +12,6 @@ use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu};
use collections::VecDeque;
pub use editor;
use editor::{Editor, MultiBuffer};
-use lazy_static::lazy_static;
use gpui::{
actions,
@@ -29,9 +27,9 @@ use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar};
use serde::Deserialize;
use serde_json::to_string_pretty;
-use settings::{keymap_file_json_schema, settings_file_json_schema, ReleaseChannel, Settings};
+use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
use std::{env, path::Path, str, sync::Arc};
-use util::ResultExt;
+use util::{channel::ReleaseChannel, paths, ResultExt};
pub use workspace;
use workspace::{sidebar::SidebarSide, AppState, Workspace};
@@ -70,17 +68,6 @@ actions!(
const MIN_FONT_SIZE: f32 = 6.0;
-lazy_static! {
- pub static ref RELEASE_CHANNEL_NAME: String =
- env::var("ZED_RELEASE_CHANNEL").unwrap_or(include_str!("../RELEASE_CHANNEL").to_string());
- pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
- "dev" => ReleaseChannel::Dev,
- "preview" => ReleaseChannel::Preview,
- "stable" => ReleaseChannel::Stable,
- _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME),
- };
-}
-
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_action(about);
cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
@@ -390,7 +377,7 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
}
fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
- let app_name = cx.global::<ReleaseChannel>().name();
+ let app_name = cx.global::<ReleaseChannel>().display_name();
let version = env!("CARGO_PKG_VERSION");
cx.prompt(
gpui::PromptLevel::Info,
@@ -463,10 +450,11 @@ fn open_config_file(
workspace
.update(&mut cx, |workspace, cx| {
- workspace.with_local_workspace(cx, app_state, |workspace, cx| {
+ workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
workspace.open_paths(vec![path.to_path_buf()], false, cx)
})
})
+ .await
.await;
Ok::<_, anyhow::Error>(())
})
@@ -480,51 +468,55 @@ fn open_log_file(
) {
const MAX_LINES: usize = 1000;
- workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
- cx.spawn_weak(|workspace, mut cx| async move {
- let (old_log, new_log) = futures::join!(
- app_state.fs.load(&paths::OLD_LOG),
- app_state.fs.load(&paths::LOG)
- );
+ workspace
+ .with_local_workspace(&app_state.clone(), cx, move |_, cx| {
+ cx.spawn_weak(|workspace, mut cx| async move {
+ let (old_log, new_log) = futures::join!(
+ app_state.fs.load(&paths::OLD_LOG),
+ app_state.fs.load(&paths::LOG)
+ );
- if let Some(workspace) = workspace.upgrade(&cx) {
- let mut lines = VecDeque::with_capacity(MAX_LINES);
- for line in old_log
- .iter()
- .flat_map(|log| log.lines())
- .chain(new_log.iter().flat_map(|log| log.lines()))
- {
- if lines.len() == MAX_LINES {
- lines.pop_front();
+ if let Some(workspace) = workspace.upgrade(&cx) {
+ let mut lines = VecDeque::with_capacity(MAX_LINES);
+ for line in old_log
+ .iter()
+ .flat_map(|log| log.lines())
+ .chain(new_log.iter().flat_map(|log| log.lines()))
+ {
+ if lines.len() == MAX_LINES {
+ lines.pop_front();
+ }
+ lines.push_back(line);
}
- lines.push_back(line);
- }
- let log = lines
- .into_iter()
- .flat_map(|line| [line, "\n"])
- .collect::<String>();
-
- workspace.update(&mut cx, |workspace, cx| {
- let project = workspace.project().clone();
- let buffer = project
- .update(cx, |project, cx| project.create_buffer("", None, cx))
- .expect("creating buffers on a local workspace always succeeds");
- buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
-
- let buffer = cx.add_model(|cx| {
- MultiBuffer::singleton(buffer, cx).with_title("Log".into())
+ let log = lines
+ .into_iter()
+ .flat_map(|line| [line, "\n"])
+ .collect::<String>();
+
+ workspace.update(&mut cx, |workspace, cx| {
+ let project = workspace.project().clone();
+ let buffer = project
+ .update(cx, |project, cx| project.create_buffer("", None, cx))
+ .expect("creating buffers on a local workspace always succeeds");
+ buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
+
+ let buffer = cx.add_model(|cx| {
+ MultiBuffer::singleton(buffer, cx).with_title("Log".into())
+ });
+ workspace.add_item(
+ Box::new(
+ cx.add_view(|cx| {
+ Editor::for_multibuffer(buffer, Some(project), cx)
+ }),
+ ),
+ cx,
+ );
});
- workspace.add_item(
- Box::new(
- cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx)),
- ),
- cx,
- );
- });
- }
+ }
+ })
+ .detach();
})
.detach();
- });
}
fn open_telemetry_log_file(
@@ -532,7 +524,7 @@ fn open_telemetry_log_file(
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
- workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
+ workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
cx.spawn_weak(|workspace, mut cx| async move {
let workspace = workspace.upgrade(&cx)?;
let path = app_state.client.telemetry_log_file_path()?;
@@ -580,31 +572,36 @@ fn open_telemetry_log_file(
Some(())
})
.detach();
- });
+ }).detach();
}
fn open_bundled_config_file(
workspace: &mut Workspace,
app_state: Arc<AppState>,
asset_path: &'static str,
- title: &str,
+ title: &'static str,
cx: &mut ViewContext<Workspace>,
) {
- workspace.with_local_workspace(cx, app_state, |workspace, cx| {
- let project = workspace.project().clone();
- let buffer = project.update(cx, |project, cx| {
- let text = Assets::get(asset_path).unwrap().data;
- let text = str::from_utf8(text.as_ref()).unwrap();
- project
- .create_buffer(text, project.languages().get_language("JSON"), cx)
- .expect("creating buffers on a local workspace always succeeds")
- });
- let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
- workspace.add_item(
- Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), cx))),
- cx,
- );
- });
+ workspace
+ .with_local_workspace(&app_state.clone(), cx, |workspace, cx| {
+ let project = workspace.project().clone();
+ let buffer = project.update(cx, |project, cx| {
+ let text = Assets::get(asset_path).unwrap().data;
+ let text = str::from_utf8(text.as_ref()).unwrap();
+ project
+ .create_buffer(text, project.languages().get_language("JSON"), cx)
+ .expect("creating buffers on a local workspace always succeeds")
+ });
+ let buffer =
+ cx.add_model(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
+ workspace.add_item(
+ Box::new(
+ cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), cx)),
+ ),
+ cx,
+ );
+ })
+ .detach();
}
fn schema_file_match(path: &Path) -> &Path {
@@ -628,7 +625,8 @@ mod tests {
};
use theme::ThemeRegistry;
use workspace::{
- open_paths, pane, Item, ItemHandle, NewFile, Pane, SplitDirection, WorkspaceHandle,
+ item::{Item, ItemHandle},
+ open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle,
};
#[gpui::test]
@@ -764,7 +762,8 @@ mod tests {
#[gpui::test]
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
let app_state = init(cx);
- cx.dispatch_global_action(workspace::NewFile);
+ cx.update(|cx| open_new(&app_state, cx)).await;
+
let window_id = *cx.window_ids().first().unwrap();
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
let editor = workspace.update(cx, |workspace, cx| {
@@ -808,8 +807,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
@@ -928,8 +928,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
// Open a file within an existing worktree.
cx.update(|cx| {
@@ -1088,8 +1089,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (window_id, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (window_id, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
// Open a file within an existing worktree.
cx.update(|cx| {
@@ -1131,8 +1133,9 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(rust_lang()));
- let (window_id, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (window_id, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
// Create a new untitled buffer
@@ -1221,8 +1224,9 @@ mod tests {
let project = Project::test(app_state.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(rust_lang()));
- let (window_id, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (window_id, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
// Create a new untitled buffer
cx.dispatch_action(window_id, NewFile);
@@ -1275,8 +1279,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (window_id, workspace) =
- cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+ let (window_id, workspace) = cx.add_window(|cx| {
+ Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+ });
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
@@ -1350,8 +1355,15 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
@@ -1615,8 +1627,15 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let entries = cx.read(|cx| workspace.file_project_paths(cx));
@@ -12,6 +12,7 @@ import sharedScreen from "./sharedScreen";
import projectDiagnostics from "./projectDiagnostics";
import contactNotification from "./contactNotification";
import updateNotification from "./updateNotification";
+import simpleMessageNotification from "./simpleMessageNotification";
import projectSharedNotification from "./projectSharedNotification";
import tooltip from "./tooltip";
import terminal from "./terminal";
@@ -47,6 +48,7 @@ export default function app(colorScheme: ColorScheme): Object {
},
},
updateNotification: updateNotification(colorScheme),
+ simpleMessageNotification: simpleMessageNotification(colorScheme),
tooltip: tooltip(colorScheme),
terminal: terminal(colorScheme),
colorScheme: {
@@ -0,0 +1,31 @@
+import { ColorScheme } from "../themes/common/colorScheme";
+import { foreground, text } from "./components";
+
+const headerPadding = 8;
+
+export default function simpleMessageNotification(colorScheme: ColorScheme): Object {
+ let layer = colorScheme.middle;
+ return {
+ message: {
+ ...text(layer, "sans", { size: "md" }),
+ margin: { left: headerPadding, right: headerPadding },
+ },
+ actionMessage: {
+ ...text(layer, "sans", { size: "md" }),
+ margin: { left: headerPadding, top: 6, bottom: 6 },
+ hover: {
+ color: foreground(layer, "hovered"),
+ },
+ },
+ dismissButton: {
+ color: foreground(layer),
+ iconWidth: 8,
+ iconHeight: 8,
+ buttonWidth: 8,
+ buttonHeight: 8,
+ hover: {
+ color: foreground(layer, "hovered"),
+ },
+ },
+ };
+}