.github/pull_request_template.md 🔗
@@ -0,0 +1,5 @@
+[[PR Description]]
+
+Release Notes:
+
+* [[Added foo / Fixed bar / No notes]]
Antonio Scandurra created
.github/pull_request_template.md | 5
.github/workflows/ci.yml | 15
.github/workflows/release_actions.yml | 2
Cargo.lock | 360
Cargo.toml | 4
assets/keymaps/default.json | 6
assets/keymaps/jetbrains.json | 3
assets/settings/default.json | 35
crates/activity_indicator/Cargo.toml | 5
crates/activity_indicator/src/activity_indicator.rs | 8
crates/auto_update/src/auto_update.rs | 37
crates/auto_update/src/update_notification.rs | 3
crates/breadcrumbs/src/breadcrumbs.rs | 3
crates/cli/Cargo.toml | 1
crates/cli/src/cli.rs | 12
crates/cli/src/main.rs | 233
crates/client/Cargo.toml | 1
crates/client/src/client.rs | 90
crates/client/src/telemetry.rs | 16
crates/client/src/user.rs | 13
crates/collab/Cargo.toml | 4
crates/collab/migrations.sqlite/20221109000000_test_schema.sql | 19
crates/collab/migrations/20230511004019_add_repository_statuses.sql | 15
crates/collab/src/db.rs | 155
crates/collab/src/db/worktree_repository_statuses.rs | 23
crates/collab/src/rpc.rs | 14
crates/collab/src/tests.rs | 12
crates/collab/src/tests/integration_tests.rs | 224
crates/collab/src/tests/randomized_integration_tests.rs | 264
crates/collab_ui/src/collab_titlebar_item.rs | 7
crates/collab_ui/src/contact_finder.rs | 3
crates/collab_ui/src/contact_list.rs | 5
crates/collab_ui/src/contacts_popover.rs | 3
crates/collab_ui/src/incoming_call_notification.rs | 28
crates/collab_ui/src/notifications.rs | 3
crates/collab_ui/src/project_shared_notification.rs | 27
crates/collab_ui/src/sharing_status_indicator.rs | 6
crates/command_palette/Cargo.toml | 1
crates/command_palette/src/command_palette.rs | 25
crates/context_menu/src/context_menu.rs | 5
crates/copilot/src/copilot.rs | 89
crates/copilot/src/sign_in.rs | 5
crates/copilot_button/Cargo.toml | 5
crates/copilot_button/src/copilot_button.rs | 104
crates/diagnostics/Cargo.toml | 1
crates/diagnostics/src/diagnostics.rs | 55
crates/diagnostics/src/items.rs | 10
crates/editor/Cargo.toml | 2
crates/editor/src/blink_manager.rs | 12
crates/editor/src/display_map.rs | 65
crates/editor/src/display_map/block_map.rs | 13
crates/editor/src/display_map/fold_map.rs | 14
crates/editor/src/display_map/suggestion_map.rs | 10
crates/editor/src/display_map/wrap_map.rs | 14
crates/editor/src/editor.rs | 382
crates/editor/src/editor_settings.rs | 43
crates/editor/src/editor_tests.rs | 345
crates/editor/src/element.rs | 182
crates/editor/src/git.rs | 16
crates/editor/src/highlight_matching_bracket.rs | 4
crates/editor/src/hover_popover.rs | 23
crates/editor/src/items.rs | 21
crates/editor/src/link_go_to_definition.rs | 16
crates/editor/src/mouse_context_menu.rs | 5
crates/editor/src/movement.rs | 33
crates/editor/src/multi_buffer.rs | 154
crates/editor/src/test/editor_lsp_test_context.rs | 8
crates/editor/src/test/editor_test_context.rs | 28
crates/feedback/Cargo.toml | 3
crates/feedback/src/deploy_feedback_button.rs | 3
crates/feedback/src/feedback_info_text.rs | 3
crates/feedback/src/submit_feedback_button.rs | 3
crates/file_finder/Cargo.toml | 7
crates/file_finder/src/file_finder.rs | 669
crates/fs/Cargo.toml | 1
crates/fs/src/fs.rs | 34
crates/fs/src/repository.rs | 129
crates/git/src/diff.rs | 61
crates/go_to_line/Cargo.toml | 5
crates/go_to_line/src/go_to_line.rs | 46
crates/gpui/Cargo.toml | 4
crates/gpui/src/app.rs | 11
crates/gpui/src/app/test_app_context.rs | 2
crates/gpui/src/color.rs | 2
crates/gpui/src/elements.rs | 9
crates/gpui/src/executor.rs | 16
crates/gpui/src/keymap_matcher/binding.rs | 13
crates/gpui/src/platform/mac/window.rs | 2
crates/journal/Cargo.toml | 8
crates/journal/src/journal.rs | 56
crates/language/Cargo.toml | 3
crates/language/src/buffer.rs | 31
crates/language/src/buffer_tests.rs | 73
crates/language/src/language.rs | 5
crates/language/src/language_settings.rs | 338
crates/language/src/syntax_map.rs | 6
crates/language_selector/Cargo.toml | 3
crates/language_selector/src/active_buffer_language.rs | 3
crates/language_selector/src/language_selector.rs | 4
crates/lsp_log/Cargo.toml | 1
crates/lsp_log/src/lsp_log.rs | 3
crates/outline/Cargo.toml | 5
crates/outline/src/outline.rs | 7
crates/picker/Cargo.toml | 1
crates/picker/src/picker.rs | 2
crates/project/Cargo.toml | 6
crates/project/src/lsp_glob_set.rs | 121
crates/project/src/project.rs | 327
crates/project/src/project_settings.rs | 31
crates/project/src/project_tests.rs | 199
crates/project/src/search.rs | 70
crates/project/src/terminals.rs | 22
crates/project/src/worktree.rs | 684
crates/project_panel/Cargo.toml | 4
crates/project_panel/src/project_panel.rs | 324
crates/project_symbols/Cargo.toml | 5
crates/project_symbols/src/project_symbols.rs | 25
crates/recent_projects/Cargo.toml | 4
crates/recent_projects/src/recent_projects.rs | 6
crates/rpc/proto/zed.proto | 14
crates/rpc/src/proto.rs | 40
crates/rpc/src/rpc.rs | 2
crates/search/Cargo.toml | 3
crates/search/src/buffer_search.rs | 35
crates/search/src/project_search.rs | 136
crates/settings/Cargo.toml | 13
crates/settings/src/keymap_file.rs | 20
crates/settings/src/settings.rs | 1596
crates/settings/src/settings_file.rs | 456
crates/settings/src/settings_store.rs | 1246
crates/settings/src/watched_json.rs | 126
crates/sum_tree/src/sum_tree.rs | 2
crates/sum_tree/src/tree_map.rs | 176
crates/terminal/Cargo.toml | 2
crates/terminal/src/terminal.rs | 139
crates/terminal_view/Cargo.toml | 1
crates/terminal_view/src/terminal_element.rs | 74
crates/terminal_view/src/terminal_panel.rs | 69
crates/terminal_view/src/terminal_view.rs | 92
crates/text/src/text.rs | 21
crates/theme/Cargo.toml | 19
crates/theme/src/theme.rs | 27
crates/theme/src/theme_registry.rs | 16
crates/theme/src/theme_settings.rs | 184
crates/theme/src/ui.rs | 57
crates/theme_selector/Cargo.toml | 4
crates/theme_selector/src/theme_selector.rs | 54
crates/theme_testbench/src/theme_testbench.rs | 9
crates/util/Cargo.toml | 1
crates/util/src/paths.rs | 207
crates/util/src/util.rs | 23
crates/vim/Cargo.toml | 1
crates/vim/src/test/vim_test_context.rs | 19
crates/vim/src/vim.rs | 32
crates/welcome/Cargo.toml | 12
crates/welcome/src/base_keymap_picker.rs | 26
crates/welcome/src/base_keymap_setting.rs | 65
crates/welcome/src/welcome.rs | 56
crates/workspace/Cargo.toml | 2
crates/workspace/src/dock.rs | 10
crates/workspace/src/item.rs | 17
crates/workspace/src/notifications.rs | 9
crates/workspace/src/pane.rs | 143
crates/workspace/src/pane/dragged_item_receiver.rs | 12
crates/workspace/src/pane_group.rs | 5
crates/workspace/src/persistence.rs | 4
crates/workspace/src/persistence/model.rs | 63
crates/workspace/src/shared_screen.rs | 3
crates/workspace/src/status_bar.rs | 3
crates/workspace/src/toolbar.rs | 5
crates/workspace/src/workspace.rs | 536
crates/workspace/src/workspace_settings.rs | 58
crates/zed/Cargo.toml | 4
crates/zed/src/languages.rs | 8
crates/zed/src/languages/c.rs | 15
crates/zed/src/languages/json.rs | 33
crates/zed/src/languages/python.rs | 15
crates/zed/src/languages/rust.rs | 15
crates/zed/src/languages/yaml.rs | 8
crates/zed/src/main.rs | 310
crates/zed/src/zed.rs | 308
script/clear-target-dir-if-larger-than | 20
script/get-preview-channel-changes | 77
183 files changed, 8,566 insertions(+), 4,946 deletions(-)
@@ -0,0 +1,5 @@
+[[PR Description]]
+
+Release Notes:
+
+* [[Added foo / Fixed bar / No notes]]
@@ -42,6 +42,7 @@ jobs:
runs-on:
- self-hosted
- test
+ needs: rustfmt
env:
RUSTFLAGS: -D warnings
steps:
@@ -62,6 +63,9 @@ jobs:
clean: false
submodules: 'recursive'
+ - name: Limit target directory size
+ run: script/clear-target-dir-if-larger-than 70
+
- name: Run check
run: cargo check --workspace
@@ -82,7 +86,7 @@ jobs:
runs-on:
- self-hosted
- bundle
- if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
+ if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
needs: tests
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
@@ -110,6 +114,9 @@ jobs:
clean: false
submodules: 'recursive'
+ - name: Limit target directory size
+ run: script/clear-target-dir-if-larger-than 70
+
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
@@ -141,11 +148,11 @@ jobs:
- name: Create app bundle
run: script/bundle
- - name: Upload app bundle to workflow run if main branch
+ - name: Upload app bundle to workflow run if main branch or specifi label
uses: actions/upload-artifact@v2
- if: ${{ github.ref == 'refs/heads/main' }}
+ if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
with:
- name: Zed.dmg
+ name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: target/release/Zed.dmg
- uses: softprops/action-gh-release@v1
@@ -14,7 +14,7 @@ jobs:
content: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
- Restart your Zed or head to https://zed.dev/releases/latest to grab it.
+ Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
```md
# Changelog
@@ -14,12 +14,13 @@ version = "0.1.0"
dependencies = [
"auto_update",
"editor",
- "futures 0.3.25",
+ "futures 0.3.28",
"gpui",
"language",
"project",
"settings",
"smallvec",
+ "theme",
"util",
"workspace",
]
@@ -30,7 +31,16 @@ version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
- "gimli",
+ "gimli 0.26.2",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
+dependencies = [
+ "gimli 0.27.2",
]
[[package]]
@@ -51,7 +61,18 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
- "getrandom 0.2.8",
+ "getrandom 0.2.9",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
+dependencies = [
+ "cfg-if 1.0.0",
"once_cell",
"version_check",
]
@@ -65,6 +86,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "aho-corasick"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "alacritty_config"
version = "0.1.1-dev"
@@ -82,7 +112,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
@@ -92,7 +122,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed
dependencies = [
"alacritty_config",
"alacritty_config_derive",
- "base64",
+ "base64 0.13.1",
"bitflags",
"dirs 4.0.0",
"libc",
@@ -145,15 +175,15 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.66"
+version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "arrayref"
-version = "0.3.6"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
[[package]]
name = "arrayvec"
@@ -232,9 +262,9 @@ dependencies = [
[[package]]
name = "async-executor"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b"
+checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb"
dependencies = [
"async-lock",
"async-task",
@@ -273,32 +303,31 @@ dependencies = [
[[package]]
name = "async-io"
-version = "1.12.0"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
dependencies = [
"async-lock",
"autocfg 1.1.0",
+ "cfg-if 1.0.0",
"concurrent-queue",
"futures-lite",
- "libc",
"log",
"parking",
"polling",
+ "rustix 0.37.19",
"slab",
"socket2",
"waker-fn",
- "windows-sys 0.42.0",
]
[[package]]
name = "async-lock"
-version = "2.6.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
dependencies = [
"event-listener",
- "futures-lite",
]
[[package]]
@@ -318,15 +347,15 @@ name = "async-pipe"
version = "0.1.3"
source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553"
dependencies = [
- "futures 0.3.25",
+ "futures 0.3.28",
"log",
]
[[package]]
name = "async-process"
-version = "1.6.0"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4"
+checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
dependencies = [
"async-io",
"async-lock",
@@ -335,9 +364,9 @@ dependencies = [
"cfg-if 1.0.0",
"event-listener",
"futures-lite",
- "libc",
+ "rustix 0.37.19",
"signal-hook",
- "windows-sys 0.42.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -348,18 +377,18 @@ checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
name = "async-recursion"
-version = "1.0.0"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
+checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.15",
]
[[package]]
@@ -372,7 +401,7 @@ dependencies = [
"async-global-executor",
"async-io",
"async-lock",
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
"futures-channel",
"futures-core",
"futures-io",
@@ -390,23 +419,24 @@ dependencies = [
[[package]]
name = "async-stream"
-version = "0.3.3"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
dependencies = [
"async-stream-impl",
"futures-core",
+ "pin-project-lite 0.2.9",
]
[[package]]
name = "async-stream-impl"
-version = "0.3.3"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.15",
]
[[package]]
@@ -419,7 +449,7 @@ dependencies = [
"filetime",
"libc",
"pin-project",
- "redox_syscall",
+ "redox_syscall 0.2.16",
"xattr",
]
@@ -443,13 +473,13 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.59"
+version = "0.1.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
+checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.15",
]
[[package]]
@@ -486,9 +516,9 @@ dependencies = [
[[package]]
name = "atomic-waker"
-version = "1.0.0"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
+checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
[[package]]
name = "atty"
@@ -548,9 +578,9 @@ checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43"
dependencies = [
"async-trait",
"axum-core",
- "base64",
+ "base64 0.13.1",
"bitflags",
- "bytes 1.3.0",
+ "bytes 1.4.0",
"futures-util",
"headers",
"http",
@@ -582,7 +612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc"
dependencies = [
"async-trait",
- "bytes 1.3.0",
+ "bytes 1.4.0",
"futures-util",
"http",
"http-body",
@@ -598,7 +628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb"
dependencies = [
"axum",
- "bytes 1.3.0",
+ "bytes 1.4.0",
"futures-util",
"http",
"mime",
@@ -614,16 +644,16 @@ dependencies = [
[[package]]
name = "backtrace"
-version = "0.3.66"
+version = "0.3.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
dependencies = [
- "addr2line",
+ "addr2line 0.19.0",
"cc",
"cfg-if 1.0.0",
"libc",
- "miniz_oxide 0.5.4",
- "object 0.29.0",
+ "miniz_oxide 0.6.2",
+ "object 0.30.3",
"rustc-demangle",
]
@@ -637,7 +667,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
@@ -646,11 +676,17 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+[[package]]
+name = "base64"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
+
[[package]]
name = "base64ct"
-version = "1.5.3"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bincode"
@@ -671,7 +707,7 @@ dependencies = [
"cexpr",
"clang-sys",
"clap 2.34.0",
- "env_logger",
+ "env_logger 0.9.3",
"lazy_static",
"lazycell",
"log",
@@ -707,18 +743,18 @@ dependencies = [
[[package]]
name = "block-buffer"
-version = "0.10.3"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "blocking"
-version = "1.3.0"
+version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8"
+checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65"
dependencies = [
"async-channel",
"async-lock",
@@ -726,51 +762,52 @@ dependencies = [
"atomic-waker",
"fastrand",
"futures-lite",
+ "log",
]
[[package]]
name = "borsh"
-version = "0.9.3"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa"
+checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b"
dependencies = [
"borsh-derive",
- "hashbrown 0.11.2",
+ "hashbrown 0.13.2",
]
[[package]]
name = "borsh-derive"
-version = "0.9.3"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775"
+checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7"
dependencies = [
"borsh-derive-internal",
"borsh-schema-derive-internal",
"proc-macro-crate",
"proc-macro2",
- "syn",
+ "syn 1.0.109",
]
[[package]]
name = "borsh-derive-internal"
-version = "0.9.3"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065"
+checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
name = "borsh-schema-derive-internal"
-version = "0.9.3"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0"
+checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
@@ -803,45 +840,47 @@ dependencies = [
[[package]]
name = "bstr"
-version = "0.2.17"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
dependencies = [
"memchr",
+ "serde",
]
[[package]]
name = "bumpalo"
-version = "3.11.1"
+version = "3.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
[[package]]
name = "bytecheck"
-version = "0.6.9"
+version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f"
+checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f"
dependencies = [
"bytecheck_derive",
"ptr_meta",
+ "simdutf8",
]
[[package]]
name = "bytecheck_derive"
-version = "0.6.9"
+version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf"
+checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
name = "bytemuck"
-version = "1.12.3"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
+checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
[[package]]
name = "byteorder"
@@ -861,9 +900,9 @@ dependencies = [
[[package]]
name = "bytes"
-version = "1.3.0"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "call"
@@ -874,7 +913,7 @@ dependencies = [
"client",
"collections",
"fs",
- "futures 0.3.25",
+ "futures 0.3.28",
"gpui",
"language",
"live_kit_client",
@@ -894,7 +933,7 @@ checksum = "e54b86398b5852ddd45784b1d9b196b98beb39171821bad4b8b44534a1e87927"
dependencies = [
"cap-primitives",
"cap-std",
- "io-lifetimes",
+ "io-lifetimes 0.5.3",
"winapi 0.3.9",
]
@@ -905,13 +944,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb8fca3e81fae1d91a36e9784ca22a39ef623702b5f7904d89dc31f10184a178"
dependencies = [
"ambient-authority",
- "errno",
+ "errno 0.2.8",
"fs-set-times",
"io-extras",
- "io-lifetimes",
+ "io-lifetimes 0.5.3",
"ipnet",
"maybe-owned",
- "rustix",
+ "rustix 0.33.7",
"winapi 0.3.9",
"winapi-util",
"winx",
@@ -935,9 +974,9 @@ checksum = "2247568946095c7765ad2b441a56caffc08027734c634a6d5edda648f04e32eb"
dependencies = [
"cap-primitives",
"io-extras",
- "io-lifetimes",
+ "io-lifetimes 0.5.3",
"ipnet",
- "rustix",
+ "rustix 0.33.7",
]
[[package]]
@@ -948,7 +987,7 @@ checksum = "c50472b6ebc302af0401fa3fb939694cd8ff00e0d4c9182001e434fc822ab83a"
dependencies = [
"cap-primitives",
"once_cell",
- "rustix",
+ "rustix 0.33.7",
"winx",
]
@@ -960,9 +999,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
[[package]]
name = "cc"
-version = "1.0.77"
+version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
dependencies = [
"jobserver",
]
@@ -990,9 +1029,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.23"
+version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -1006,9 +1045,9 @@ dependencies = [
[[package]]
name = "chunked_transfer"
-version = "1.4.0"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a"
[[package]]
name = "cipher"
@@ -1021,9 +1060,9 @@ dependencies = [
[[package]]
name = "clang-sys"
-version = "1.4.0"
+version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
+checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
dependencies = [
"glob",
"libc",
@@ -1047,9 +1086,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "3.2.23"
+version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
"bitflags",
@@ -1064,15 +1103,15 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "3.2.18"
+version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
+checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
dependencies = [
- "heck 0.4.0",
+ "heck 0.4.1",
"proc-macro-error",
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
@@ -1089,7 +1128,7 @@ name = "cli"
version = "0.1.0"
dependencies = [
"anyhow",
- "clap 3.2.23",
+ "clap 3.2.25",
"core-foundation",
"core-services",
"dirs 3.0.2",
@@ -1097,6 +1136,7 @@ dependencies = [
"plist",
"serde",
"serde_derive",
+ "util",
]
[[package]]
@@ -1108,7 +1148,7 @@ dependencies = [
"async-tungstenite",
"collections",
"db",
- "futures 0.3.25",
+ "futures 0.3.28",
"gpui",
"image",
"lazy_static",
@@ -1117,6 +1157,7 @@ dependencies = [
"postage",
"rand 0.8.5",
"rpc",
+ "schemars",
"serde",
"serde_derive",
"settings",
@@ -1125,11 +1166,11 @@ dependencies = [
"sum_tree",
"tempfile",
"thiserror",
- "time 0.3.17",
+ "time 0.3.21",
"tiny_http",
"url",
"util",
- "uuid 1.2.2",
+ "uuid 1.3.2",
]
[[package]]
@@ -1141,9 +1182,9 @@ dependencies = [
[[package]]
name = "cmake"
-version = "0.1.49"
+version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c"
+checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
dependencies = [
"cc",
]
@@ -1189,24 +1230,24 @@ dependencies = [
[[package]]
name = "collab"
-version = "0.12.0"
+version = "0.12.4"
dependencies = [
"anyhow",
"async-tungstenite",
"axum",
"axum-extra",
- "base64",
+ "base64 0.13.1",
"call",
- "clap 3.2.23",
+ "clap 3.2.25",
"client",
"collections",
"ctor",
"dashmap",
"editor",
- "env_logger",
+ "env_logger 0.9.3",
"envy",
"fs",
- "futures 0.3.25",
+ "futures 0.3.28",
"git",
"gpui",
"hyper",
@@ -1236,7 +1277,7 @@ dependencies = [
"sha-1 0.9.8",
"sqlx",
"theme",
- "time 0.3.17",
+ "time 0.3.21",
"tokio",
"tokio-tungstenite",
"toml",
@@ -1263,7 +1304,7 @@ dependencies = [
"context_menu",
"editor",
"feedback",
- "futures 0.3.25",
+ "futures 0.3.28",
"fuzzy",
"gpui",
"log",
@@ -1299,9 +1340,10 @@ dependencies = [
"collections",
"ctor",
"editor",
- "env_logger",
+ "env_logger 0.9.3",
"fuzzy",
"gpui",
+ "language",
"picker",
"project",
"serde_json",
@@ -1313,13 +1355,19 @@ dependencies = [
[[package]]
name = "concurrent-queue"
-version = "2.0.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
+checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
dependencies = [
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
]
+[[package]]
+name = "const-cstr"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6"
+
[[package]]
name = "context_menu"
version = "0.1.0"
@@ -1342,7 +1390,7 @@ dependencies = [
"collections",
"context_menu",
"fs",
- "futures 0.3.25",
+ "futures 0.3.28",
"gpui",
"language",
"log",
@@ -1366,8 +1414,10 @@ dependencies = [
"context_menu",
"copilot",
"editor",
- "futures 0.3.25",
+ "fs",
+ "futures 0.3.28",
"gpui",
+ "language",
"settings",
"smol",
"theme",
@@ -1445,9 +1495,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
-version = "0.2.5"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
dependencies = [
"libc",
]
@@ -1472,7 +1522,7 @@ dependencies = [
"cranelift-codegen-shared",
"cranelift-entity",
"cranelift-isle",
- "gimli",
+ "gimli 0.26.2",
"log",
"regalloc2",
"smallvec",
@@ -1550,18 +1600,18 @@ dependencies = [
[[package]]
name = "crc"
-version = "3.0.0"
+version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3"
+checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
-version = "2.1.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff"
+checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484"
[[package]]
name = "crc32fast"
@@ -1584,35 +1634,35 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
-version = "0.5.6"
+version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
]
[[package]]
name = "crossbeam-deque"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
]
[[package]]
name = "crossbeam-epoch"
-version = "0.9.13"
+version = "0.9.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
+checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
dependencies = [
"autocfg 1.1.0",
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.14",
- "memoffset 0.7.1",
+ "crossbeam-utils 0.8.15",
+ "memoffset 0.8.0",
"scopeguard",
]
@@ -1623,7 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
]
[[package]]
@@ -1639,9 +1689,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
-version = "0.8.14"
+version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
dependencies = [
"cfg-if 1.0.0",
]
@@ -1673,7 +1723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
@@ -1693,9 +1743,9 @@ dependencies = [
[[package]]
name = "curl-sys"
-version = "0.4.59+curl-7.86.0"
+version = "0.4.61+curl-8.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
+checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79"
dependencies = [
"cc",
"libc",
@@ -1709,9 +1759,9 @@ dependencies = [
[[package]]
name = "cxx"
-version = "1.0.83"
+version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
+checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -1721,9 +1771,9 @@ dependencies = [
[[package]]
name = "cxx-build"
-version = "1.0.83"
+version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
+checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b"
dependencies = [
"cc",
"codespan-reporting",
@@ -1731,24 +1781,24 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
- "syn",
+ "syn 2.0.15",
]
[[package]]
name = "cxxbridge-flags"
-version = "1.0.83"
+version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
+checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb"
[[package]]
name = "cxxbridge-macro"
-version = "1.0.83"
+version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
+checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.15",
]
[[package]]
@@ -77,7 +77,7 @@ async-trait = { version = "0.1" }
ctor = { version = "0.1" }
env_logger = { version = "0.9" }
futures = { version = "0.3" }
-glob = { version = "0.3.1" }
+globset = { version = "0.4" }
lazy_static = { version = "1.4.0" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" }
@@ -85,6 +85,7 @@ parking_lot = { version = "0.11.1" }
postage = { version = "0.5", features = ["futures-traits"] }
rand = { version = "0.8.5" }
regex = { version = "1.5" }
+schemars = { version = "0.8" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
@@ -93,6 +94,7 @@ smol = { version = "1.2" }
tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
+toml = { version = "0.5" }
unindent = { version = "0.1.7" }
[patch.crates-io]
@@ -192,7 +192,7 @@
}
},
{
- "context": "BufferSearchBar > Editor",
+ "context": "BufferSearchBar",
"bindings": {
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
@@ -201,13 +201,13 @@
}
},
{
- "context": "ProjectSearchBar > Editor",
+ "context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus"
}
},
{
- "context": "ProjectSearchView > Editor",
+ "context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus"
}
@@ -11,6 +11,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"cmd-d": "editor::DuplicateLine",
+ "cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
@@ -33,6 +34,7 @@
],
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
+ "cmd-alt-l": "editor::Format",
"cmd-[": "pane::GoBack",
"cmd-]": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
@@ -63,6 +65,7 @@
{
"context": "Workspace",
"bindings": {
+ "cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle",
"cmd-1": "workspace::ToggleLeftDock",
@@ -1,6 +1,15 @@
{
// The name of the Zed theme to use for the UI
"theme": "One Dark",
+ // The name of a base set of key bindings to use.
+ // This setting can take four values, each named after another
+ // text editor:
+ //
+ // 1. "VSCode"
+ // 2. "JetBrains"
+ // 3. "SublimeText"
+ // 4. "Atom"
+ "base_keymap": "VSCode",
// Features that can be globally enabled or disabled
"features": {
// Show Copilot icon in status bar
@@ -43,6 +52,19 @@
// 3. Draw all invisible symbols:
// "all"
"show_whitespaces": "selection",
+ // Whether to show the scrollbar in the editor.
+ // This setting can take four values:
+ //
+ // 1. Show the scrollbar if there's important information or
+ // follow the system's configured behavior (default):
+ // "auto"
+ // 2. Match the system's configured behavior:
+ // "system"
+ // 3. Always show the scrollbar:
+ // "always"
+ // 4. Never show the scrollbar:
+ // "never"
+ "show_scrollbars": "auto",
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
@@ -106,11 +128,14 @@
},
// Automatically update Zed
"auto_update": true,
- // Git gutter behavior configuration.
+ // Settings specific to the project panel
"project_panel": {
- "dock": "left",
- "default_width": 240
+ // Where to dock project panel. Can be 'left' or 'right'.
+ "dock": "left",
+ // Default width of the project panel.
+ "default_width": 240
},
+ // Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
@@ -155,6 +180,10 @@
"shell": "system",
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
"dock": "bottom",
+ // Default width when the terminal is docked to the left or right.
+ "default_width": 640,
+ // Default height when the terminal is docked to the bottom.
+ "default_height": 320,
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
@@ -16,6 +16,11 @@ gpui = { path = "../gpui" }
project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" }
+theme = { path = "../theme" }
workspace = { path = "../workspace" }
+
futures.workspace = true
smallvec.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -9,7 +9,6 @@ use gpui::{
};
use language::{LanguageRegistry, LanguageServerBinaryStatus};
use project::{LanguageServerProgress, Project};
-use settings::Settings;
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc};
use util::ResultExt;
@@ -325,12 +324,7 @@ impl View for ActivityIndicator {
} = self.content_to_render(cx);
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
- let theme = &cx
- .global::<Settings>()
- .theme
- .workspace
- .status_bar
- .lsp_status;
+ let theme = &theme::current(cx).workspace.status_bar.lsp_status;
let style = if state.hovered() && on_click.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default)
} else {
@@ -1,7 +1,7 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
-use client::{Client, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@@ -10,7 +10,7 @@ use gpui::{
use isahc::AsyncBody;
use serde::Deserialize;
use serde_derive::Serialize;
-use settings::Settings;
+use settings::{Setting, SettingsStore};
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
@@ -58,18 +58,37 @@ impl Entity for AutoUpdater {
type Event = ();
}
+struct AutoUpdateSetting(bool);
+
+impl Setting for AutoUpdateSetting {
+ const KEY: Option<&'static str> = Some("auto_update");
+
+ type FileContent = Option<bool>;
+
+ fn load(
+ default_value: &Option<bool>,
+ user_values: &[&Option<bool>],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Ok(Self(
+ Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
+ ))
+ }
+}
+
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
+ settings::register::<AutoUpdateSetting>(cx);
+
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url);
- let mut update_subscription = cx
- .global::<Settings>()
- .auto_update
+ let mut update_subscription = settings::get::<AutoUpdateSetting>(cx)
+ .0
.then(|| updater.start_polling(cx));
- cx.observe_global::<Settings, _>(move |updater, cx| {
- if cx.global::<Settings>().auto_update {
+ cx.observe_global::<SettingsStore, _>(move |updater, cx| {
+ if settings::get::<AutoUpdateSetting>(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
@@ -102,7 +121,7 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
{
format!("{server_url}/releases/preview/latest")
} else {
- format!("{server_url}/releases/latest")
+ format!("{server_url}/releases/stable/latest")
};
cx.platform().open_url(&latest_release_url);
}
@@ -262,7 +281,7 @@ impl AutoUpdater {
let release_channel = cx
.has_global::<ReleaseChannel>()
.then(|| cx.global::<ReleaseChannel>().display_name());
- let telemetry = cx.global::<Settings>().telemetry().metrics();
+ let telemetry = settings::get::<TelemetrySettings>(cx).metrics;
(installation_id, release_channel, telemetry)
});
@@ -5,7 +5,6 @@ use gpui::{
Element, Entity, View, ViewContext,
};
use menu::Cancel;
-use settings::Settings;
use util::channel::ReleaseChannel;
use workspace::notifications::Notification;
@@ -27,7 +26,7 @@ impl View for UpdateNotification {
}
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let theme = &theme.update_notification;
let app_name = cx.global::<ReleaseChannel>().display_name();
@@ -4,7 +4,6 @@ use gpui::{
};
use itertools::Itertools;
use search::ProjectSearchView;
-use settings::Settings;
use workspace::{
item::{ItemEvent, ItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -50,7 +49,7 @@ impl View for Breadcrumbs {
};
let not_editor = active_item.downcast::<editor::Editor>().is_none();
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let style = &theme.workspace.breadcrumbs;
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
@@ -19,6 +19,7 @@ dirs = "3.0"
ipc-channel = "0.16"
serde.workspace = true
serde_derive.workspace = true
+util = { path = "../util" }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"
@@ -1,6 +1,5 @@
pub use ipc_channel::ipc;
use serde::{Deserialize, Serialize};
-use std::path::PathBuf;
#[derive(Serialize, Deserialize)]
pub struct IpcHandshake {
@@ -10,7 +9,12 @@ pub struct IpcHandshake {
#[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest {
- Open { paths: Vec<PathBuf>, wait: bool },
+ // The filed is named `path` for compatibility, but now CLI can request
+ // opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`.
+ //
+ // Since Zed CLI has to be installed separately, there can be situations when old CLI is
+ // querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later.
+ Open { paths: Vec<String>, wait: bool },
}
#[derive(Debug, Serialize, Deserialize)]
@@ -20,3 +24,7 @@ pub enum CliResponse {
Stderr { message: String },
Exit { status: i32 },
}
+
+/// When Zed started not as an *.app but as a binary (e.g. local development),
+/// there's a possibility to tell it to behave "regularly".
+pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";
@@ -1,6 +1,6 @@
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
use clap::Parser;
-use cli::{CliRequest, CliResponse, IpcHandshake};
+use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
@@ -16,16 +16,20 @@ use std::{
path::{Path, PathBuf},
ptr,
};
+use util::paths::PathLikeWithPosition;
#[derive(Parser)]
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
struct Args {
- /// Wait for all of the given paths to be closed before exiting.
+ /// Wait for all of the given paths to be opened/closed before exiting.
#[clap(short, long)]
wait: bool,
/// A sequence of space-separated paths that you want to open.
- #[clap()]
- paths: Vec<PathBuf>,
+ ///
+ /// Use `path:line:row` syntax to open a file at a specific location.
+ /// Non-existing paths and directories will ignore `:line:row` suffix.
+ #[clap(value_parser = parse_path_with_position)]
+ paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
/// Print Zed's version and the app path.
#[clap(short, long)]
version: bool,
@@ -34,6 +38,14 @@ struct Args {
bundle_path: Option<PathBuf>,
}
+fn parse_path_with_position(
+ argument_str: &str,
+) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
+ PathLikeWithPosition::parse_str(argument_str, |path_str| {
+ Ok(Path::new(path_str).to_path_buf())
+ })
+}
+
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
@@ -43,37 +55,37 @@ struct InfoPlist {
fn main() -> Result<()> {
let args = Args::parse();
- let bundle_path = if let Some(bundle_path) = args.bundle_path {
- bundle_path.canonicalize()?
- } else {
- locate_bundle()?
- };
+ let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
if args.version {
- let plist_path = bundle_path.join("Contents/Info.plist");
- let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
- println!(
- "Zed {} – {}",
- plist.bundle_short_version_string,
- bundle_path.to_string_lossy()
- );
+ println!("{}", bundle.zed_version_string());
return Ok(());
}
- for path in args.paths.iter() {
+ for path in args
+ .paths_with_position
+ .iter()
+ .map(|path_with_position| &path_with_position.path_like)
+ {
if !path.exists() {
touch(path.as_path())?;
}
}
- let (tx, rx) = launch_app(bundle_path)?;
+ let (tx, rx) = bundle.launch()?;
tx.send(CliRequest::Open {
paths: args
- .paths
+ .paths_with_position
.into_iter()
- .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
- .collect::<Result<Vec<PathBuf>>>()?,
+ .map(|path_with_position| {
+ let path_with_position = path_with_position.map_path_like(|path| {
+ fs::canonicalize(&path)
+ .with_context(|| format!("path {path:?} canonicalization"))
+ })?;
+ Ok(path_with_position.to_string(|path| path.display().to_string()))
+ })
+ .collect::<Result<_>>()?,
wait: args.wait,
})?;
@@ -89,6 +101,148 @@ fn main() -> Result<()> {
Ok(())
}
+enum Bundle {
+ App {
+ app_bundle: PathBuf,
+ plist: InfoPlist,
+ },
+ LocalPath {
+ executable: PathBuf,
+ plist: InfoPlist,
+ },
+}
+
+impl Bundle {
+ fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
+ let bundle_path = if let Some(bundle_path) = args_bundle_path {
+ bundle_path
+ .canonicalize()
+ .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
+ } else {
+ locate_bundle().context("bundle autodiscovery")?
+ };
+
+ match bundle_path.extension().and_then(|ext| ext.to_str()) {
+ Some("app") => {
+ let plist_path = bundle_path.join("Contents/Info.plist");
+ let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
+ format!("Reading *.app bundle plist file at {plist_path:?}")
+ })?;
+ Ok(Self::App {
+ app_bundle: bundle_path,
+ plist,
+ })
+ }
+ _ => {
+ println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
+ let plist_path = bundle_path
+ .parent()
+ .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
+ .join("WebRTC.framework/Resources/Info.plist");
+ let plist = plist::from_file::<_, InfoPlist>(&plist_path)
+ .with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
+ Ok(Self::LocalPath {
+ executable: bundle_path,
+ plist,
+ })
+ }
+ }
+ }
+
+ fn plist(&self) -> &InfoPlist {
+ match self {
+ Self::App { plist, .. } => plist,
+ Self::LocalPath { plist, .. } => plist,
+ }
+ }
+
+ fn path(&self) -> &Path {
+ match self {
+ Self::App { app_bundle, .. } => app_bundle,
+ Self::LocalPath {
+ executable: excutable,
+ ..
+ } => excutable,
+ }
+ }
+
+ fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
+ let (server, server_name) =
+ IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
+ let url = format!("zed-cli://{server_name}");
+
+ match self {
+ Self::App { app_bundle, .. } => {
+ let app_path = app_bundle;
+
+ let status = unsafe {
+ let app_url = CFURL::from_path(app_path, true)
+ .with_context(|| format!("invalid app path {app_path:?}"))?;
+ let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
+ ptr::null(),
+ url.as_ptr(),
+ url.len() as CFIndex,
+ kCFStringEncodingUTF8,
+ ptr::null(),
+ ));
+ let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
+ LSOpenFromURLSpec(
+ &LSLaunchURLSpec {
+ appURL: app_url.as_concrete_TypeRef(),
+ itemURLs: urls_to_open.as_concrete_TypeRef(),
+ passThruParams: ptr::null(),
+ launchFlags: kLSLaunchDefaults,
+ asyncRefCon: ptr::null_mut(),
+ },
+ ptr::null_mut(),
+ )
+ };
+
+ anyhow::ensure!(
+ status == 0,
+ "cannot start app bundle {}",
+ self.zed_version_string()
+ );
+ }
+ Self::LocalPath { executable, .. } => {
+ let executable_parent = executable
+ .parent()
+ .with_context(|| format!("Executable {executable:?} path has no parent"))?;
+ let subprocess_stdout_file =
+ fs::File::create(executable_parent.join("zed_dev.log"))
+ .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
+ let subprocess_stdin_file =
+ subprocess_stdout_file.try_clone().with_context(|| {
+ format!("Cloning descriptor for file {subprocess_stdout_file:?}")
+ })?;
+ let mut command = std::process::Command::new(executable);
+ let command = command
+ .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
+ .stderr(subprocess_stdout_file)
+ .stdout(subprocess_stdin_file)
+ .arg(url);
+
+ command
+ .spawn()
+ .with_context(|| format!("Spawning {command:?}"))?;
+ }
+ }
+
+ let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
+ Ok((handshake.requests, handshake.responses))
+ }
+
+ fn zed_version_string(&self) -> String {
+ let is_dev = matches!(self, Self::LocalPath { .. });
+ format!(
+ "Zed {}{} – {}",
+ self.plist().bundle_short_version_string,
+ if is_dev { " (dev)" } else { "" },
+ self.path().display(),
+ )
+ }
+}
+
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
@@ -106,38 +260,3 @@ fn locate_bundle() -> Result<PathBuf> {
}
Ok(app_path)
}
-
-fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
- let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
- let url = format!("zed-cli://{server_name}");
-
- let status = unsafe {
- let app_url =
- CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
- let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
- ptr::null(),
- url.as_ptr(),
- url.len() as CFIndex,
- kCFStringEncodingUTF8,
- ptr::null(),
- ));
- let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
- LSOpenFromURLSpec(
- &LSLaunchURLSpec {
- appURL: app_url.as_concrete_TypeRef(),
- itemURLs: urls_to_open.as_concrete_TypeRef(),
- passThruParams: ptr::null(),
- launchFlags: kLSLaunchDefaults,
- asyncRefCon: ptr::null_mut(),
- },
- ptr::null_mut(),
- )
- };
-
- if status == 0 {
- let (_, handshake) = server.accept()?;
- Ok((handshake.requests, handshake.responses))
- } else {
- Err(anyhow!("cannot start {:?}", app_path))
- }
-}
@@ -31,6 +31,7 @@ log.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
+schemars.workspace = true
smol.workspace = true
thiserror.workspace = true
time.workspace = true
@@ -15,19 +15,17 @@ use futures::{
TryStreamExt,
};
use gpui::{
- actions,
- platform::AppVersion,
- serde_json::{self},
- AnyModelHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
- ModelHandle, Task, View, ViewContext, WeakViewHandle,
+ actions, platform::AppVersion, serde_json, AnyModelHandle, AnyWeakModelHandle,
+ AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext,
+ WeakViewHandle,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
-use serde::Deserialize;
-use settings::Settings;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
use std::{
any::TypeId,
collections::HashMap,
@@ -72,25 +70,34 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [SignIn, SignOut]);
-pub fn init(client: Arc<Client>, cx: &mut AppContext) {
+pub fn init_settings(cx: &mut AppContext) {
+ settings::register::<TelemetrySettings>(cx);
+}
+
+pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
+ init_settings(cx);
+
+ let client = Arc::downgrade(client);
cx.add_global_action({
let client = client.clone();
move |_: &SignIn, cx| {
- let client = client.clone();
- cx.spawn(
- |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
- )
- .detach();
+ if let Some(client) = client.upgrade() {
+ cx.spawn(
+ |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
+ )
+ .detach();
+ }
}
});
cx.add_global_action({
let client = client.clone();
move |_: &SignOut, cx| {
- let client = client.clone();
- cx.spawn(|cx| async move {
- client.disconnect(&cx);
- })
- .detach();
+ if let Some(client) = client.upgrade() {
+ cx.spawn(|cx| async move {
+ client.disconnect(&cx);
+ })
+ .detach();
+ }
}
});
}
@@ -326,6 +333,42 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
}
}
+#[derive(Copy, Clone)]
+pub struct TelemetrySettings {
+ pub diagnostics: bool,
+ pub metrics: bool,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct TelemetrySettingsContent {
+ pub diagnostics: Option<bool>,
+ pub metrics: Option<bool>,
+}
+
+impl settings::Setting for TelemetrySettings {
+ const KEY: Option<&'static str> = Some("telemetry");
+
+ type FileContent = TelemetrySettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Ok(Self {
+ diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
+ default_value
+ .diagnostics
+ .ok_or_else(Self::missing_default)?,
+ ),
+ metrics: user_values
+ .first()
+ .and_then(|v| v.metrics)
+ .unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
+ })
+ }
+}
+
impl Client {
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
Arc::new(Self {
@@ -447,9 +490,7 @@ impl Client {
}));
}
Status::SignedOut | Status::UpgradeRequired => {
- let telemetry_settings = cx.read(|cx| cx.global::<Settings>().telemetry());
- self.telemetry
- .set_authenticated_user_info(None, false, telemetry_settings);
+ cx.read(|cx| self.telemetry.set_authenticated_user_info(None, false, cx));
state._reconnect_task.take();
}
_ => {}
@@ -740,7 +781,7 @@ impl Client {
self.telemetry().report_mixpanel_event(
"read credentials from keychain",
Default::default(),
- cx.global::<Settings>().telemetry(),
+ *settings::get::<TelemetrySettings>(cx),
);
});
}
@@ -1033,7 +1074,8 @@ impl Client {
let executor = cx.background();
let telemetry = self.telemetry.clone();
let http = self.http.clone();
- let metrics_enabled = cx.read(|cx| cx.global::<Settings>().telemetry());
+
+ let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
@@ -1120,7 +1162,7 @@ impl Client {
telemetry.report_mixpanel_event(
"authenticate with browser",
Default::default(),
- metrics_enabled,
+ telemetry_settings,
);
Ok(Credentials {
@@ -1,4 +1,4 @@
-use crate::{ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
+use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
executor::Background,
@@ -9,7 +9,6 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
-use settings::TelemetrySettings;
use std::{
io::Write,
mem,
@@ -86,6 +85,11 @@ pub enum ClickhouseEvent {
copilot_enabled: bool,
copilot_enabled_for_language: bool,
},
+ Copilot {
+ suggestion_id: Option<String>,
+ suggestion_accepted: bool,
+ file_extension: Option<String>,
+ },
}
#[derive(Serialize, Debug)]
@@ -241,9 +245,9 @@ impl Telemetry {
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
- telemetry_settings: TelemetrySettings,
+ cx: &AppContext,
) {
- if !telemetry_settings.metrics() {
+ if !settings::get::<TelemetrySettings>(cx).metrics {
return;
}
@@ -285,7 +289,7 @@ impl Telemetry {
event: ClickhouseEvent,
telemetry_settings: TelemetrySettings,
) {
- if !telemetry_settings.metrics() {
+ if !telemetry_settings.metrics {
return;
}
@@ -321,7 +325,7 @@ impl Telemetry {
properties: Value,
telemetry_settings: TelemetrySettings,
) {
- if !telemetry_settings.metrics() {
+ if !telemetry_settings.metrics {
return;
}
@@ -5,7 +5,6 @@ use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
-use settings::Settings;
use staff_mode::StaffMode;
use std::sync::{Arc, Weak};
use util::http::HttpClient;
@@ -144,11 +143,13 @@ impl UserStore {
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
- client.telemetry.set_authenticated_user_info(
- info.as_ref().map(|info| info.metrics_id.clone()),
- info.as_ref().map(|info| info.staff).unwrap_or(false),
- cx.read(|cx| cx.global::<Settings>().telemetry()),
- );
+ cx.read(|cx| {
+ client.telemetry.set_authenticated_user_info(
+ info.as_ref().map(|info| info.metrics_id.clone()),
+ info.as_ref().map(|info| info.staff).unwrap_or(false),
+ cx,
+ )
+ });
cx.update(|cx| {
cx.update_default_global(|staff_mode: &mut StaffMode, _| {
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
-version = "0.12.0"
+version = "0.12.4"
publish = false
[[bin]]
@@ -51,7 +51,7 @@ tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.17"
tonic = "0.6"
tower = "0.4"
-toml = "0.5.8"
+toml.workspace = true
tracing = "0.1.34"
tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
@@ -86,8 +86,8 @@ CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
- "scan_id" INTEGER NOT NULL,
"branch" VARCHAR,
+ "scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
@@ -96,6 +96,23 @@ CREATE TABLE "worktree_repositories" (
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
+CREATE TABLE "worktree_repository_statuses" (
+ "project_id" INTEGER NOT NULL,
+ "worktree_id" INTEGER NOT NULL,
+ "work_directory_id" INTEGER NOT NULL,
+ "repo_path" VARCHAR NOT NULL,
+ "status" INTEGER NOT NULL,
+ "scan_id" INTEGER NOT NULL,
+ "is_deleted" BOOL NOT NULL,
+ PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
+ FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
+);
+CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
+CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
+CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
+
+
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
@@ -0,0 +1,15 @@
+CREATE TABLE "worktree_repository_statuses" (
+ "project_id" INTEGER NOT NULL,
+ "worktree_id" INT8 NOT NULL,
+ "work_directory_id" INT8 NOT NULL,
+ "repo_path" VARCHAR NOT NULL,
+ "status" INT8 NOT NULL,
+ "scan_id" INT8 NOT NULL,
+ "is_deleted" BOOL NOT NULL,
+ PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
+ FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
+);
+CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
+CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
+CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
@@ -15,6 +15,7 @@ mod worktree;
mod worktree_diagnostic_summary;
mod worktree_entry;
mod worktree_repository;
+mod worktree_repository_statuses;
use crate::executor::Executor;
use crate::{Error, Result};
@@ -1513,6 +1514,7 @@ impl Database {
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
+ .add(worktree_entry::Column::ProjectId.eq(project.id))
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
@@ -1552,6 +1554,7 @@ impl Database {
let mut db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
+ .add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
@@ -1568,6 +1571,54 @@ impl Database {
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
+ removed_repo_paths: Default::default(),
+ updated_statuses: Default::default(),
+ });
+ }
+ }
+ }
+
+ // Repository Status Entries
+ for repository in worktree.updated_repositories.iter_mut() {
+ let repository_status_entry_filter =
+ if let Some(rejoined_worktree) = rejoined_worktree {
+ worktree_repository_statuses::Column::ScanId
+ .gt(rejoined_worktree.scan_id)
+ } else {
+ worktree_repository_statuses::Column::IsDeleted.eq(false)
+ };
+
+ let mut db_repository_statuses =
+ worktree_repository_statuses::Entity::find()
+ .filter(
+ Condition::all()
+ .add(
+ worktree_repository_statuses::Column::ProjectId
+ .eq(project.id),
+ )
+ .add(
+ worktree_repository_statuses::Column::WorktreeId
+ .eq(worktree.id),
+ )
+ .add(
+ worktree_repository_statuses::Column::WorkDirectoryId
+ .eq(repository.work_directory_id),
+ )
+ .add(repository_status_entry_filter),
+ )
+ .stream(&*tx)
+ .await?;
+
+ while let Some(db_status_entry) = db_repository_statuses.next().await {
+ let db_status_entry = db_status_entry?;
+ if db_status_entry.is_deleted {
+ repository
+ .removed_repo_paths
+ .push(db_status_entry.repo_path);
+ } else {
+ repository.updated_statuses.push(proto::StatusEntry {
+ repo_path: db_status_entry.repo_path,
+ status: db_status_entry.status as i32,
});
}
}
@@ -2395,6 +2446,68 @@ impl Database {
)
.exec(&*tx)
.await?;
+
+ for repository in update.updated_repositories.iter() {
+ if !repository.updated_statuses.is_empty() {
+ worktree_repository_statuses::Entity::insert_many(
+ repository.updated_statuses.iter().map(|status_entry| {
+ worktree_repository_statuses::ActiveModel {
+ project_id: ActiveValue::set(project_id),
+ worktree_id: ActiveValue::set(worktree_id),
+ work_directory_id: ActiveValue::set(
+ repository.work_directory_id as i64,
+ ),
+ repo_path: ActiveValue::set(status_entry.repo_path.clone()),
+ status: ActiveValue::set(status_entry.status as i64),
+ scan_id: ActiveValue::set(update.scan_id as i64),
+ is_deleted: ActiveValue::set(false),
+ }
+ }),
+ )
+ .on_conflict(
+ OnConflict::columns([
+ worktree_repository_statuses::Column::ProjectId,
+ worktree_repository_statuses::Column::WorktreeId,
+ worktree_repository_statuses::Column::WorkDirectoryId,
+ worktree_repository_statuses::Column::RepoPath,
+ ])
+ .update_columns([
+ worktree_repository_statuses::Column::ScanId,
+ worktree_repository_statuses::Column::Status,
+ worktree_repository_statuses::Column::IsDeleted,
+ ])
+ .to_owned(),
+ )
+ .exec(&*tx)
+ .await?;
+ }
+
+ if !repository.removed_repo_paths.is_empty() {
+ worktree_repository_statuses::Entity::update_many()
+ .filter(
+ worktree_repository_statuses::Column::ProjectId
+ .eq(project_id)
+ .and(
+ worktree_repository_statuses::Column::WorktreeId
+ .eq(worktree_id),
+ )
+ .and(
+ worktree_repository_statuses::Column::WorkDirectoryId
+ .eq(repository.work_directory_id as i64),
+ )
+ .and(worktree_repository_statuses::Column::RepoPath.is_in(
+ repository.removed_repo_paths.iter().map(String::as_str),
+ )),
+ )
+ .set(worktree_repository_statuses::ActiveModel {
+ is_deleted: ActiveValue::Set(true),
+ scan_id: ActiveValue::Set(update.scan_id as i64),
+ ..Default::default()
+ })
+ .exec(&*tx)
+ .await?;
+ }
+ }
}
if !update.removed_repositories.is_empty() {
@@ -2645,10 +2758,42 @@ impl Database {
if let Some(worktree) =
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
- worktree.repository_entries.push(proto::RepositoryEntry {
- work_directory_id: db_repository_entry.work_directory_id as u64,
- branch: db_repository_entry.branch,
- });
+ worktree.repository_entries.insert(
+ db_repository_entry.work_directory_id as u64,
+ proto::RepositoryEntry {
+ work_directory_id: db_repository_entry.work_directory_id as u64,
+ branch: db_repository_entry.branch,
+ removed_repo_paths: Default::default(),
+ updated_statuses: Default::default(),
+ },
+ );
+ }
+ }
+ }
+
+ {
+ let mut db_status_entries = worktree_repository_statuses::Entity::find()
+ .filter(
+ Condition::all()
+ .add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
+ .add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
+ )
+ .stream(&*tx)
+ .await?;
+
+ while let Some(db_status_entry) = db_status_entries.next().await {
+ let db_status_entry = db_status_entry?;
+ if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
+ {
+ if let Some(repository_entry) = worktree
+ .repository_entries
+ .get_mut(&(db_status_entry.work_directory_id as u64))
+ {
+ repository_entry.updated_statuses.push(proto::StatusEntry {
+ repo_path: db_status_entry.repo_path,
+ status: db_status_entry.status as i32,
+ });
+ }
}
}
}
@@ -3390,7 +3535,7 @@ pub struct Worktree {
pub root_name: String,
pub visible: bool,
pub entries: Vec<proto::Entry>,
- pub repository_entries: Vec<proto::RepositoryEntry>,
+ pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub completed_scan_id: u64,
@@ -0,0 +1,23 @@
+use super::ProjectId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "worktree_repository_statuses")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub project_id: ProjectId,
+ #[sea_orm(primary_key)]
+ pub worktree_id: i64,
+ #[sea_orm(primary_key)]
+ pub work_directory_id: i64,
+ #[sea_orm(primary_key)]
+ pub repo_path: String,
+ pub status: i64,
+ pub scan_id: i64,
+ pub is_deleted: bool,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}
@@ -51,7 +51,7 @@ use std::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
- time::Duration,
+ time::{Duration, Instant},
};
use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder;
@@ -397,10 +397,16 @@ impl Server {
"message received"
);
});
+ let start_time = Instant::now();
let future = (handler)(*envelope, session);
async move {
- if let Err(error) = future.await {
- tracing::error!(%error, "error handling message");
+ let result = future.await;
+ let duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0;
+ match result {
+ Err(error) => {
+ tracing::error!(%error, ?duration_ms, "error handling message")
+ }
+ Ok(()) => tracing::info!(?duration_ms, "finished handling message"),
}
}
.instrument(span)
@@ -1385,7 +1391,7 @@ async fn join_project(
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.scan_id == worktree.completed_scan_id,
- updated_repositories: worktree.repository_entries,
+ updated_repositories: worktree.repository_entries.into_values().collect(),
removed_repositories: Default::default(),
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
@@ -19,7 +19,7 @@ use gpui::{
use language::LanguageRegistry;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
-use settings::Settings;
+use settings::SettingsStore;
use std::{
cell::{Ref, RefCell, RefMut},
env,
@@ -30,7 +30,6 @@ use std::{
Arc,
},
};
-use theme::ThemeRegistry;
use util::http::FakeHttpClient;
use workspace::Workspace;
@@ -102,7 +101,7 @@ impl TestServer {
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
cx.update(|cx| {
- cx.set_global(Settings::test(cx));
+ cx.set_global(SettingsStore::test(cx));
});
let http = FakeHttpClient::with_404_response();
@@ -191,15 +190,18 @@ impl TestServer {
client: client.clone(),
user_store: user_store.clone(),
languages: Arc::new(LanguageRegistry::test()),
- themes: ThemeRegistry::new((), cx.font_cache()),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| unimplemented!(),
background_actions: || &[],
});
- Project::init(&client);
cx.update(|cx| {
+ theme::init((), cx);
+ Project::init(&client, cx);
+ client::init(&client, cx);
+ language::init(cx);
+ editor::init_settings(cx);
workspace::init(app_state.clone(), cx);
call::init(client.clone(), user_store.clone(), cx);
});
@@ -10,7 +10,7 @@ use editor::{
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
Undo,
};
-use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
+use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
use futures::StreamExt as _;
use gpui::{
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
@@ -18,6 +18,7 @@ use gpui::{
};
use indoc::indoc;
use language::{
+ language_settings::{AllLanguageSettings, Formatter},
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope,
};
@@ -26,7 +27,7 @@ use lsp::LanguageServerId;
use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
use rand::prelude::*;
use serde_json::json;
-use settings::{Formatter, Settings};
+use settings::SettingsStore;
use std::{
cell::{Cell, RefCell},
env, future, mem,
@@ -1438,7 +1439,6 @@ async fn test_host_disconnect(
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
- cx_b.update(editor::init);
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
@@ -1448,6 +1448,8 @@ async fn test_host_disconnect(
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
.await;
+ cx_b.update(editor::init);
+
client_a
.fs
.insert_tree(
@@ -1545,7 +1547,6 @@ async fn test_project_reconnect(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
- cx_b.update(editor::init);
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
@@ -1554,6 +1555,8 @@ async fn test_project_reconnect(
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
+ cx_b.update(editor::init);
+
client_a
.fs
.insert_tree(
@@ -2434,7 +2437,7 @@ async fn test_git_diff_base_change(
buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2454,7 +2457,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2478,7 +2481,7 @@ async fn test_git_diff_base_change(
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2489,7 +2492,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2532,7 +2535,7 @@ async fn test_git_diff_base_change(
buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2552,7 +2555,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2580,12 +2583,12 @@ async fn test_git_diff_base_change(
"{:?}",
buffer
.snapshot()
- .git_diff_hunks_in_row_range(0..4, false)
+ .git_diff_hunks_in_row_range(0..4)
.collect::<Vec<_>>()
);
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2596,7 +2599,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2690,6 +2693,154 @@ async fn test_git_branch_name(
});
}
+#[gpui::test]
+async fn test_git_status_sync(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ client_a
+ .fs
+ .insert_tree(
+ "/dir",
+ json!({
+ ".git": {},
+ "a.txt": "a",
+ "b.txt": "b",
+ }),
+ )
+ .await;
+
+ const A_TXT: &'static str = "a.txt";
+ const B_TXT: &'static str = "b.txt";
+
+ client_a
+ .fs
+ .as_fake()
+ .set_status_for_repo(
+ Path::new("/dir/.git"),
+ &[
+ (&Path::new(A_TXT), GitFileStatus::Added),
+ (&Path::new(B_TXT), GitFileStatus::Added),
+ ],
+ )
+ .await;
+
+ let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| {
+ call.share_project(project_local.clone(), cx)
+ })
+ .await
+ .unwrap();
+
+ let project_remote = client_b.build_remote_project(project_id, cx_b).await;
+
+ // Wait for it to catch up to the new status
+ deterministic.run_until_parked();
+
+ #[track_caller]
+ fn assert_status(
+ file: &impl AsRef<Path>,
+ status: Option<GitFileStatus>,
+ project: &Project,
+ cx: &AppContext,
+ ) {
+ let file = file.as_ref();
+ let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ let worktree = worktrees[0].clone();
+ let snapshot = worktree.read(cx).snapshot();
+ let root_entry = snapshot.root_git_entry().unwrap();
+ assert_eq!(root_entry.status_for_file(&snapshot, file), status);
+ }
+
+ // Smoke test status reading
+ project_local.read_with(cx_a, |project, cx| {
+ assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
+ assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
+ });
+ project_remote.read_with(cx_b, |project, cx| {
+ assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
+ assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
+ });
+
+ client_a
+ .fs
+ .as_fake()
+ .set_status_for_repo(
+ Path::new("/dir/.git"),
+ &[
+ (&Path::new(A_TXT), GitFileStatus::Modified),
+ (&Path::new(B_TXT), GitFileStatus::Modified),
+ ],
+ )
+ .await;
+
+ // Wait for buffer_local_a to receive it
+ deterministic.run_until_parked();
+
+ // Smoke test status reading
+ project_local.read_with(cx_a, |project, cx| {
+ assert_status(
+ &Path::new(A_TXT),
+ Some(GitFileStatus::Modified),
+ project,
+ cx,
+ );
+ assert_status(
+ &Path::new(B_TXT),
+ Some(GitFileStatus::Modified),
+ project,
+ cx,
+ );
+ });
+ project_remote.read_with(cx_b, |project, cx| {
+ assert_status(
+ &Path::new(A_TXT),
+ Some(GitFileStatus::Modified),
+ project,
+ cx,
+ );
+ assert_status(
+ &Path::new(B_TXT),
+ Some(GitFileStatus::Modified),
+ project,
+ cx,
+ );
+ });
+
+ // And synchronization while joining
+ let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
+ deterministic.run_until_parked();
+
+ project_remote_c.read_with(cx_c, |project, cx| {
+ assert_status(
+ &Path::new(A_TXT),
+ Some(GitFileStatus::Modified),
+ project,
+ cx,
+ );
+ assert_status(
+ &Path::new(B_TXT),
+ Some(GitFileStatus::Modified),
+ project,
+ cx,
+ );
+ });
+}
+
#[gpui::test(iterations = 10)]
async fn test_fs_operations(
deterministic: Arc<Deterministic>,
@@ -4219,10 +4370,12 @@ async fn test_formatting_buffer(
// Ensure buffer can be formatted using an external command. Notice how the
// host's configuration is honored as opposed to using the guest's settings.
cx_a.update(|cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.editor_defaults.formatter = Some(Formatter::External {
- command: "awk".to_string(),
- arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |file| {
+ file.defaults.formatter = Some(Formatter::External {
+ command: "awk".into(),
+ arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
+ });
});
});
});
@@ -4989,7 +5142,6 @@ async fn test_collaborating_with_code_actions(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
- cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -4998,6 +5150,8 @@ async fn test_collaborating_with_code_actions(
.await;
let active_call_a = cx_a.read(ActiveCall::global);
+ cx_b.update(editor::init);
+
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
@@ -5202,7 +5356,6 @@ async fn test_collaborating_with_renames(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
- cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -5211,6 +5364,8 @@ async fn test_collaborating_with_renames(
.await;
let active_call_a = cx_a.read(ActiveCall::global);
+ cx_b.update(editor::init);
+
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
@@ -5392,8 +5547,6 @@ async fn test_language_server_statuses(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
-
- cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -5402,6 +5555,8 @@ async fn test_language_server_statuses(
.await;
let active_call_a = cx_a.read(ActiveCall::global);
+ cx_b.update(editor::init);
+
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
@@ -6109,8 +6264,6 @@ async fn test_basic_following(
cx_d: &mut TestAppContext,
) {
deterministic.forbid_parking();
- cx_a.update(editor::init);
- cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
@@ -6128,6 +6281,9 @@ async fn test_basic_following(
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
client_a
.fs
.insert_tree(
@@ -6706,9 +6862,6 @@ async fn test_following_tab_order(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
- cx_a.update(editor::init);
- cx_b.update(editor::init);
-
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -6718,6 +6871,9 @@ async fn test_following_tab_order(
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
client_a
.fs
.insert_tree(
@@ -6828,9 +6984,6 @@ async fn test_peers_following_each_other(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
- cx_a.update(editor::init);
- cx_b.update(editor::init);
-
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -6840,6 +6993,9 @@ async fn test_peers_following_each_other(
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
// Client A shares a project.
client_a
.fs
@@ -6999,8 +7155,6 @@ async fn test_auto_unfollowing(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
- cx_a.update(editor::init);
- cx_b.update(editor::init);
// 2 clients connect to a server.
let mut server = TestServer::start(&deterministic).await;
@@ -7012,6 +7166,9 @@ async fn test_auto_unfollowing(
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
// Client A shares a project.
client_a
.fs
@@ -7166,8 +7323,6 @@ async fn test_peers_simultaneously_following_each_other(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
- cx_a.update(editor::init);
- cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
@@ -7177,6 +7332,9 @@ async fn test_peers_simultaneously_following_each_other(
.await;
let active_call_a = cx_a.read(ActiveCall::global);
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
client_a.fs.insert_tree("/a", json!({})).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let workspace_a = client_a.build_workspace(&project_a, cx_a);
@@ -8,19 +8,20 @@ use call::ActiveCall;
use client::RECEIVE_TIMEOUT;
use collections::BTreeMap;
use editor::Bias;
-use fs::{FakeFs, Fs as _};
+use fs::{repository::GitFileStatus, FakeFs, Fs as _};
use futures::StreamExt as _;
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
use lsp::FakeLanguageServer;
use parking_lot::Mutex;
+use pretty_assertions::assert_eq;
use project::{search::SearchQuery, Project, ProjectPath};
use rand::{
distributions::{Alphanumeric, DistString},
prelude::*,
};
use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::SettingsStore;
use std::{
env,
ops::Range,
@@ -148,8 +149,9 @@ async fn test_random_collaboration(
for (client, mut cx) in clients {
cx.update(|cx| {
+ let store = cx.remove_global::<SettingsStore>();
cx.clear_globals();
- cx.set_global(Settings::test(cx));
+ cx.set_global(store);
drop(client);
});
}
@@ -763,53 +765,85 @@ async fn apply_client_operation(
}
}
- ClientOperation::WriteGitIndex {
- repo_path,
- contents,
- } => {
- if !client.fs.directories().contains(&repo_path) {
- return Err(TestError::Inapplicable);
- }
-
- log::info!(
- "{}: writing git index for repo {:?}: {:?}",
- client.username,
+ ClientOperation::GitOperation { operation } => match operation {
+ GitOperation::WriteGitIndex {
repo_path,
- contents
- );
+ contents,
+ } => {
+ if !client.fs.directories().contains(&repo_path) {
+ return Err(TestError::Inapplicable);
+ }
- let dot_git_dir = repo_path.join(".git");
- let contents = contents
- .iter()
- .map(|(path, contents)| (path.as_path(), contents.clone()))
- .collect::<Vec<_>>();
- if client.fs.metadata(&dot_git_dir).await?.is_none() {
- client.fs.create_dir(&dot_git_dir).await?;
- }
- client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
- }
+ log::info!(
+ "{}: writing git index for repo {:?}: {:?}",
+ client.username,
+ repo_path,
+ contents
+ );
- ClientOperation::WriteGitBranch {
- repo_path,
- new_branch,
- } => {
- if !client.fs.directories().contains(&repo_path) {
- return Err(TestError::Inapplicable);
+ let dot_git_dir = repo_path.join(".git");
+ let contents = contents
+ .iter()
+ .map(|(path, contents)| (path.as_path(), contents.clone()))
+ .collect::<Vec<_>>();
+ if client.fs.metadata(&dot_git_dir).await?.is_none() {
+ client.fs.create_dir(&dot_git_dir).await?;
+ }
+ client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
}
+ GitOperation::WriteGitBranch {
+ repo_path,
+ new_branch,
+ } => {
+ if !client.fs.directories().contains(&repo_path) {
+ return Err(TestError::Inapplicable);
+ }
- log::info!(
- "{}: writing git branch for repo {:?}: {:?}",
- client.username,
+ log::info!(
+ "{}: writing git branch for repo {:?}: {:?}",
+ client.username,
+ repo_path,
+ new_branch
+ );
+
+ let dot_git_dir = repo_path.join(".git");
+ if client.fs.metadata(&dot_git_dir).await?.is_none() {
+ client.fs.create_dir(&dot_git_dir).await?;
+ }
+ client.fs.set_branch_name(&dot_git_dir, new_branch).await;
+ }
+ GitOperation::WriteGitStatuses {
repo_path,
- new_branch
- );
+ statuses,
+ } => {
+ if !client.fs.directories().contains(&repo_path) {
+ return Err(TestError::Inapplicable);
+ }
+
+ log::info!(
+ "{}: writing git statuses for repo {:?}: {:?}",
+ client.username,
+ repo_path,
+ statuses
+ );
+
+ let dot_git_dir = repo_path.join(".git");
- let dot_git_dir = repo_path.join(".git");
- if client.fs.metadata(&dot_git_dir).await?.is_none() {
- client.fs.create_dir(&dot_git_dir).await?;
+ let statuses = statuses
+ .iter()
+ .map(|(path, val)| (path.as_path(), val.clone()))
+ .collect::<Vec<_>>();
+
+ if client.fs.metadata(&dot_git_dir).await?.is_none() {
+ client.fs.create_dir(&dot_git_dir).await?;
+ }
+
+ client
+ .fs
+ .set_status_for_repo(&dot_git_dir, statuses.as_slice())
+ .await;
}
- client.fs.set_branch_name(&dot_git_dir, new_branch).await;
- }
+ },
}
Ok(())
}
@@ -1178,6 +1212,13 @@ enum ClientOperation {
is_dir: bool,
content: String,
},
+ GitOperation {
+ operation: GitOperation,
+ },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+enum GitOperation {
WriteGitIndex {
repo_path: PathBuf,
contents: Vec<(PathBuf, String)>,
@@ -1186,6 +1227,10 @@ enum ClientOperation {
repo_path: PathBuf,
new_branch: Option<String>,
},
+ WriteGitStatuses {
+ repo_path: PathBuf,
+ statuses: Vec<(PathBuf, GitFileStatus)>,
+ },
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -1698,57 +1743,10 @@ impl TestPlan {
}
}
- // Update a git index
- 91..=93 => {
- let repo_path = client
- .fs
- .directories()
- .into_iter()
- .choose(&mut self.rng)
- .unwrap()
- .clone();
-
- let mut file_paths = client
- .fs
- .files()
- .into_iter()
- .filter(|path| path.starts_with(&repo_path))
- .collect::<Vec<_>>();
- let count = self.rng.gen_range(0..=file_paths.len());
- file_paths.shuffle(&mut self.rng);
- file_paths.truncate(count);
-
- let mut contents = Vec::new();
- for abs_child_file_path in &file_paths {
- let child_file_path = abs_child_file_path
- .strip_prefix(&repo_path)
- .unwrap()
- .to_path_buf();
- let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
- contents.push((child_file_path, new_base));
- }
-
- break ClientOperation::WriteGitIndex {
- repo_path,
- contents,
- };
- }
-
- // Update a git branch
- 94..=95 => {
- let repo_path = client
- .fs
- .directories()
- .choose(&mut self.rng)
- .unwrap()
- .clone();
-
- let new_branch = (self.rng.gen_range(0..10) > 3)
- .then(|| Alphanumeric.sample_string(&mut self.rng, 8));
-
- break ClientOperation::WriteGitBranch {
- repo_path,
- new_branch,
+ // Update a git related action
+ 91..=95 => {
+ break ClientOperation::GitOperation {
+ operation: self.generate_git_operation(client),
};
}
@@ -1786,6 +1784,86 @@ impl TestPlan {
})
}
+ fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation {
+ fn generate_file_paths(
+ repo_path: &Path,
+ rng: &mut StdRng,
+ client: &TestClient,
+ ) -> Vec<PathBuf> {
+ let mut paths = client
+ .fs
+ .files()
+ .into_iter()
+ .filter(|path| path.starts_with(repo_path))
+ .collect::<Vec<_>>();
+
+ let count = rng.gen_range(0..=paths.len());
+ paths.shuffle(rng);
+ paths.truncate(count);
+
+ paths
+ .iter()
+ .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
+ .collect::<Vec<_>>()
+ }
+
+ let repo_path = client
+ .fs
+ .directories()
+ .choose(&mut self.rng)
+ .unwrap()
+ .clone();
+
+ match self.rng.gen_range(0..100_u32) {
+ 0..=25 => {
+ let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
+
+ let contents = file_paths
+ .into_iter()
+ .map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16)))
+ .collect();
+
+ GitOperation::WriteGitIndex {
+ repo_path,
+ contents,
+ }
+ }
+ 26..=63 => {
+ let new_branch = (self.rng.gen_range(0..10) > 3)
+ .then(|| Alphanumeric.sample_string(&mut self.rng, 8));
+
+ GitOperation::WriteGitBranch {
+ repo_path,
+ new_branch,
+ }
+ }
+ 64..=100 => {
+ let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
+
+ let statuses = file_paths
+ .into_iter()
+ .map(|paths| {
+ (
+ paths,
+ match self.rng.gen_range(0..3_u32) {
+ 0 => GitFileStatus::Added,
+ 1 => GitFileStatus::Modified,
+ 2 => GitFileStatus::Conflict,
+ _ => unreachable!(),
+ },
+ )
+ })
+ .collect::<Vec<_>>();
+
+ GitOperation::WriteGitStatuses {
+ repo_path,
+ statuses,
+ }
+ }
+ _ => unreachable!(),
+ }
+ }
+
fn next_root_dir_name(&mut self, user_id: UserId) -> String {
let user_ix = self
.users
@@ -18,7 +18,6 @@ use gpui::{
ViewContext, ViewHandle, WeakViewHandle,
};
use project::Project;
-use settings::Settings;
use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme};
use util::ResultExt;
@@ -70,7 +69,7 @@ impl View for CollabTitlebarItem {
};
let project = self.project.read(cx);
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let mut left_container = Flex::row();
let mut right_container = Flex::row().align_children_center();
@@ -298,7 +297,7 @@ impl CollabTitlebarItem {
}
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
let item_style = theme.context_menu.item.disabled_style().clone();
self.user_menu.update(cx, |user_menu, cx| {
@@ -866,7 +865,7 @@ impl CollabTitlebarItem {
) -> Option<AnyElement<Self>> {
enum ConnectionStatusButton {}
- let theme = &cx.global::<Settings>().theme.clone();
+ let theme = &theme::current(cx).clone();
match status {
client::Status::ConnectionError
| client::Status::ConnectionLost
@@ -1,7 +1,6 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::Settings;
use std::sync::Arc;
use util::TryFutureExt;
@@ -98,7 +97,7 @@ impl PickerDelegate for ContactFinderDelegate {
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
- let theme = &cx.global::<Settings>().theme;
+ let theme = &theme::current(cx);
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);
@@ -14,7 +14,6 @@ use gpui::{
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
use serde::Deserialize;
-use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton;
use workspace::Workspace;
@@ -192,7 +191,7 @@ impl ContactList {
.detach();
let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let is_selected = this.selection == Some(ix);
let current_project_id = this.project.read(cx).remote_id();
@@ -1313,7 +1312,7 @@ impl View for ContactList {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum AddContact {}
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
Flex::column()
.with_child(
@@ -9,7 +9,6 @@ use gpui::{
};
use picker::PickerEvent;
use project::Project;
-use settings::Settings;
use workspace::Workspace;
actions!(contacts_popover, [ToggleContactFinder]);
@@ -108,7 +107,7 @@ impl View for ContactsPopover {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let child = match &self.child {
Child::ContactList(child) => ChildView::new(child, cx),
Child::ContactFinder(child) => ChildView::new(child, cx),
@@ -9,7 +9,6 @@ use gpui::{
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AnyElement, AppContext, Entity, View, ViewContext,
};
-use settings::Settings;
use util::ResultExt;
use workspace::AppState;
@@ -26,7 +25,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
if let Some(incoming_call) = incoming_call {
const PADDING: f32 = 16.;
let window_size = cx.read(|cx| {
- let theme = &cx.global::<Settings>().theme.incoming_call_notification;
+ let theme = &theme::current(cx).incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
@@ -107,7 +106,7 @@ impl IncomingCallNotification {
}
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &cx.global::<Settings>().theme.incoming_call_notification;
+ let theme = &theme::current(cx).incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
.call
@@ -171,10 +170,11 @@ impl IncomingCallNotification {
enum Accept {}
enum Decline {}
+ let theme = theme::current(cx);
Flex::column()
.with_child(
- MouseEventHandler::<Accept, Self>::new(0, cx, |_, cx| {
- let theme = &cx.global::<Settings>().theme.incoming_call_notification;
+ MouseEventHandler::<Accept, Self>::new(0, cx, |_, _| {
+ let theme = &theme.incoming_call_notification;
Label::new("Accept", theme.accept_button.text.clone())
.aligned()
.contained()
@@ -187,8 +187,8 @@ impl IncomingCallNotification {
.flex(1., true),
)
.with_child(
- MouseEventHandler::<Decline, Self>::new(0, cx, |_, cx| {
- let theme = &cx.global::<Settings>().theme.incoming_call_notification;
+ MouseEventHandler::<Decline, Self>::new(0, cx, |_, _| {
+ let theme = &theme.incoming_call_notification;
Label::new("Decline", theme.decline_button.text.clone())
.aligned()
.contained()
@@ -201,12 +201,7 @@ impl IncomingCallNotification {
.flex(1., true),
)
.constrained()
- .with_width(
- cx.global::<Settings>()
- .theme
- .incoming_call_notification
- .button_width,
- )
+ .with_width(theme.incoming_call_notification.button_width)
.into_any()
}
}
@@ -221,12 +216,7 @@ impl View for IncomingCallNotification {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let background = cx
- .global::<Settings>()
- .theme
- .incoming_call_notification
- .background;
-
+ let background = theme::current(cx).incoming_call_notification.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
@@ -4,7 +4,6 @@ use gpui::{
platform::{CursorStyle, MouseButton},
AnyElement, Element, View, ViewContext,
};
-use settings::Settings;
use std::sync::Arc;
enum Dismiss {}
@@ -22,7 +21,7 @@ where
F: 'static + Fn(&mut V, &mut ViewContext<V>),
V: View,
{
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let theme = &theme.contact_notification;
Flex::column()
@@ -7,7 +7,6 @@ use gpui::{
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AppContext, Entity, View, ViewContext,
};
-use settings::Settings;
use std::sync::{Arc, Weak};
use workspace::AppState;
@@ -22,7 +21,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
worktree_root_names,
} => {
const PADDING: f32 = 16.;
- let theme = &cx.global::<Settings>().theme.project_shared_notification;
+ let theme = &theme::current(cx).project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() {
@@ -110,7 +109,7 @@ impl ProjectSharedNotification {
}
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &cx.global::<Settings>().theme.project_shared_notification;
+ let theme = &theme::current(cx).project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::from_data(avatar)
@@ -168,10 +167,11 @@ impl ProjectSharedNotification {
enum Open {}
enum Dismiss {}
+ let theme = theme::current(cx);
Flex::column()
.with_child(
- MouseEventHandler::<Open, Self>::new(0, cx, |_, cx| {
- let theme = &cx.global::<Settings>().theme.project_shared_notification;
+ MouseEventHandler::<Open, Self>::new(0, cx, |_, _| {
+ let theme = &theme.project_shared_notification;
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
@@ -182,8 +182,8 @@ impl ProjectSharedNotification {
.flex(1., true),
)
.with_child(
- MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, cx| {
- let theme = &cx.global::<Settings>().theme.project_shared_notification;
+ MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, _| {
+ let theme = &theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
@@ -196,12 +196,7 @@ impl ProjectSharedNotification {
.flex(1., true),
)
.constrained()
- .with_width(
- cx.global::<Settings>()
- .theme
- .project_shared_notification
- .button_width,
- )
+ .with_width(theme.project_shared_notification.button_width)
.into_any()
}
}
@@ -216,11 +211,7 @@ impl View for ProjectSharedNotification {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
- let background = cx
- .global::<Settings>()
- .theme
- .project_shared_notification
- .background;
+ let background = theme::current(cx).project_shared_notification.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
@@ -6,7 +6,7 @@ use gpui::{
platform::{Appearance, MouseButton},
AnyElement, AppContext, Element, Entity, View, ViewContext,
};
-use settings::Settings;
+use workspace::WorkspaceSettings;
pub fn init(cx: &mut AppContext) {
let active_call = ActiveCall::global(cx);
@@ -15,7 +15,9 @@ pub fn init(cx: &mut AppContext) {
cx.observe(&active_call, move |call, cx| {
if let Some(room) = call.read(cx).room() {
if room.read(cx).is_screen_sharing() {
- if status_indicator.is_none() && cx.global::<Settings>().show_call_status_icon {
+ if status_indicator.is_none()
+ && settings::get::<WorkspaceSettings>(cx).show_call_status_icon
+ {
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
}
} else if let Some((window_id, _)) = status_indicator.take() {
@@ -23,6 +23,7 @@ workspace = { path = "../workspace" }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
serde_json.workspace = true
workspace = { path = "../workspace", features = ["test-support"] }
@@ -5,7 +5,6 @@ use gpui::{
ViewContext,
};
use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::Settings;
use std::cmp;
use util::ResultExt;
use workspace::Workspace;
@@ -185,8 +184,7 @@ impl PickerDelegate for CommandPaletteDelegate {
) -> AnyElement<Picker<Self>> {
let mat = &self.matches[ix];
let command = &self.actions[mat.candidate_id];
- let settings = cx.global::<Settings>();
- let theme = &settings.theme;
+ let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
let keystroke_spacing = theme.command_palette.keystroke_spacing;
@@ -294,14 +292,7 @@ mod tests {
#[gpui::test]
async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
- deterministic.forbid_parking();
- let app_state = cx.update(AppState::test);
-
- cx.update(|cx| {
- editor::init(cx);
- workspace::init(app_state.clone(), cx);
- init(cx);
- });
+ let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -369,4 +360,16 @@ mod tests {
assert!(palette.delegate().matches.is_empty())
});
}
+
+ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+ cx.update(|cx| {
+ let app_state = AppState::test(cx);
+ theme::init((), cx);
+ language::init(cx);
+ editor::init(cx);
+ workspace::init(app_state.clone(), cx);
+ init(cx);
+ app_state
+ })
+ }
}
@@ -8,7 +8,6 @@ use gpui::{
View, ViewContext,
};
use menu::*;
-use settings::Settings;
use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
pub fn init(cx: &mut AppContext) {
@@ -323,7 +322,7 @@ impl ContextMenu {
}
fn render_menu_for_measurement(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
- let style = cx.global::<Settings>().theme.context_menu.clone();
+ let style = theme::current(cx).context_menu.clone();
Flex::row()
.with_child(
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
@@ -403,7 +402,7 @@ impl ContextMenu {
enum Menu {}
enum MenuItem {}
- let style = cx.global::<Settings>().theme.context_menu.clone();
+ let style = theme::current(cx).context_menu.clone();
MouseEventHandler::<Menu, ContextMenu>::new(0, cx, |_, cx| {
Flex::column()
@@ -10,6 +10,7 @@ use gpui::{
actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
};
use language::{
+ language_settings::{all_language_settings, language_settings},
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
ToPointUtf16,
};
@@ -17,7 +18,7 @@ use log::{debug, error};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;
use request::{LogMessage, StatusNotification};
-use settings::Settings;
+use settings::SettingsStore;
use smol::{fs, io::BufReader, stream::StreamExt};
use std::{
ffi::OsString,
@@ -258,7 +259,7 @@ impl RegisteredBuffer {
#[derive(Debug)]
pub struct Completion {
- uuid: String,
+ pub uuid: String,
pub range: Range<Anchor>,
pub text: String,
}
@@ -302,56 +303,34 @@ impl Copilot {
node_runtime: Arc<NodeRuntime>,
cx: &mut ModelContext<Self>,
) -> Self {
- cx.observe_global::<Settings, _>({
- let http = http.clone();
- let node_runtime = node_runtime.clone();
- move |this, cx| {
- if cx.global::<Settings>().features.copilot {
- if matches!(this.server, CopilotServer::Disabled) {
- let start_task = cx
- .spawn({
- let http = http.clone();
- let node_runtime = node_runtime.clone();
- move |this, cx| {
- Self::start_language_server(http, node_runtime, this, cx)
- }
- })
- .shared();
- this.server = CopilotServer::Starting { task: start_task };
- cx.notify();
- }
- } else {
- this.server = CopilotServer::Disabled;
- cx.notify();
- }
- }
- })
- .detach();
-
- if cx.global::<Settings>().features.copilot {
- let start_task = cx
- .spawn({
- let http = http.clone();
- let node_runtime = node_runtime.clone();
- move |this, cx| async {
- Self::start_language_server(http, node_runtime, this, cx).await
- }
- })
- .shared();
+ let mut this = Self {
+ http,
+ node_runtime,
+ server: CopilotServer::Disabled,
+ buffers: Default::default(),
+ };
+ this.enable_or_disable_copilot(cx);
+ cx.observe_global::<SettingsStore, _>(move |this, cx| this.enable_or_disable_copilot(cx))
+ .detach();
+ this
+ }
- Self {
- http,
- node_runtime,
- server: CopilotServer::Starting { task: start_task },
- buffers: Default::default(),
+ fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
+ let http = self.http.clone();
+ let node_runtime = self.node_runtime.clone();
+ if all_language_settings(cx).copilot_enabled(None, None) {
+ if matches!(self.server, CopilotServer::Disabled) {
+ let start_task = cx
+ .spawn({
+ move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
+ })
+ .shared();
+ self.server = CopilotServer::Starting { task: start_task };
+ cx.notify();
}
} else {
- Self {
- http,
- node_runtime,
- server: CopilotServer::Disabled,
- buffers: Default::default(),
- }
+ self.server = CopilotServer::Disabled;
+ cx.notify();
}
}
@@ -805,13 +784,13 @@ impl Copilot {
let snapshot = registered_buffer.report_changes(buffer, cx);
let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone();
- let settings = cx.global::<Settings>();
let position = position.to_point_utf16(buffer);
- let language = buffer.language_at(position);
- let language_name = language.map(|language| language.name());
- let language_name = language_name.as_deref();
- let tab_size = settings.tab_size(language_name);
- let hard_tabs = settings.hard_tabs(language_name);
+ let settings = language_settings(
+ buffer.language_at(position).map(|l| l.name()).as_deref(),
+ cx,
+ );
+ let tab_size = settings.tab_size;
+ let hard_tabs = settings.hard_tabs;
let relative_path = buffer
.file()
.map(|file| file.path().to_path_buf())
@@ -6,7 +6,6 @@ use gpui::{
AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
ViewHandle,
};
-use settings::Settings;
use theme::ui::modal;
#[derive(PartialEq, Eq, Debug, Clone)]
@@ -68,7 +67,7 @@ fn create_copilot_auth_window(
cx: &mut AppContext,
status: &Status,
) -> ViewHandle<CopilotCodeVerification> {
- let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
+ let window_size = theme::current(cx).copilot.modal.dimensions();
let window_options = WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
titlebar: None,
@@ -339,7 +338,7 @@ impl View for CopilotCodeVerification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum ConnectModal {}
- let style = cx.global::<Settings>().theme.clone();
+ let style = theme::current(cx).clone();
modal::<ConnectModal, _, _, _, _>(
"Connect Copilot to Zed",
@@ -12,8 +12,10 @@ doctest = false
assets = { path = "../assets" }
copilot = { path = "../copilot" }
editor = { path = "../editor" }
+fs = { path = "../fs" }
context_menu = { path = "../context_menu" }
gpui = { path = "../gpui" }
+language = { path = "../language" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
@@ -21,3 +23,6 @@ workspace = { path = "../workspace" }
anyhow.workspace = true
smol.workspace = true
futures.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -2,13 +2,15 @@ use anyhow::Result;
use context_menu::{ContextMenu, ContextMenuItem};
use copilot::{Copilot, SignOut, Status};
use editor::{scroll::autoscroll::Autoscroll, Editor};
+use fs::Fs;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
-use settings::{settings_file::SettingsFile, Settings};
+use language::language_settings::{self, all_language_settings, AllLanguageSettings};
+use settings::{update_settings_file, SettingsStore};
use std::{path::Path, sync::Arc};
use util::{paths, ResultExt};
use workspace::{
@@ -26,6 +28,7 @@ pub struct CopilotButton {
editor_enabled: Option<bool>,
language: Option<Arc<str>>,
path: Option<Arc<Path>>,
+ fs: Arc<dyn Fs>,
}
impl Entity for CopilotButton {
@@ -38,13 +41,12 @@ impl View for CopilotButton {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let settings = cx.global::<Settings>();
-
- if !settings.features.copilot {
+ let all_language_settings = &all_language_settings(cx);
+ if !all_language_settings.copilot.feature_enabled {
return Empty::new().into_any();
}
- let theme = settings.theme.clone();
+ let theme = theme::current(cx).clone();
let active = self.popup_menu.read(cx).visible();
let Some(copilot) = Copilot::global(cx) else {
return Empty::new().into_any();
@@ -53,7 +55,7 @@ impl View for CopilotButton {
let enabled = self
.editor_enabled
- .unwrap_or(settings.show_copilot_suggestions(None, None));
+ .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
Stack::new()
.with_child(
@@ -143,7 +145,7 @@ impl View for CopilotButton {
}
impl CopilotButton {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
+ pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
let button_view_id = cx.view_id();
let menu = cx.add_view(|cx| {
let mut menu = ContextMenu::new(button_view_id, cx);
@@ -155,7 +157,7 @@ impl CopilotButton {
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
- cx.observe_global::<Settings, _>(move |_, cx| cx.notify())
+ cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify())
.detach();
Self {
@@ -164,17 +166,19 @@ impl CopilotButton {
editor_enabled: None,
language: None,
path: None,
+ fs,
}
}
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut menu_options = Vec::with_capacity(2);
+ let fs = self.fs.clone();
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
initiate_sign_in(cx)
}));
- menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| {
- hide_copilot(cx)
+ menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
+ hide_copilot(fs.clone(), cx)
}));
self.popup_menu.update(cx, |menu, cx| {
@@ -188,22 +192,26 @@ impl CopilotButton {
}
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
- let settings = cx.global::<Settings>();
-
+ let fs = self.fs.clone();
let mut menu_options = Vec::with_capacity(8);
if let Some(language) = self.language.clone() {
- let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref()));
+ let fs = fs.clone();
+ let language_enabled =
+ language_settings::language_settings(Some(language.as_ref()), cx)
+ .show_copilot_suggestions;
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language
),
- move |cx| toggle_copilot_for_language(language.clone(), cx),
+ move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
));
}
+ let settings = settings::get::<AllLanguageSettings>(cx);
+
if let Some(path) = self.path.as_ref() {
let path_enabled = settings.copilot_enabled_for_path(path);
let path = path.clone();
@@ -228,19 +236,19 @@ impl CopilotButton {
));
}
- let globally_enabled = cx.global::<Settings>().features.copilot;
+ let globally_enabled = settings.copilot_enabled(None, None);
menu_options.push(ContextMenuItem::handler(
if globally_enabled {
"Hide Suggestions for All Files"
} else {
"Show Suggestions for All Files"
},
- |cx| toggle_copilot_globally(cx),
+ move |cx| toggle_copilot_globally(fs.clone(), cx),
));
menu_options.push(ContextMenuItem::Separator);
- let icon_style = settings.theme.copilot.out_link_icon.clone();
+ let icon_style = theme::current(cx).copilot.out_link_icon.clone();
menu_options.push(ContextMenuItem::action(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
@@ -266,22 +274,19 @@ impl CopilotButton {
pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
-
let snapshot = editor.buffer().read(cx).snapshot(cx);
- let settings = cx.global::<Settings>();
let suggestion_anchor = editor.selections.newest_anchor().start;
-
let language_name = snapshot
.language_at(suggestion_anchor)
.map(|language| language.name());
- let path = snapshot
- .file_at(suggestion_anchor)
- .map(|file| file.path().clone());
+ let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
- self.editor_enabled =
- Some(settings.show_copilot_suggestions(language_name.as_deref(), path.as_deref()));
+ self.editor_enabled = Some(
+ all_language_settings(cx)
+ .copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
+ );
self.language = language_name;
- self.path = path;
+ self.path = path.cloned();
cx.notify()
}
@@ -310,7 +315,7 @@ async fn configure_disabled_globs(
let settings_editor = workspace
.update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
- Settings::initial_user_settings_content(&assets::Assets)
+ settings::initial_user_settings_content(&assets::Assets)
.as_ref()
.into()
})
@@ -322,16 +327,17 @@ async fn configure_disabled_globs(
settings_editor.downgrade().update(&mut cx, |item, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
- let edits = SettingsFile::update_unsaved(&text, cx, |file| {
+ let settings = cx.global::<SettingsStore>();
+ let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
let copilot = file.copilot.get_or_insert_with(Default::default);
let globs = copilot.disabled_globs.get_or_insert_with(|| {
- cx.global::<Settings>()
+ settings
+ .get::<AllLanguageSettings>(None)
.copilot
.disabled_globs
- .clone()
.iter()
- .map(|glob| glob.as_str().to_string())
- .collect::<Vec<_>>()
+ .map(|glob| glob.glob().to_string())
+ .collect()
});
if let Some(path_to_disable) = &path_to_disable {
@@ -356,32 +362,26 @@ async fn configure_disabled_globs(
anyhow::Ok(())
}
-fn toggle_copilot_globally(cx: &mut AppContext) {
- let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None);
- SettingsFile::update(cx, move |file_contents| {
- file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
+fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
});
}
-fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
- let show_copilot_suggestions = cx
- .global::<Settings>()
- .show_copilot_suggestions(Some(&language), None);
-
- SettingsFile::update(cx, move |file_contents| {
- file_contents.languages.insert(
- language,
- settings::EditorSettings {
- show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
- ..Default::default()
- },
- );
+fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.languages
+ .entry(language)
+ .or_default()
+ .show_copilot_suggestions = Some(!show_copilot_suggestions);
});
}
-fn hide_copilot(cx: &mut AppContext) {
- SettingsFile::update(cx, move |file_contents| {
- file_contents.features.copilot = Some(false)
+fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.features.get_or_insert(Default::default()).copilot = Some(false);
});
}
@@ -31,6 +31,7 @@ language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }
serde_json.workspace = true
unindent.workspace = true
@@ -20,7 +20,6 @@ use language::{
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json;
-use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
@@ -30,6 +29,7 @@ use std::{
path::PathBuf,
sync::Arc,
};
+use theme::ThemeSettings;
use util::TryFutureExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@@ -89,7 +89,7 @@ impl View for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if self.path_states.is_empty() {
- let theme = &cx.global::<Settings>().theme.project_diagnostics;
+ let theme = &theme::current(cx).project_diagnostics;
Label::new("No problems in workspace", theme.empty_message.clone())
.aligned()
.contained()
@@ -537,7 +537,7 @@ impl Item for ProjectDiagnosticsEditor {
render_summary(
&self.summary,
&style.label.text,
- &cx.global::<Settings>().theme.project_diagnostics,
+ &theme::current(cx).project_diagnostics,
)
}
@@ -679,10 +679,10 @@ impl Item for ProjectDiagnosticsEditor {
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
Arc::new(move |cx| {
- let settings = cx.global::<Settings>();
+ let settings = settings::get::<ThemeSettings>(cx);
let theme = &settings.theme.editor;
let style = theme.diagnostic_header.clone();
- let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
+ let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
let icon_width = cx.em_width * style.icon_width_factor;
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
Svg::new("icons/circle_x_mark_12.svg")
@@ -818,33 +818,35 @@ mod tests {
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
use project::FakeFs;
use serde_json::json;
+ use settings::SettingsStore;
use unindent::Unindent as _;
#[gpui::test]
async fn test_diagnostics(cx: &mut TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/test",
json!({
"consts.rs": "
- const a: i32 = 'a';
- const b: i32 = c;
- "
+ const a: i32 = 'a';
+ const b: i32 = c;
+ "
.unindent(),
"main.rs": "
- fn main() {
- let x = vec![];
- let y = vec![];
- a(x);
- b(y);
- // comment 1
- // comment 2
- c(y);
- d(x);
- }
- "
+ fn main() {
+ let x = vec![];
+ let y = vec![];
+ a(x);
+ b(y);
+ // comment 1
+ // comment 2
+ c(y);
+ d(x);
+ }
+ "
.unindent(),
}),
)
@@ -1225,7 +1227,8 @@ mod tests {
#[gpui::test]
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/test",
@@ -1489,6 +1492,16 @@ mod tests {
});
}
+ fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ language::init(cx);
+ client::init_settings(cx);
+ workspace::init_settings(cx);
+ });
+ }
+
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
@@ -7,7 +7,6 @@ use gpui::{
};
use language::Diagnostic;
use lsp::LanguageServerId;
-use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::ProjectDiagnosticsEditor;
@@ -92,13 +91,12 @@ impl View for DiagnosticIndicator {
enum Summary {}
enum Message {}
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
let in_progress = !self.in_progress_checks.is_empty();
let mut element = Flex::row().with_child(
MouseEventHandler::<Summary, _>::new(0, cx, |state, cx| {
- let style = cx
- .global::<Settings>()
- .theme
+ let theme = theme::current(cx);
+ let style = theme
.workspace
.status_bar
.diagnostic_summary
@@ -184,7 +182,7 @@ impl View for DiagnosticIndicator {
.into_any(),
);
- let style = &cx.global::<Settings>().theme.workspace.status_bar;
+ let style = &theme::current(cx).workspace.status_bar;
let item_spacing = style.item_spacing;
if in_progress {
@@ -58,6 +58,7 @@ parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
rand = { workspace = true, optional = true }
+schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
@@ -80,7 +81,6 @@ workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
-glob.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter = "0.20"
@@ -1,8 +1,8 @@
-use std::time::Duration;
-
+use crate::EditorSettings;
use gpui::{Entity, ModelContext};
-use settings::Settings;
+use settings::SettingsStore;
use smol::Timer;
+use std::time::Duration;
pub struct BlinkManager {
blink_interval: Duration,
@@ -15,8 +15,8 @@ pub struct BlinkManager {
impl BlinkManager {
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
- cx.observe_global::<Settings, _>(move |this, cx| {
- // Make sure we blink the cursors if the setting is re-enabled
+ // Make sure we blink the cursors if the setting is re-enabled
+ cx.observe_global::<SettingsStore, _>(move |this, cx| {
this.blink_cursors(this.blink_epoch, cx)
})
.detach();
@@ -64,7 +64,7 @@ impl BlinkManager {
}
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
- if cx.global::<Settings>().cursor_blink {
+ if settings::get::<EditorSettings>(cx).cursor_blink {
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
self.visible = !self.visible;
cx.notify();
@@ -13,8 +13,9 @@ use gpui::{
fonts::{FontId, HighlightStyle},
Entity, ModelContext, ModelHandle,
};
-use language::{OffsetUtf16, Point, Subscription as BufferSubscription};
-use settings::Settings;
+use language::{
+ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
+};
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
pub use suggestion_map::Suggestion;
use suggestion_map::SuggestionMap;
@@ -276,8 +277,7 @@ impl DisplayMap {
.as_singleton()
.and_then(|buffer| buffer.read(cx).language())
.map(|language| language.name());
-
- cx.global::<Settings>().tab_size(language_name.as_deref())
+ language_settings(language_name.as_deref(), cx).tab_size
}
#[cfg(test)]
@@ -844,8 +844,12 @@ pub mod tests {
use super::*;
use crate::{movement, test::marked_display_snapshot};
use gpui::{color::Color, elements::*, test::observe, AppContext};
- use language::{Buffer, Language, LanguageConfig, SelectionGoal};
+ use language::{
+ language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
+ Buffer, Language, LanguageConfig, SelectionGoal,
+ };
use rand::{prelude::*, Rng};
+ use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use theme::SyntaxTheme;
@@ -882,9 +886,7 @@ pub mod tests {
log::info!("wrap width: {:?}", wrap_width);
cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
- cx.set_global(settings)
+ init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
});
let buffer = cx.update(|cx| {
@@ -939,9 +941,11 @@ pub mod tests {
tab_size = *tab_sizes.choose(&mut rng).unwrap();
log::info!("setting tab size to {:?}", tab_size);
cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
- cx.set_global(settings)
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+ s.defaults.tab_size = NonZeroU32::new(tab_size);
+ });
+ });
});
}
30..=44 => {
@@ -1119,7 +1123,7 @@ pub mod tests {
#[gpui::test(retries = 5)]
fn test_soft_wraps(cx: &mut AppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
- cx.foreground().forbid_parking();
+ init_test(cx, |_| {});
let font_cache = cx.font_cache();
@@ -1131,7 +1135,6 @@ pub mod tests {
.unwrap();
let font_size = 12.0;
let wrap_width = Some(64.);
- cx.set_global(Settings::test(cx));
let text = "one two three four five\nsix seven eight";
let buffer = MultiBuffer::build_simple(text, cx);
@@ -1211,7 +1214,8 @@ pub mod tests {
#[gpui::test]
fn test_text_chunks(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx, |_| {});
+
let text = sample_text(6, 6, 'a');
let buffer = MultiBuffer::build_simple(&text, cx);
let family_id = cx
@@ -1225,6 +1229,7 @@ pub mod tests {
let font_size = 14.0;
let map =
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
+
buffer.update(cx, |buffer, cx| {
buffer.edit(
vec![
@@ -1289,11 +1294,8 @@ pub mod tests {
.unwrap(),
);
language.set_theme(&theme);
- cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
- cx.set_global(settings);
- });
+
+ cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
@@ -1382,7 +1384,7 @@ pub mod tests {
);
language.set_theme(&theme);
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
@@ -1429,9 +1431,8 @@ pub mod tests {
#[gpui::test]
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
- cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
+ cx.update(|cx| init_test(cx, |_| {}));
- cx.update(|cx| cx.set_global(Settings::test(cx)));
let theme = SyntaxTheme::new(vec![
("operator".to_string(), Color::red().into()),
("string".to_string(), Color::green().into()),
@@ -1510,7 +1511,8 @@ pub mod tests {
#[gpui::test]
fn test_clip_point(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx, |_| {});
+
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
@@ -1559,7 +1561,7 @@ pub mod tests {
#[gpui::test]
fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx, |_| {});
fn assert(text: &str, cx: &mut gpui::AppContext) {
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
@@ -1578,7 +1580,8 @@ pub mod tests {
#[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx, |_| {});
+
let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = MultiBuffer::build_simple(text, cx);
let font_cache = cx.font_cache();
@@ -1639,7 +1642,8 @@ pub mod tests {
#[gpui::test]
fn test_max_point(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx, |_| {});
+
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let font_cache = cx.font_cache();
let family_id = font_cache
@@ -1718,4 +1722,13 @@ pub mod tests {
}
chunks
}
+
+ fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+ cx.foreground().forbid_parking();
+ cx.set_global(SettingsStore::test(cx));
+ language::init(cx);
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, f);
+ });
+ }
}
@@ -993,7 +993,7 @@ mod tests {
use crate::multi_buffer::MultiBuffer;
use gpui::{elements::Empty, Element};
use rand::prelude::*;
- use settings::Settings;
+ use settings::SettingsStore;
use std::env;
use util::RandomCharIter;
@@ -1013,7 +1013,7 @@ mod tests {
#[gpui::test]
fn test_basic_blocks(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
let family_id = cx
.font_cache()
@@ -1189,7 +1189,7 @@ mod tests {
#[gpui::test]
fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
let family_id = cx
.font_cache()
@@ -1239,7 +1239,7 @@ mod tests {
#[gpui::test(iterations = 100)]
fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
@@ -1647,6 +1647,11 @@ mod tests {
}
}
+ fn init_test(cx: &mut gpui::AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ }
+
impl TransformBlock {
fn as_custom(&self) -> Option<&Block> {
match self {
@@ -1204,7 +1204,7 @@ mod tests {
use crate::{MultiBuffer, ToPoint};
use collections::HashSet;
use rand::prelude::*;
- use settings::Settings;
+ use settings::SettingsStore;
use std::{cmp::Reverse, env, mem, sync::Arc};
use sum_tree::TreeMap;
use util::test::sample_text;
@@ -1213,7 +1213,7 @@ mod tests {
#[gpui::test]
fn test_basic_folds(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1286,7 +1286,7 @@ mod tests {
#[gpui::test]
fn test_adjacent_folds(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1349,7 +1349,7 @@ mod tests {
#[gpui::test]
fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1400,7 +1400,7 @@ mod tests {
#[gpui::test(iterations = 100)]
fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
@@ -1676,6 +1676,10 @@ mod tests {
assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
}
+ fn init_test(cx: &mut gpui::AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ }
+
impl FoldMap {
fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
let buffer = self.buffer.lock().clone();
@@ -578,7 +578,7 @@ mod tests {
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
use gpui::AppContext;
use rand::{prelude::StdRng, Rng};
- use settings::Settings;
+ use settings::SettingsStore;
use std::{
env,
ops::{Bound, RangeBounds},
@@ -631,7 +631,8 @@ mod tests {
#[gpui::test(iterations = 100)]
fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
@@ -834,6 +835,11 @@ mod tests {
}
}
+ fn init_test(cx: &mut AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ }
+
impl SuggestionMap {
pub fn randomly_mutate(
&self,
@@ -1043,16 +1043,16 @@ mod tests {
};
use gpui::test::observe;
use rand::prelude::*;
- use settings::Settings;
+ use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{cmp, env, num::NonZeroU32};
use text::Rope;
#[gpui::test(iterations = 100)]
async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx);
+
cx.foreground().set_block_on_ticks(0..=50);
- cx.foreground().forbid_parking();
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
@@ -1287,6 +1287,14 @@ mod tests {
wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
}
+ fn init_test(cx: &mut gpui::TestAppContext) {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ });
+ }
+
fn wrap_text(
unwrapped_text: &str,
wrap_width: Option<f32>,
@@ -1,5 +1,6 @@
mod blink_manager;
pub mod display_map;
+mod editor_settings;
mod element;
mod git;
@@ -19,15 +20,17 @@ mod editor_tests;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
+use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Result};
use blink_manager::BlinkManager;
-use client::ClickhouseEvent;
+use client::{ClickhouseEvent, TelemetrySettings};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use copilot::Copilot;
pub use display_map::DisplayPoint;
use display_map::*;
+pub use editor_settings::EditorSettings;
pub use element::*;
use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -51,6 +54,7 @@ pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
pub use language::{char_kind, CharKind};
use language::{
+ language_settings::{self, all_language_settings},
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
@@ -70,7 +74,7 @@ use scroll::{
};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::SettingsStore;
use smallvec::SmallVec;
use snippet::Snippet;
use std::{
@@ -85,7 +89,7 @@ use std::{
time::{Duration, Instant},
};
pub use sum_tree::Bias;
-use theme::{DiagnosticStyle, Theme};
+use theme::{DiagnosticStyle, Theme, ThemeSettings};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, ViewId, Workspace};
@@ -286,7 +290,12 @@ pub enum Direction {
Next,
}
+pub fn init_settings(cx: &mut AppContext) {
+ settings::register::<EditorSettings>(cx);
+}
+
pub fn init(cx: &mut AppContext) {
+ init_settings(cx);
cx.add_action(Editor::new_file);
cx.add_action(Editor::cancel);
cx.add_action(Editor::newline);
@@ -436,7 +445,7 @@ pub enum EditorMode {
Full,
}
-#[derive(Clone)]
+#[derive(Clone, Debug)]
pub enum SoftWrap {
None,
EditorWidth,
@@ -471,7 +480,7 @@ pub struct Editor {
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
ime_transaction: Option<TransactionId>,
active_diagnostics: Option<ActiveDiagnosticGroup>,
- soft_wrap_mode_override: Option<settings::SoftWrap>,
+ soft_wrap_mode_override: Option<language_settings::SoftWrap>,
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
override_text_style: Option<Box<OverrideTextStyle>>,
project: Option<ModelHandle<Project>>,
@@ -516,6 +525,15 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll,
}
+impl EditorSnapshot {
+ fn has_scrollbar_info(&self) -> bool {
+ self.buffer_snapshot
+ .git_diff_hunks_in_range(0..self.max_point().row())
+ .next()
+ .is_some()
+ }
+}
+
#[derive(Clone, Debug)]
struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>,
@@ -1229,8 +1247,8 @@ impl Editor {
) -> Self {
let editor_view_id = cx.view_id();
let display_map = cx.add_model(|cx| {
- let settings = cx.global::<Settings>();
- let style = build_style(&*settings, get_field_editor_theme.as_deref(), None, cx);
+ let settings = settings::get::<ThemeSettings>(cx);
+ let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx);
DisplayMap::new(
buffer.clone(),
style.text.font_id,
@@ -1247,7 +1265,17 @@ impl Editor {
let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let soft_wrap_mode_override =
- (mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None);
+ (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
+
+ let mut project_subscription = None;
+ if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
+ if let Some(project) = project.as_ref() {
+ project_subscription = Some(cx.observe(project, |_, _, cx| {
+ cx.emit(Event::TitleChanged);
+ }))
+ }
+ }
+
let mut this = Self {
handle: cx.weak_handle(),
buffer: buffer.clone(),
@@ -1301,9 +1329,14 @@ impl Editor {
cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
- cx.observe_global::<Settings, _>(Self::settings_changed),
+ cx.observe_global::<SettingsStore, _>(Self::settings_changed),
],
};
+
+ if let Some(project_subscription) = project_subscription {
+ this._subscriptions.push(project_subscription);
+ }
+
this.end_selection(cx);
this.scroll_manager.show_scrollbar(cx);
@@ -1315,7 +1348,7 @@ impl Editor {
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
}
- this.report_editor_event("open", cx);
+ this.report_editor_event("open", None, cx);
this
}
@@ -1395,7 +1428,7 @@ impl Editor {
fn style(&self, cx: &AppContext) -> EditorStyle {
build_style(
- cx.global::<Settings>(),
+ settings::get::<ThemeSettings>(cx),
self.get_field_editor_theme.as_deref(),
self.override_text_style.as_deref(),
cx,
@@ -2353,7 +2386,7 @@ impl Editor {
}
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
- if !cx.global::<Settings>().show_completions_on_input {
+ if !settings::get::<EditorSettings>(cx).show_completions_on_input {
return;
}
@@ -3082,6 +3115,8 @@ impl Editor {
copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx);
+
+ self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
}
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
cx.notify();
@@ -3099,6 +3134,8 @@ impl Editor {
copilot.discard_completions(&self.copilot_state.completions, cx)
})
.detach_and_log_err(cx);
+
+ self.report_copilot_event(None, false, cx)
}
self.display_map
@@ -3116,17 +3153,12 @@ impl Editor {
snapshot: &MultiBufferSnapshot,
cx: &mut ViewContext<Self>,
) -> bool {
- let settings = cx.global::<Settings>();
-
- let path = snapshot.file_at(location).map(|file| file.path());
+ let path = snapshot.file_at(location).map(|file| file.path().as_ref());
let language_name = snapshot
.language_at(location)
.map(|language| language.name());
- if !settings.show_copilot_suggestions(language_name.as_deref(), path.map(|p| p.as_ref())) {
- return false;
- }
-
- true
+ let settings = all_language_settings(cx);
+ settings.copilot_enabled(language_name.as_deref(), path)
}
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@@ -3427,12 +3459,9 @@ impl Editor {
{
let indent_size =
buffer.indent_size_for_line(line_buffer_range.start.row);
- let language_name = buffer
- .language_at(line_buffer_range.start)
- .map(|language| language.name());
let indent_len = match indent_size.kind {
IndentKind::Space => {
- cx.global::<Settings>().tab_size(language_name.as_deref())
+ buffer.settings_at(line_buffer_range.start, cx).tab_size
}
IndentKind::Tab => NonZeroU32::new(1).unwrap(),
};
@@ -3544,12 +3573,11 @@ impl Editor {
}
// Otherwise, insert a hard or soft tab.
- let settings = cx.global::<Settings>();
- let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
- let tab_size = if settings.hard_tabs(language_name.as_deref()) {
+ let settings = buffer.settings_at(cursor, cx);
+ let tab_size = if settings.hard_tabs {
IndentSize::tab()
} else {
- let tab_size = settings.tab_size(language_name.as_deref()).get();
+ let tab_size = settings.tab_size.get();
let char_column = snapshot
.text_for_range(Point::new(cursor.row, 0)..cursor)
.flat_map(str::chars)
@@ -3602,10 +3630,9 @@ impl Editor {
delta_for_start_row: u32,
cx: &AppContext,
) -> u32 {
- let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
- let settings = cx.global::<Settings>();
- let tab_size = settings.tab_size(language_name.as_deref()).get();
- let indent_kind = if settings.hard_tabs(language_name.as_deref()) {
+ let settings = buffer.settings_at(selection.start, cx);
+ let tab_size = settings.tab_size.get();
+ let indent_kind = if settings.hard_tabs {
IndentKind::Tab
} else {
IndentKind::Space
@@ -3674,11 +3701,8 @@ impl Editor {
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
for selection in &selections {
- let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
- let tab_size = cx
- .global::<Settings>()
- .tab_size(language_name.as_deref())
- .get();
+ let settings = buffer.settings_at(selection.start, cx);
+ let tab_size = settings.tab_size.get();
let mut rows = selection.spanned_rows(false, &display_map);
// Avoid re-outdenting a row that has already been outdented by a
@@ -5546,68 +5570,91 @@ impl Editor {
}
fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
- self.go_to_hunk_impl(Direction::Next, cx)
- }
+ let snapshot = self
+ .display_map
+ .update(cx, |display_map, cx| display_map.snapshot(cx));
+ let selection = self.selections.newest::<Point>(cx);
- fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
- self.go_to_hunk_impl(Direction::Prev, cx)
+ if !self.seek_in_direction(
+ &snapshot,
+ selection.head(),
+ false,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range((selection.head().row + 1)..u32::MAX),
+ cx,
+ ) {
+ let wrapped_point = Point::zero();
+ self.seek_in_direction(
+ &snapshot,
+ wrapped_point,
+ true,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX),
+ cx,
+ );
+ }
}
- pub fn go_to_hunk_impl(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+ fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
let snapshot = self
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
let selection = self.selections.newest::<Point>(cx);
- fn seek_in_direction(
- this: &mut Editor,
- snapshot: &DisplaySnapshot,
- initial_point: Point,
- is_wrapped: bool,
- direction: Direction,
- cx: &mut ViewContext<Editor>,
- ) -> bool {
- let hunks = if direction == Direction::Next {
- snapshot
- .buffer_snapshot
- .git_diff_hunks_in_range(initial_point.row..u32::MAX, false)
- } else {
+ if !self.seek_in_direction(
+ &snapshot,
+ selection.head(),
+ false,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range_rev(0..selection.head().row),
+ cx,
+ ) {
+ let wrapped_point = snapshot.buffer_snapshot.max_point();
+ self.seek_in_direction(
+ &snapshot,
+ wrapped_point,
+ true,
snapshot
.buffer_snapshot
- .git_diff_hunks_in_range(0..initial_point.row, true)
- };
-
- let display_point = initial_point.to_display_point(snapshot);
- let mut hunks = hunks
- .map(|hunk| diff_hunk_to_display(hunk, &snapshot))
- .skip_while(|hunk| {
- if is_wrapped {
- false
- } else {
- hunk.contains_display_row(display_point.row())
- }
- })
- .dedup();
+ .git_diff_hunks_in_range_rev(0..wrapped_point.row),
+ cx,
+ );
+ }
+ }
- if let Some(hunk) = hunks.next() {
- this.change_selections(Some(Autoscroll::fit()), cx, |s| {
- let row = hunk.start_display_row();
- let point = DisplayPoint::new(row, 0);
- s.select_display_ranges([point..point]);
- });
+ fn seek_in_direction(
+ &mut self,
+ snapshot: &DisplaySnapshot,
+ initial_point: Point,
+ is_wrapped: bool,
+ hunks: impl Iterator<Item = DiffHunk<u32>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ let display_point = initial_point.to_display_point(snapshot);
+ let mut hunks = hunks
+ .map(|hunk| diff_hunk_to_display(hunk, &snapshot))
+ .skip_while(|hunk| {
+ if is_wrapped {
+ false
+ } else {
+ hunk.contains_display_row(display_point.row())
+ }
+ })
+ .dedup();
- true
- } else {
- false
- }
- }
+ if let Some(hunk) = hunks.next() {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let row = hunk.start_display_row();
+ let point = DisplayPoint::new(row, 0);
+ s.select_display_ranges([point..point]);
+ });
- if !seek_in_direction(self, &snapshot, selection.head(), false, direction, cx) {
- let wrapped_point = match direction {
- Direction::Next => Point::zero(),
- Direction::Prev => snapshot.buffer_snapshot.max_point(),
- };
- seek_in_direction(self, &snapshot, wrapped_point, true, direction, cx);
+ true
+ } else {
+ false
}
}
@@ -6439,27 +6486,24 @@ impl Editor {
}
pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
- let language_name = self
- .buffer
- .read(cx)
- .as_singleton()
- .and_then(|singleton_buffer| singleton_buffer.read(cx).language())
- .map(|l| l.name());
-
- let settings = cx.global::<Settings>();
+ let settings = self.buffer.read(cx).settings_at(0, cx);
let mode = self
.soft_wrap_mode_override
- .unwrap_or_else(|| settings.soft_wrap(language_name.as_deref()));
+ .unwrap_or_else(|| settings.soft_wrap);
match mode {
- settings::SoftWrap::None => SoftWrap::None,
- settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
- settings::SoftWrap::PreferredLineLength => {
- SoftWrap::Column(settings.preferred_line_length(language_name.as_deref()))
+ language_settings::SoftWrap::None => SoftWrap::None,
+ language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
+ language_settings::SoftWrap::PreferredLineLength => {
+ SoftWrap::Column(settings.preferred_line_length)
}
}
}
- pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext<Self>) {
+ pub fn set_soft_wrap_mode(
+ &mut self,
+ mode: language_settings::SoftWrap,
+ cx: &mut ViewContext<Self>,
+ ) {
self.soft_wrap_mode_override = Some(mode);
cx.notify();
}
@@ -6474,8 +6518,8 @@ impl Editor {
self.soft_wrap_mode_override.take();
} else {
let soft_wrap = match self.soft_wrap_mode(cx) {
- SoftWrap::None => settings::SoftWrap::EditorWidth,
- SoftWrap::EditorWidth | SoftWrap::Column(_) => settings::SoftWrap::None,
+ SoftWrap::None => language_settings::SoftWrap::EditorWidth,
+ SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None,
};
self.soft_wrap_mode_override = Some(soft_wrap);
}
@@ -6550,8 +6594,8 @@ impl Editor {
let buffer = &snapshot.buffer_snapshot;
let start = buffer.anchor_before(0);
let end = buffer.anchor_after(buffer.len());
- let theme = cx.global::<Settings>().theme.as_ref();
- self.background_highlights_in_range(start..end, &snapshot, theme)
+ let theme = theme::current(cx);
+ self.background_highlights_in_range(start..end, &snapshot, theme.as_ref())
}
fn document_highlights_for_position<'a>(
@@ -6861,44 +6905,88 @@ impl Editor {
.collect()
}
- fn report_editor_event(&self, name: &'static str, cx: &AppContext) {
- if let Some((project, file)) = self.project.as_ref().zip(
- self.buffer
- .read(cx)
- .as_singleton()
- .and_then(|b| b.read(cx).file()),
- ) {
- let settings = cx.global::<Settings>();
-
- let extension = Path::new(file.file_name(cx))
- .extension()
- .and_then(|e| e.to_str());
- let telemetry = project.read(cx).client().telemetry().clone();
- telemetry.report_mixpanel_event(
- match name {
- "open" => "open editor",
- "save" => "save editor",
- _ => name,
- },
- json!({ "File Extension": extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }),
- settings.telemetry(),
- );
- let event = ClickhouseEvent::Editor {
- file_extension: extension.map(ToString::to_string),
- vim_mode: settings.vim_mode,
- operation: name,
- copilot_enabled: settings.features.copilot,
- copilot_enabled_for_language: settings.show_copilot_suggestions(
- self.language_at(0, cx)
- .map(|language| language.name())
- .as_deref(),
- self.file_at(0, cx)
- .map(|file| file.path().clone())
- .as_deref(),
- ),
- };
- telemetry.report_clickhouse_event(event, settings.telemetry())
- }
+ fn report_copilot_event(
+ &self,
+ suggestion_id: Option<String>,
+ suggestion_accepted: bool,
+ cx: &AppContext,
+ ) {
+ let Some(project) = &self.project else {
+ return
+ };
+
+ // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension
+ let file_extension = self
+ .buffer
+ .read(cx)
+ .as_singleton()
+ .and_then(|b| b.read(cx).file())
+ .and_then(|file| Path::new(file.file_name(cx)).extension())
+ .and_then(|e| e.to_str())
+ .map(|a| a.to_string());
+
+ let telemetry = project.read(cx).client().telemetry().clone();
+ let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+
+ let event = ClickhouseEvent::Copilot {
+ suggestion_id,
+ suggestion_accepted,
+ file_extension,
+ };
+ telemetry.report_clickhouse_event(event, telemetry_settings);
+ }
+
+ fn report_editor_event(
+ &self,
+ name: &'static str,
+ file_extension: Option<String>,
+ cx: &AppContext,
+ ) {
+ let Some(project) = &self.project else {
+ return
+ };
+
+ // If None, we are in a file without an extension
+ let file_extension = file_extension.or(self
+ .buffer
+ .read(cx)
+ .as_singleton()
+ .and_then(|b| b.read(cx).file())
+ .and_then(|file| Path::new(file.file_name(cx)).extension())
+ .and_then(|e| e.to_str())
+ .map(|a| a.to_string()));
+
+ let vim_mode = cx
+ .global::<SettingsStore>()
+ .untyped_user_settings()
+ .get("vim_mode")
+ == Some(&serde_json::Value::Bool(true));
+ let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+ let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None);
+ let copilot_enabled_for_language = self
+ .buffer
+ .read(cx)
+ .settings_at(0, cx)
+ .show_copilot_suggestions;
+
+ let telemetry = project.read(cx).client().telemetry().clone();
+ telemetry.report_mixpanel_event(
+ match name {
+ "open" => "open editor",
+ "save" => "save editor",
+ _ => name,
+ },
+ json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true }),
+ telemetry_settings,
+ );
+ let event = ClickhouseEvent::Editor {
+ file_extension,
+ vim_mode,
+ operation: name,
+ copilot_enabled,
+ copilot_enabled_for_language,
+ };
+ telemetry.report_clickhouse_event(event, telemetry_settings)
}
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
@@ -6930,7 +7018,7 @@ impl Editor {
let mut lines = Vec::new();
let mut line: VecDeque<Chunk> = VecDeque::new();
- let theme = &cx.global::<Settings>().theme.editor.syntax;
+ let theme = &theme::current(cx).editor.syntax;
for chunk in chunks {
let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme));
@@ -7352,7 +7440,7 @@ impl View for Editor {
}
fn build_style(
- settings: &Settings,
+ settings: &ThemeSettings,
get_field_editor_theme: Option<&GetFieldEditorTheme>,
override_text_style: Option<&OverrideTextStyle>,
cx: &AppContext,
@@ -7382,7 +7470,7 @@ fn build_style(
let font_id = font_cache
.select_font(font_family_id, &font_properties)
.unwrap();
- let font_size = settings.buffer_font_size;
+ let font_size = settings.buffer_font_size(cx);
EditorStyle {
text: TextStyle {
color: settings.theme.editor.text_color,
@@ -7552,10 +7640,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
}
Arc::new(move |cx: &mut BlockContext| {
- let settings = cx.global::<Settings>();
+ let settings = settings::get::<ThemeSettings>(cx);
let theme = &settings.theme.editor;
let style = diagnostic_style(diagnostic.severity, is_valid, theme);
- let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
+ let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
Flex::column()
.with_children(highlighted_lines.iter().map(|(line, highlights)| {
Label::new(
@@ -0,0 +1,43 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize)]
+pub struct EditorSettings {
+ pub cursor_blink: bool,
+ pub hover_popover_enabled: bool,
+ pub show_completions_on_input: bool,
+ pub show_scrollbars: ShowScrollbars,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowScrollbars {
+ #[default]
+ Auto,
+ System,
+ Always,
+ Never,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct EditorSettingsContent {
+ pub cursor_blink: Option<bool>,
+ pub hover_popover_enabled: Option<bool>,
+ pub show_completions_on_input: Option<bool>,
+ pub show_scrollbars: Option<ShowScrollbars>,
+}
+
+impl Setting for EditorSettings {
+ const KEY: Option<&'static str> = None;
+
+ type FileContent = EditorSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &gpui::AppContext,
+ ) -> anyhow::Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
@@ -12,10 +12,12 @@ use gpui::{
serde_json, TestAppContext,
};
use indoc::indoc;
-use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
+use language::{
+ language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
+ BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
+};
use parking_lot::Mutex;
use project::FakeFs;
-use settings::EditorSettings;
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use unindent::Unindent;
use util::{
@@ -29,7 +31,8 @@ use workspace::{
#[gpui::test]
fn test_edit_events(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let buffer = cx.add_model(|cx| {
let mut buffer = language::Buffer::new(0, "123456", cx);
buffer.set_group_interval(Duration::from_secs(1));
@@ -156,7 +159,8 @@ fn test_edit_events(cx: &mut TestAppContext) {
#[gpui::test]
fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let mut now = Instant::now();
let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
@@ -226,7 +230,8 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
#[gpui::test]
fn test_ime_composition(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let buffer = cx.add_model(|cx| {
let mut buffer = language::Buffer::new(0, "abcde", cx);
// Ensure automatic grouping doesn't occur.
@@ -328,7 +333,7 @@ fn test_ime_composition(cx: &mut TestAppContext) {
#[gpui::test]
fn test_selection_with_mouse(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
@@ -395,7 +400,8 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
#[gpui::test]
fn test_canceling_pending_selection(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
build_editor(buffer, cx)
@@ -429,6 +435,8 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) {
#[gpui::test]
fn test_clone(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
let (text, selection_ranges) = marked_text_ranges(
indoc! {"
one
@@ -439,7 +447,6 @@ fn test_clone(cx: &mut TestAppContext) {
"},
true,
);
- cx.update(|cx| cx.set_global(Settings::test(cx)));
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&text, cx);
@@ -487,7 +494,8 @@ fn test_clone(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_navigation_history(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
cx.set_global(DragAndDrop::<Workspace>::default());
use workspace::item::Item;
@@ -600,7 +608,8 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
#[gpui::test]
fn test_cancel(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
build_editor(buffer, cx)
@@ -642,7 +651,8 @@ fn test_cancel(cx: &mut TestAppContext) {
#[gpui::test]
fn test_fold_action(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(
&"
@@ -731,7 +741,8 @@ fn test_fold_action(cx: &mut TestAppContext) {
#[gpui::test]
fn test_move_cursor(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
@@ -806,7 +817,8 @@ fn test_move_cursor(cx: &mut TestAppContext) {
#[gpui::test]
fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
build_editor(buffer.clone(), cx)
@@ -910,7 +922,8 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
#[gpui::test]
fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
build_editor(buffer.clone(), cx)
@@ -959,7 +972,8 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
#[gpui::test]
fn test_beginning_end_of_line(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\n def", cx);
build_editor(buffer, cx)
@@ -1121,7 +1135,8 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
#[gpui::test]
fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
build_editor(buffer, cx)
@@ -1172,7 +1187,8 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
#[gpui::test]
fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
build_editor(buffer, cx)
@@ -1229,6 +1245,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
@@ -1343,6 +1360,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
cx.set_state("one «two threeˇ» four");
cx.update_editor(|editor, cx| {
@@ -1353,7 +1371,8 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("one two three four", cx);
build_editor(buffer.clone(), cx)
@@ -1388,7 +1407,8 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
#[gpui::test]
fn test_newline(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
build_editor(buffer.clone(), cx)
@@ -1410,7 +1430,8 @@ fn test_newline(cx: &mut TestAppContext) {
#[gpui::test]
fn test_newline_with_old_selections(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(
"
@@ -1491,11 +1512,8 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_newline_above(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx);
- cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|settings, _| {
- settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
- });
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(
@@ -1506,8 +1524,9 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
.with_indents_query(r#"(_ "(" ")" @end) @indent"#)
.unwrap(),
);
- cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+ let mut cx = EditorTestContext::new(cx);
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
const a: ˇA = (
(ˇ
@@ -1516,6 +1535,7 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
)ˇ
ˇ);ˇ
"});
+
cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
cx.assert_editor_state(indoc! {"
ˇ
@@ -1540,11 +1560,8 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx);
- cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|settings, _| {
- settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
- });
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(
@@ -1555,8 +1572,9 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
.with_indents_query(r#"(_ "(" ")" @end) @indent"#)
.unwrap(),
);
- cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+ let mut cx = EditorTestContext::new(cx);
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
const a: ˇA = (
(ˇ
@@ -1565,6 +1583,7 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
)ˇ
ˇ);ˇ
"});
+
cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
cx.assert_editor_state(indoc! {"
const a: A = (
@@ -1589,7 +1608,8 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
let mut editor = build_editor(buffer.clone(), cx);
@@ -1615,12 +1635,11 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_tab(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx);
- cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|settings, _| {
- settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
- });
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(3)
});
+
+ let mut cx = EditorTestContext::new(cx);
cx.set_state(indoc! {"
ˇabˇc
ˇ🏀ˇ🏀ˇefg
@@ -1646,6 +1665,8 @@ async fn test_tab(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
let language = Arc::new(
Language::new(
@@ -1704,7 +1725,10 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAp
#[gpui::test]
async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx);
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4)
+ });
+
let language = Arc::new(
Language::new(
LanguageConfig::default(),
@@ -1713,14 +1737,9 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
.with_indents_query(r#"(_ "{" "}" @end) @indent"#)
.unwrap(),
);
- cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-
- cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|settings, _| {
- settings.editor_overrides.tab_size = Some(4.try_into().unwrap());
- });
- });
+ let mut cx = EditorTestContext::new(cx);
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
fn a() {
if b {
@@ -1741,6 +1760,10 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4);
+ });
+
let mut cx = EditorTestContext::new(cx);
cx.set_state(indoc! {"
@@ -1810,13 +1833,12 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx);
- cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|settings, _| {
- settings.editor_overrides.hard_tabs = Some(true);
- });
+ init_test(cx, |settings| {
+ settings.defaults.hard_tabs = Some(true);
});
+ let mut cx = EditorTestContext::new(cx);
+
// select two ranges on one line
cx.set_state(indoc! {"
«oneˇ» «twoˇ»
@@ -1907,25 +1929,25 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
- cx.update(|cx| {
- cx.set_global(
- Settings::test(cx)
- .with_language_defaults(
- "TOML",
- EditorSettings {
- tab_size: Some(2.try_into().unwrap()),
- ..Default::default()
- },
- )
- .with_language_defaults(
- "Rust",
- EditorSettings {
- tab_size: Some(4.try_into().unwrap()),
- ..Default::default()
- },
- ),
- );
+ init_test(cx, |settings| {
+ settings.languages.extend([
+ (
+ "TOML".into(),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(2),
+ ..Default::default()
+ },
+ ),
+ (
+ "Rust".into(),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(4),
+ ..Default::default()
+ },
+ ),
+ ]);
});
+
let toml_language = Arc::new(Language::new(
LanguageConfig {
name: "TOML".into(),
@@ -2020,6 +2042,8 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_backspace(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
// Basic backspace
@@ -2067,8 +2091,9 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_delete(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx);
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx);
cx.set_state(indoc! {"
onˇe two three
fou«rˇ» five six
@@ -2095,7 +2120,8 @@ async fn test_delete(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_delete_line(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx)
@@ -2119,7 +2145,6 @@ fn test_delete_line(cx: &mut TestAppContext) {
);
});
- cx.update(|cx| cx.set_global(Settings::test(cx)));
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx)
@@ -2139,7 +2164,8 @@ fn test_delete_line(cx: &mut TestAppContext) {
#[gpui::test]
fn test_duplicate_line(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx)
@@ -2191,7 +2217,8 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
#[gpui::test]
fn test_move_line_up_down(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
build_editor(buffer, cx)
@@ -2289,7 +2316,8 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
#[gpui::test]
fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
build_editor(buffer, cx)
@@ -2315,7 +2343,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
#[gpui::test]
fn test_transpose(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
_ = cx
.add_window(|cx| {
@@ -2417,6 +2445,8 @@ fn test_transpose(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_clipboard(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
@@ -2497,6 +2527,8 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
let language = Arc::new(Language::new(
LanguageConfig::default(),
@@ -2609,7 +2641,8 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_select_all(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
build_editor(buffer, cx)
@@ -2625,7 +2658,8 @@ fn test_select_all(cx: &mut TestAppContext) {
#[gpui::test]
fn test_select_line(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
build_editor(buffer, cx)
@@ -2671,7 +2705,8 @@ fn test_select_line(cx: &mut TestAppContext) {
#[gpui::test]
fn test_split_selection_into_lines(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
build_editor(buffer, cx)
@@ -2741,7 +2776,8 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
#[gpui::test]
fn test_add_selection_above_below(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
build_editor(buffer, cx)
@@ -2935,6 +2971,8 @@ fn test_add_selection_above_below(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_select_next(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
@@ -2959,7 +2997,8 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::language()),
@@ -3100,7 +3139,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let language = Arc::new(
Language::new(
LanguageConfig {
@@ -3160,6 +3200,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
let language = Arc::new(Language::new(
@@ -3329,6 +3371,8 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
let html_language = Arc::new(
@@ -3563,6 +3607,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
let rust_language = Arc::new(
@@ -3660,7 +3706,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let language = Arc::new(Language::new(
LanguageConfig {
brackets: BracketPairConfig {
@@ -3814,7 +3861,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let language = Arc::new(Language::new(
LanguageConfig {
brackets: BracketPairConfig {
@@ -3919,7 +3967,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_snippets(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
let (text, insertion_ranges) = marked_text_ranges(
indoc! {"
@@ -4027,7 +4075,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx, |_| {});
let mut language = Language::new(
LanguageConfig {
@@ -4111,16 +4159,14 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(!cx.read(|cx| editor.is_dirty(cx)));
// Set rust language override and assert overriden tabsize is sent to language server
- cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|settings, _| {
- settings.language_overrides.insert(
- "Rust".into(),
- EditorSettings {
- tab_size: Some(8.try_into().unwrap()),
- ..Default::default()
- },
- );
- })
+ update_test_settings(cx, |settings| {
+ settings.languages.insert(
+ "Rust".into(),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(8),
+ ..Default::default()
+ },
+ );
});
let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
@@ -4141,7 +4187,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx, |_| {});
let mut language = Language::new(
LanguageConfig {
@@ -4227,16 +4273,14 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(!cx.read(|cx| editor.is_dirty(cx)));
// Set rust language override and assert overriden tabsize is sent to language server
- cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|settings, _| {
- settings.language_overrides.insert(
- "Rust".into(),
- EditorSettings {
- tab_size: Some(8.try_into().unwrap()),
- ..Default::default()
- },
- );
- })
+ update_test_settings(cx, |settings| {
+ settings.languages.insert(
+ "Rust".into(),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(8),
+ ..Default::default()
+ },
+ );
});
let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
@@ -4257,7 +4301,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx, |_| {});
let mut language = Language::new(
LanguageConfig {
@@ -4342,7 +4386,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
@@ -4399,7 +4443,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
@@ -4514,6 +4558,8 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
#[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -4651,8 +4697,10 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
apply_additional_edits.await.unwrap();
cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|settings, _| {
- settings.show_completions_on_input = false;
+ cx.update_global::<SettingsStore, _, _>(|settings, cx| {
+ settings.update_user_settings::<EditorSettings>(cx, |settings| {
+ settings.show_completions_on_input = Some(false);
+ });
})
});
cx.set_state("editorˇ");
@@ -4681,7 +4729,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let language = Arc::new(Language::new(
LanguageConfig {
line_comment: Some("// ".into()),
@@ -4764,8 +4813,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx);
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
let language = Arc::new(Language::new(
LanguageConfig {
@@ -4778,6 +4826,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
let registry = Arc::new(LanguageRegistry::test());
registry.add(language.clone());
+ let mut cx = EditorTestContext::new(cx);
cx.update_buffer(|buffer, cx| {
buffer.set_language_registry(registry);
buffer.set_language(Some(language), cx);
@@ -4897,6 +4946,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
#[gpui::test]
async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
let html_language = Arc::new(
@@ -5021,7 +5072,8 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
@@ -5067,7 +5119,8 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
#[gpui::test]
fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let markers = vec![('[', ']').into(), ('(', ')').into()];
let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
indoc! {"
@@ -5140,7 +5193,8 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
#[gpui::test]
fn test_refresh_selections(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
let mut excerpt1_id = None;
let multibuffer = cx.add_model(|cx| {
@@ -5224,7 +5278,8 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
#[gpui::test]
fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
let mut excerpt1_id = None;
let multibuffer = cx.add_model(|cx| {
@@ -5282,7 +5337,8 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let language = Arc::new(
Language::new(
LanguageConfig {
@@ -5355,7 +5411,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_highlighted_ranges(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
build_editor(buffer.clone(), cx)
@@ -5395,7 +5452,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
let mut highlighted_ranges = editor.background_highlights_in_range(
anchor_range(Point::new(3, 4)..Point::new(7, 4)),
&snapshot,
- cx.global::<Settings>().theme.as_ref(),
+ theme::current(cx).as_ref(),
);
// Enforce a consistent ordering based on color without relying on the ordering of the
// highlight's `TypeId` which is non-deterministic.
@@ -5425,7 +5482,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
editor.background_highlights_in_range(
anchor_range(Point::new(5, 6)..Point::new(6, 4)),
&snapshot,
- cx.global::<Settings>().theme.as_ref(),
+ theme::current(cx).as_ref(),
),
&[(
DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
@@ -5437,7 +5494,8 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_following(cx: &mut gpui::TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx, |_| {});
+
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
@@ -5459,10 +5517,12 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
});
let is_still_following = Rc::new(RefCell::new(true));
+ let follower_edit_event_count = Rc::new(RefCell::new(0));
let pending_update = Rc::new(RefCell::new(None));
follower.update(cx, {
let update = pending_update.clone();
let is_still_following = is_still_following.clone();
+ let follower_edit_event_count = follower_edit_event_count.clone();
|_, cx| {
cx.subscribe(&leader, move |_, leader, event, cx| {
leader
@@ -5475,6 +5535,9 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
if Editor::should_unfollow_on_event(event, cx) {
*is_still_following.borrow_mut() = false;
}
+ if let Event::BufferEdited = event {
+ *follower_edit_event_count.borrow_mut() += 1;
+ }
})
.detach();
}
@@ -5494,6 +5557,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
assert_eq!(follower.selections.ranges(cx), vec![1..1]);
});
assert_eq!(*is_still_following.borrow(), true);
+ assert_eq!(*follower_edit_event_count.borrow(), 0);
// Update the scroll position only
leader.update(cx, |leader, cx| {
@@ -5510,6 +5574,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
vec2f(1.5, 3.5)
);
assert_eq!(*is_still_following.borrow(), true);
+ assert_eq!(*follower_edit_event_count.borrow(), 0);
// Update the selections and scroll position. The follower's scroll position is updated
// via autoscroll, not via the leader's exact scroll position.
@@ -5576,7 +5641,8 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx, |_| {});
+
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -5805,6 +5871,8 @@ fn test_combine_syntax_and_fuzzy_match_highlights() {
#[gpui::test]
async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorTestContext::new(cx);
let diff_base = r#"
@@ -5924,6 +5992,8 @@ fn test_split_words() {
#[gpui::test]
async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
let mut assert = |before, after| {
let _state_context = cx.set_state(before);
@@ -5972,6 +6042,8 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| cx.set_global(copilot));
let mut cx = EditorLspTestContext::new_rust(
@@ -6223,6 +6295,8 @@ async fn test_copilot_completion_invalidation(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
+ init_test(cx, |_| {});
+
let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| cx.set_global(copilot));
let mut cx = EditorLspTestContext::new_rust(
@@ -6288,11 +6362,10 @@ async fn test_copilot_multibuffer(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
+ init_test(cx, |_| {});
+
let (copilot, copilot_lsp) = Copilot::fake(cx);
- cx.update(|cx| {
- cx.set_global(Settings::test(cx));
- cx.set_global(copilot)
- });
+ cx.update(|cx| cx.set_global(copilot));
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
@@ -6392,14 +6465,16 @@ async fn test_copilot_disabled_globs(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
- let (copilot, copilot_lsp) = Copilot::fake(cx);
- cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.copilot.disabled_globs = vec![glob::Pattern::new(".env*").unwrap()];
- cx.set_global(settings);
- cx.set_global(copilot)
+ init_test(cx, |settings| {
+ settings
+ .copilot
+ .get_or_insert(Default::default())
+ .disabled_globs = Some(vec![".env*".to_string()]);
});
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+ cx.update(|cx| cx.set_global(copilot));
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/test",
@@ -5,6 +5,7 @@ use super::{
};
use crate::{
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
+ editor_settings::ShowScrollbars,
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
@@ -13,7 +14,7 @@ use crate::{
link_go_to_definition::{
go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
},
- mouse_context_menu, EditorStyle, GutterHover, UnfoldAt,
+ mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
@@ -35,9 +36,11 @@ use gpui::{
};
use itertools::Itertools;
use json::json;
-use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection};
+use language::{
+ language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
+ Selection,
+};
use project::ProjectPath;
-use settings::{GitGutter, Settings, ShowWhitespaces};
use smallvec::SmallVec;
use std::{
borrow::Cow,
@@ -47,7 +50,8 @@ use std::{
ops::Range,
sync::Arc,
};
-use workspace::item::Item;
+use text::Point;
+use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
enum FoldMarkers {}
@@ -547,11 +551,11 @@ impl EditorElement {
let scroll_top = scroll_position.y() * line_height;
let show_gutter = matches!(
- &cx.global::<Settings>()
- .git_overrides
+ settings::get::<WorkspaceSettings>(cx)
+ .git
.git_gutter
.unwrap_or_default(),
- GitGutter::TrackedFiles
+ GitGutterSetting::TrackedFiles
);
if show_gutter {
@@ -608,7 +612,7 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut ViewContext<Editor>,
) {
- let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
+ let diff_style = &theme::current(cx).editor.diff.clone();
let line_height = layout.position_map.line_height;
let scroll_position = layout.position_map.snapshot.scroll_position();
@@ -648,7 +652,7 @@ impl EditorElement {
//TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => {
- let row = *display_row_range.start();
+ let row = display_row_range.start;
let offset = line_height / 2.;
let start_y = row as f32 * line_height - offset - scroll_top;
@@ -670,11 +674,11 @@ impl EditorElement {
}
};
- let start_row = *display_row_range.start();
- let end_row = *display_row_range.end();
+ let start_row = display_row_range.start;
+ let end_row = display_row_range.end;
let start_y = start_row as f32 * line_height - scroll_top;
- let end_y = end_row as f32 * line_height - scroll_top + line_height;
+ let end_y = end_row as f32 * line_height - scroll_top;
let width = diff_style.width_em * line_height;
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
@@ -708,6 +712,7 @@ impl EditorElement {
let scroll_left = scroll_position.x() * max_glyph_width;
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
let line_end_overshoot = 0.15 * layout.position_map.line_height;
+ let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
scene.push_layer(Some(bounds));
@@ -882,9 +887,10 @@ impl EditorElement {
content_origin,
scroll_left,
visible_text_bounds,
- cx,
+ whitespace_setting,
&invisible_display_ranges,
visible_bounds,
+ cx,
)
}
}
@@ -1022,15 +1028,16 @@ impl EditorElement {
let mut first_row_y_offset = 0.0;
// Impose a minimum height on the scrollbar thumb
+ let row_height = height / max_row;
let min_thumb_height =
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
- let thumb_height = (row_range.end - row_range.start) * height / max_row;
+ let thumb_height = (row_range.end - row_range.start) * row_height;
if thumb_height < min_thumb_height {
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
height -= min_thumb_height - thumb_height;
}
- let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row };
+ let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height };
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
@@ -1044,6 +1051,54 @@ impl EditorElement {
background: style.track.background_color,
..Default::default()
});
+
+ let diff_style = theme::current(cx).editor.diff.clone();
+ for hunk in layout
+ .position_map
+ .snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range(0..(max_row.floor() as u32))
+ {
+ let start_display = Point::new(hunk.buffer_range.start, 0)
+ .to_display_point(&layout.position_map.snapshot.display_snapshot);
+ let end_display = Point::new(hunk.buffer_range.end, 0)
+ .to_display_point(&layout.position_map.snapshot.display_snapshot);
+ let start_y = y_for_row(start_display.row() as f32);
+ let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
+ y_for_row((end_display.row() + 1) as f32)
+ } else {
+ y_for_row((end_display.row()) as f32)
+ };
+
+ if end_y - start_y < 1. {
+ end_y = start_y + 1.;
+ }
+ let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+ let color = match hunk.status() {
+ DiffHunkStatus::Added => diff_style.inserted,
+ DiffHunkStatus::Modified => diff_style.modified,
+ DiffHunkStatus::Removed => diff_style.deleted,
+ };
+
+ let border = Border {
+ width: 1.,
+ color: style.thumb.border.color,
+ overlay: false,
+ top: false,
+ right: true,
+ bottom: false,
+ left: true,
+ };
+
+ scene.push_quad(Quad {
+ bounds,
+ background: Some(color),
+ border,
+ corner_radius: style.thumb.corner_radius,
+ })
+ }
+
scene.push_quad(Quad {
bounds: thumb_bounds,
border: style.thumb.border,
@@ -1219,7 +1274,7 @@ impl EditorElement {
.row;
buffer_snapshot
- .git_diff_hunks_in_range(buffer_start_row..buffer_end_row, false)
+ .git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
.map(|hunk| diff_hunk_to_display(hunk, snapshot))
.dedup()
.collect()
@@ -1412,7 +1467,7 @@ impl EditorElement {
editor: &mut Editor,
cx: &mut LayoutContext<Editor>,
) -> (f32, Vec<BlockLayout>) {
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
let scroll_x = snapshot.scroll_anchor.offset.x();
let (fixed_blocks, non_fixed_blocks) = snapshot
.blocks_in_range(rows.clone())
@@ -1738,9 +1793,10 @@ impl LineWithInvisibles {
content_origin: Vector2F,
scroll_left: f32,
visible_text_bounds: RectF,
- cx: &mut ViewContext<Editor>,
+ whitespace_setting: ShowWhitespaceSetting,
selection_ranges: &[Range<DisplayPoint>],
visible_bounds: RectF,
+ cx: &mut ViewContext<Editor>,
) {
let line_height = layout.position_map.line_height;
let line_y = row as f32 * line_height - scroll_top;
@@ -1754,7 +1810,6 @@ impl LineWithInvisibles {
);
self.draw_invisibles(
- cx,
&selection_ranges,
layout,
content_origin,
@@ -1764,12 +1819,13 @@ impl LineWithInvisibles {
scene,
visible_bounds,
line_height,
+ whitespace_setting,
+ cx,
);
}
fn draw_invisibles(
&self,
- cx: &mut ViewContext<Editor>,
selection_ranges: &[Range<DisplayPoint>],
layout: &LayoutState,
content_origin: Vector2F,
@@ -1779,17 +1835,13 @@ impl LineWithInvisibles {
scene: &mut SceneBuilder,
visible_bounds: RectF,
line_height: f32,
+ whitespace_setting: ShowWhitespaceSetting,
+ cx: &mut ViewContext<Editor>,
) {
- let settings = cx.global::<Settings>();
- let allowed_invisibles_regions = match settings
- .editor_overrides
- .show_whitespaces
- .or(settings.editor_defaults.show_whitespaces)
- .unwrap_or_default()
- {
- ShowWhitespaces::None => return,
- ShowWhitespaces::Selection => Some(selection_ranges),
- ShowWhitespaces::All => None,
+ let allowed_invisibles_regions = match whitespace_setting {
+ ShowWhitespaceSetting::None => return,
+ ShowWhitespaceSetting::Selection => Some(selection_ranges),
+ ShowWhitespaceSetting::All => None,
};
for invisible in &self.invisibles {
@@ -1934,11 +1986,11 @@ impl Element<Editor> for EditorElement {
let is_singleton = editor.is_singleton(cx);
let highlighted_rows = editor.highlighted_rows();
- let theme = cx.global::<Settings>().theme.as_ref();
+ let theme = theme::current(cx);
let highlighted_ranges = editor.background_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
- theme,
+ theme.as_ref(),
);
fold_ranges.extend(
@@ -2013,7 +2065,15 @@ impl Element<Editor> for EditorElement {
));
}
- let show_scrollbars = editor.scroll_manager.scrollbars_visible();
+ let show_scrollbars = match settings::get::<EditorSettings>(cx).show_scrollbars {
+ ShowScrollbars::Auto => {
+ snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
+ }
+ ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
+ ShowScrollbars::Always => true,
+ ShowScrollbars::Never => false,
+ };
+
let include_root = editor
.project
.as_ref()
@@ -2773,17 +2833,19 @@ mod tests {
use super::*;
use crate::{
display_map::{BlockDisposition, BlockProperties},
+ editor_tests::{init_test, update_test_settings},
Editor, MultiBuffer,
};
use gpui::TestAppContext;
+ use language::language_settings;
use log::info;
- use settings::Settings;
use std::{num::NonZeroU32, sync::Arc};
use util::test::sample_text;
#[gpui::test]
fn test_layout_line_numbers(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
Editor::new(EditorMode::Full, buffer, None, None, cx)
@@ -2801,7 +2863,8 @@ mod tests {
#[gpui::test]
fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx, |_| {});
+
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("", cx);
Editor::new(EditorMode::Full, buffer, None, None, cx)
@@ -2861,26 +2924,27 @@ mod tests {
#[gpui::test]
fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
- let tab_size = 4;
+ const TAB_SIZE: u32 = 4;
+
let input_text = "\t \t|\t| a b";
let expected_invisibles = vec![
Invisible::Tab {
line_start_offset: 0,
},
Invisible::Whitespace {
- line_offset: tab_size as usize,
+ line_offset: TAB_SIZE as usize,
},
Invisible::Tab {
- line_start_offset: tab_size as usize + 1,
+ line_start_offset: TAB_SIZE as usize + 1,
},
Invisible::Tab {
- line_start_offset: tab_size as usize * 2 + 1,
+ line_start_offset: TAB_SIZE as usize * 2 + 1,
},
Invisible::Whitespace {
- line_offset: tab_size as usize * 3 + 1,
+ line_offset: TAB_SIZE as usize * 3 + 1,
},
Invisible::Whitespace {
- line_offset: tab_size as usize * 3 + 3,
+ line_offset: TAB_SIZE as usize * 3 + 3,
},
];
assert_eq!(
@@ -2892,12 +2956,11 @@ mod tests {
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
);
- cx.update(|cx| {
- let mut test_settings = Settings::test(cx);
- test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
- test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
- cx.set_global(test_settings);
+ init_test(cx, |s| {
+ s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+ s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
});
+
let actual_invisibles =
collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0);
@@ -2906,11 +2969,9 @@ mod tests {
#[gpui::test]
fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
- cx.update(|cx| {
- let mut test_settings = Settings::test(cx);
- test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
- test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
- cx.set_global(test_settings);
+ init_test(cx, |s| {
+ s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+ s.defaults.tab_size = NonZeroU32::new(4);
});
for editor_mode_without_invisibles in [
@@ -2961,19 +3022,18 @@ mod tests {
);
info!("Expected invisibles: {expected_invisibles:?}");
+ init_test(cx, |_| {});
+
// Put the same string with repeating whitespace pattern into editors of various size,
// take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
let resize_step = 10.0;
let mut editor_width = 200.0;
while editor_width <= 1000.0 {
- cx.update(|cx| {
- let mut test_settings = Settings::test(cx);
- test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
- test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
- test_settings.editor_defaults.preferred_line_length = Some(editor_width as u32);
- test_settings.editor_defaults.soft_wrap =
- Some(settings::SoftWrap::PreferredLineLength);
- cx.set_global(test_settings);
+ update_test_settings(cx, |s| {
+ s.defaults.tab_size = NonZeroU32::new(tab_size);
+ s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+ s.defaults.preferred_line_length = Some(editor_width as u32);
+ s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
});
let actual_invisibles =
@@ -3021,7 +3081,7 @@ mod tests {
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
let (_, layout_state) = editor.update(cx, |editor, cx| {
- editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx);
+ editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_wrap_width(Some(editor_width), cx);
let mut new_parents = Default::default();
@@ -1,4 +1,4 @@
-use std::ops::RangeInclusive;
+use std::ops::Range;
use git::diff::{DiffHunk, DiffHunkStatus};
use language::Point;
@@ -15,7 +15,7 @@ pub enum DisplayDiffHunk {
},
Unfolded {
- display_row_range: RangeInclusive<u32>,
+ display_row_range: Range<u32>,
status: DiffHunkStatus,
},
}
@@ -26,7 +26,7 @@ impl DisplayDiffHunk {
&DisplayDiffHunk::Folded { display_row } => display_row,
DisplayDiffHunk::Unfolded {
display_row_range, ..
- } => *display_row_range.start(),
+ } => display_row_range.start,
}
}
@@ -36,7 +36,7 @@ impl DisplayDiffHunk {
DisplayDiffHunk::Unfolded {
display_row_range, ..
- } => display_row_range.clone(),
+ } => display_row_range.start..=display_row_range.end - 1,
};
range.contains(&display_row)
@@ -77,16 +77,12 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
} else {
let start = hunk_start_point.to_display_point(snapshot).row();
- let hunk_end_row_inclusive = hunk
- .buffer_range
- .end
- .saturating_sub(1)
- .max(hunk.buffer_range.start);
+ let hunk_end_row_inclusive = hunk.buffer_range.end.max(hunk.buffer_range.start);
let hunk_end_point = Point::new(hunk_end_row_inclusive, 0);
let end = hunk_end_point.to_display_point(snapshot).row();
DisplayDiffHunk::Unfolded {
- display_row_range: start..=end,
+ display_row_range: start..end,
status: hunk.status(),
}
}
@@ -33,12 +33,14 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
#[cfg(test)]
mod tests {
use super::*;
- use crate::test::editor_lsp_test_context::EditorLspTestContext;
+ use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use indoc::indoc;
use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
#[gpui::test]
async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new(
Language::new(
LanguageConfig {
@@ -1,6 +1,6 @@
use crate::{
- display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
- EditorStyle, RangeToAnchorExt,
+ display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings,
+ EditorSnapshot, EditorStyle, RangeToAnchorExt,
};
use futures::FutureExt;
use gpui::{
@@ -12,7 +12,6 @@ use gpui::{
};
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
use project::{HoverBlock, HoverBlockKind, Project};
-use settings::Settings;
use std::{ops::Range, sync::Arc, time::Duration};
use util::TryFutureExt;
@@ -38,7 +37,7 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
/// The internal hover action dispatches between `show_hover` or `hide_hover`
/// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
- if cx.global::<Settings>().hover_popover_enabled {
+ if settings::get::<EditorSettings>(cx).hover_popover_enabled {
if let Some(point) = point {
show_hover(editor, point, false, cx);
} else {
@@ -654,7 +653,7 @@ impl DiagnosticPopover {
_ => style.hover_popover.container,
};
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
MouseEventHandler::<DiagnosticPopover, _>::new(0, cx, |_, _| {
text.with_soft_wrap(true)
@@ -694,7 +693,7 @@ impl DiagnosticPopover {
#[cfg(test)]
mod tests {
use super::*;
- use crate::test::editor_lsp_test_context::EditorLspTestContext;
+ use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use gpui::fonts::Weight;
use indoc::indoc;
use language::{Diagnostic, DiagnosticSet};
@@ -706,6 +705,8 @@ mod tests {
#[gpui::test]
async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -773,6 +774,8 @@ mod tests {
#[gpui::test]
async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -816,6 +819,8 @@ mod tests {
#[gpui::test]
async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -882,7 +887,8 @@ mod tests {
#[gpui::test]
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx, |_| {});
+
cx.add_window(|cx| {
let editor = Editor::single_line(None, cx);
let style = editor.style(cx);
@@ -1006,8 +1012,7 @@ mod tests {
.zip(expected_styles.iter().cloned())
.collect::<Vec<_>>();
assert_eq!(
- rendered.text,
- dbg!(expected_text),
+ rendered.text, expected_text,
"wrong text for input {blocks:?}"
);
assert_eq!(
@@ -16,7 +16,6 @@ use language::{
};
use project::{FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view};
-use settings::Settings;
use smallvec::SmallVec;
use std::{
borrow::Cow,
@@ -27,7 +26,7 @@ use std::{
path::{Path, PathBuf},
};
use text::Selection;
-use util::{ResultExt, TryFutureExt};
+use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowableItemHandle};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
@@ -566,7 +565,7 @@ impl Item for Editor {
cx: &AppContext,
) -> AnyElement<T> {
Flex::row()
- .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).aligned())
+ .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any())
.with_children(detail.and_then(|detail| {
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
let description = path.to_string_lossy();
@@ -580,6 +579,7 @@ impl Item for Editor {
.aligned(),
)
}))
+ .align_children_center()
.into_any()
}
@@ -636,7 +636,7 @@ impl Item for Editor {
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
- self.report_editor_event("save", cx);
+ self.report_editor_event("save", None, cx);
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
let buffers = self.buffer().clone().read(cx).all_buffers();
cx.spawn(|_, mut cx| async move {
@@ -685,6 +685,11 @@ impl Item for Editor {
.as_singleton()
.expect("cannot call save_as on an excerpt list");
+ let file_extension = abs_path
+ .extension()
+ .map(|a| a.to_string_lossy().to_string());
+ self.report_editor_event("save", file_extension, cx);
+
project.update(cx, |project, cx| {
project.save_buffer_as(buffer, abs_path, cx)
})
@@ -1110,8 +1115,12 @@ impl View for CursorPosition {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(position) = self.position {
- let theme = &cx.global::<Settings>().theme.workspace.status_bar;
- let mut text = format!("{},{}", position.row + 1, position.column + 1);
+ let theme = &theme::current(cx).workspace.status_bar;
+ let mut text = format!(
+ "{}{FILE_ROW_COLUMN_DELIMITER}{}",
+ position.row + 1,
+ position.column + 1
+ );
if self.selected_count > 0 {
write!(text, " ({} selected)", self.selected_count).unwrap();
}
@@ -1,10 +1,8 @@
-use std::ops::Range;
-
use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
use gpui::{Task, ViewContext};
use language::{Bias, ToOffset};
use project::LocationLink;
-use settings::Settings;
+use std::ops::Range;
use util::TryFutureExt;
#[derive(Debug, Default)]
@@ -211,7 +209,7 @@ pub fn show_link_definition(
});
// Highlight symbol using theme link definition highlight style
- let style = cx.global::<Settings>().theme.editor.link_definition;
+ let style = theme::current(cx).editor.link_definition;
this.highlight_text::<LinkGoToDefinitionState>(
vec![highlight_range],
style,
@@ -297,6 +295,8 @@ fn go_to_fetched_definition_of_kind(
#[cfg(test)]
mod tests {
+ use super::*;
+ use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use futures::StreamExt;
use gpui::{
platform::{self, Modifiers, ModifiersChangedEvent},
@@ -305,12 +305,10 @@ mod tests {
use indoc::indoc;
use lsp::request::{GotoDefinition, GotoTypeDefinition};
- use crate::test::editor_lsp_test_context::EditorLspTestContext;
-
- use super::*;
-
#[gpui::test]
async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -417,6 +415,8 @@ mod tests {
#[gpui::test]
async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -57,13 +57,14 @@ pub fn deploy_context_menu(
#[cfg(test)]
mod tests {
- use crate::test::editor_lsp_test_context::EditorLspTestContext;
-
use super::*;
+ use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use indoc::indoc;
#[gpui::test]
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -369,11 +369,12 @@ pub fn split_display_range_by_lines(
mod tests {
use super::*;
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
- use settings::Settings;
+ use settings::SettingsStore;
#[gpui::test]
fn test_previous_word_start(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -400,7 +401,8 @@ mod tests {
#[gpui::test]
fn test_previous_subword_start(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -434,7 +436,8 @@ mod tests {
#[gpui::test]
fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
fn assert(
marked_text: &str,
cx: &mut gpui::AppContext,
@@ -466,7 +469,8 @@ mod tests {
#[gpui::test]
fn test_next_word_end(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -490,7 +494,8 @@ mod tests {
#[gpui::test]
fn test_next_subword_end(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -523,7 +528,8 @@ mod tests {
#[gpui::test]
fn test_find_boundary(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
fn assert(
marked_text: &str,
cx: &mut gpui::AppContext,
@@ -555,7 +561,8 @@ mod tests {
#[gpui::test]
fn test_surrounding_word(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -576,7 +583,8 @@ mod tests {
#[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_test(cx);
+
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
@@ -691,4 +699,11 @@ mod tests {
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
);
}
+
+ fn init_test(cx: &mut gpui::AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ language::init(cx);
+ crate::init(cx);
+ }
}
@@ -9,7 +9,9 @@ use git::diff::DiffHunk;
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
- char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
+ char_kind,
+ language_settings::{language_settings, LanguageSettings},
+ AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
@@ -1165,6 +1167,9 @@ impl MultiBuffer {
) {
self.sync(cx);
let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
+ if ids.is_empty() {
+ return;
+ }
let mut buffers = self.buffers.borrow_mut();
let mut snapshot = self.snapshot.borrow_mut();
@@ -1372,6 +1377,15 @@ impl MultiBuffer {
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
}
+ pub fn settings_at<'a, T: ToOffset>(
+ &self,
+ point: T,
+ cx: &'a AppContext,
+ ) -> &'a LanguageSettings {
+ let language = self.language_at(point, cx);
+ language_settings(language.map(|l| l.name()).as_deref(), cx)
+ }
+
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
self.buffers
.borrow()
@@ -2764,6 +2778,16 @@ impl MultiBufferSnapshot {
.and_then(|(buffer, offset)| buffer.language_at(offset))
}
+ pub fn settings_at<'a, T: ToOffset>(
+ &'a self,
+ point: T,
+ cx: &'a AppContext,
+ ) -> &'a LanguageSettings {
+ self.point_to_buffer_offset(point)
+ .map(|(buffer, offset)| buffer.settings_at(offset, cx))
+ .unwrap_or_else(|| language_settings(None, cx))
+ }
+
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
self.point_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_scope_at(offset))
@@ -2817,20 +2841,15 @@ impl MultiBufferSnapshot {
})
}
- pub fn git_diff_hunks_in_range<'a>(
+ pub fn git_diff_hunks_in_range_rev<'a>(
&'a self,
row_range: Range<u32>,
- reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.excerpts.cursor::<Point>();
- if reversed {
- cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
- if cursor.item().is_none() {
- cursor.prev(&());
- }
- } else {
- cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
+ cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
+ if cursor.item().is_none() {
+ cursor.prev(&());
}
std::iter::from_fn(move || {
@@ -2860,7 +2879,7 @@ impl MultiBufferSnapshot {
let buffer_hunks = excerpt
.buffer
- .git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed)
+ .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end)
.filter_map(move |hunk| {
let start = multibuffer_start.row
+ hunk
@@ -2880,12 +2899,70 @@ impl MultiBufferSnapshot {
})
});
- if reversed {
- cursor.prev(&());
- } else {
- cursor.next(&());
+ cursor.prev(&());
+
+ Some(buffer_hunks)
+ })
+ .flatten()
+ }
+
+ pub fn git_diff_hunks_in_range<'a>(
+ &'a self,
+ row_range: Range<u32>,
+ ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+ let mut cursor = self.excerpts.cursor::<Point>();
+
+ cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
+
+ std::iter::from_fn(move || {
+ let excerpt = cursor.item()?;
+ let multibuffer_start = *cursor.start();
+ let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
+ if multibuffer_start.row >= row_range.end {
+ return None;
}
+ let mut buffer_start = excerpt.range.context.start;
+ let mut buffer_end = excerpt.range.context.end;
+ let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
+ let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
+
+ if row_range.start > multibuffer_start.row {
+ let buffer_start_point =
+ excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
+ buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
+ }
+
+ if row_range.end < multibuffer_end.row {
+ let buffer_end_point =
+ excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
+ buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
+ }
+
+ let buffer_hunks = excerpt
+ .buffer
+ .git_diff_hunks_intersecting_range(buffer_start..buffer_end)
+ .filter_map(move |hunk| {
+ let start = multibuffer_start.row
+ + hunk
+ .buffer_range
+ .start
+ .saturating_sub(excerpt_start_point.row);
+ let end = multibuffer_start.row
+ + hunk
+ .buffer_range
+ .end
+ .min(excerpt_end_point.row + 1)
+ .saturating_sub(excerpt_start_point.row);
+
+ Some(DiffHunk {
+ buffer_range: start..end,
+ diff_base_byte_range: hunk.diff_base_byte_range.clone(),
+ })
+ });
+
+ cursor.next(&());
+
Some(buffer_hunks)
})
.flatten()
@@ -3785,10 +3862,9 @@ mod tests {
use gpui::{AppContext, TestAppContext};
use language::{Buffer, Rope};
use rand::prelude::*;
- use settings::Settings;
+ use settings::SettingsStore;
use std::{env, rc::Rc};
use unindent::Unindent;
-
use util::test::sample_text;
#[gpui::test]
@@ -4080,19 +4156,25 @@ mod tests {
let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+ let follower_edit_event_count = Rc::new(RefCell::new(0));
follower_multibuffer.update(cx, |_, cx| {
- cx.subscribe(&leader_multibuffer, |follower, _, event, cx| {
- match event.clone() {
+ let follower_edit_event_count = follower_edit_event_count.clone();
+ cx.subscribe(
+ &leader_multibuffer,
+ move |follower, _, event, cx| match event.clone() {
Event::ExcerptsAdded {
buffer,
predecessor,
excerpts,
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
+ Event::Edited => {
+ *follower_edit_event_count.borrow_mut() += 1;
+ }
_ => {}
- }
- })
+ },
+ )
.detach();
});
@@ -4131,6 +4213,7 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
+ assert_eq!(*follower_edit_event_count.borrow(), 2);
leader_multibuffer.update(cx, |leader, cx| {
let excerpt_ids = leader.excerpt_ids();
@@ -4140,6 +4223,27 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
+ assert_eq!(*follower_edit_event_count.borrow(), 3);
+
+ // Removing an empty set of excerpts is a noop.
+ leader_multibuffer.update(cx, |leader, cx| {
+ leader.remove_excerpts([], cx);
+ });
+ assert_eq!(
+ leader_multibuffer.read(cx).snapshot(cx).text(),
+ follower_multibuffer.read(cx).snapshot(cx).text(),
+ );
+ assert_eq!(*follower_edit_event_count.borrow(), 3);
+
+ // Adding an empty set of excerpts is a noop.
+ leader_multibuffer.update(cx, |leader, cx| {
+ leader.push_excerpts::<usize>(buffer_2.clone(), [], cx);
+ });
+ assert_eq!(
+ leader_multibuffer.read(cx).snapshot(cx).text(),
+ follower_multibuffer.read(cx).snapshot(cx).text(),
+ );
+ assert_eq!(*follower_edit_event_count.borrow(), 3);
leader_multibuffer.update(cx, |leader, cx| {
leader.clear(cx);
@@ -4148,6 +4252,7 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
+ assert_eq!(*follower_edit_event_count.borrow(), 4);
}
#[gpui::test]
@@ -4595,7 +4700,7 @@ mod tests {
assert_eq!(
snapshot
- .git_diff_hunks_in_range(0..12, false)
+ .git_diff_hunks_in_range(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
&expected,
@@ -4603,7 +4708,7 @@ mod tests {
assert_eq!(
snapshot
- .git_diff_hunks_in_range(0..12, true)
+ .git_diff_hunks_in_range_rev(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
expected
@@ -5034,7 +5139,8 @@ mod tests {
#[gpui::test]
fn test_history(cx: &mut AppContext) {
- cx.set_global(Settings::test(cx));
+ cx.set_global(SettingsStore::test(cx));
+
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -34,13 +34,17 @@ impl<'a> EditorLspTestContext<'a> {
) -> EditorLspTestContext<'a> {
use json::json;
+ let app_state = cx.update(AppState::test);
+
cx.update(|cx| {
+ theme::init((), cx);
+ language::init(cx);
crate::init(cx);
pane::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
});
- let app_state = cx.update(AppState::test);
-
let file_name = format!(
"file.{}",
language
@@ -1,19 +1,16 @@
-use std::{
- any::TypeId,
- ops::{Deref, DerefMut, Range},
-};
-
-use futures::Future;
-use indoc::indoc;
-
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
+use futures::Future;
use gpui::{
keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
};
+use indoc::indoc;
use language::{Buffer, BufferSnapshot};
-use settings::Settings;
+use std::{
+ any::TypeId,
+ ops::{Deref, DerefMut, Range},
+};
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
@@ -30,15 +27,10 @@ pub struct EditorTestContext<'a> {
impl<'a> EditorTestContext<'a> {
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let (window_id, editor) = cx.update(|cx| {
- cx.set_global(Settings::test(cx));
- crate::init(cx);
-
- let (window_id, editor) = cx.add_window(Default::default(), |cx| {
+ cx.add_window(Default::default(), |cx| {
cx.focus_self();
build_editor(MultiBuffer::build_simple("", cx), cx)
- });
-
- (window_id, editor)
+ })
});
Self {
@@ -212,6 +204,7 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, marked_text.to_string())
}
+ #[track_caller]
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
@@ -228,6 +221,7 @@ impl<'a> EditorTestContext<'a> {
assert_set_eq!(actual_ranges, expected_ranges);
}
+ #[track_caller]
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
@@ -241,12 +235,14 @@ impl<'a> EditorTestContext<'a> {
assert_set_eq!(actual_ranges, expected_ranges);
}
+ #[track_caller]
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text)
}
+ #[track_caller]
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
@@ -35,3 +35,6 @@ serde_derive.workspace = true
sysinfo = "0.27.1"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2"
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -3,7 +3,6 @@ use gpui::{
platform::{CursorStyle, MouseButton},
Entity, View, ViewContext, WeakViewHandle,
};
-use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
@@ -33,7 +32,7 @@ impl View for DeployFeedbackButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let active = self.active;
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
Stack::new()
.with_child(
MouseEventHandler::<Self, Self>::new(0, cx, |state, _| {
@@ -3,7 +3,6 @@ use gpui::{
platform::{CursorStyle, MouseButton},
AnyElement, Element, Entity, View, ViewContext, ViewHandle,
};
-use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo};
@@ -30,7 +29,7 @@ impl View for FeedbackInfoText {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
Flex::row()
.with_child(
@@ -5,7 +5,6 @@ use gpui::{
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
};
-use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
pub fn init(cx: &mut AppContext) {
@@ -46,7 +45,7 @@ impl View for SubmitFeedbackButton {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
enum SubmitFeedbackButton {}
MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
let style = theme.feedback.submit_button.style_for(state, false);
@@ -16,14 +16,19 @@ menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
+text = { path = "../text" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
postage.workspace = true
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
-serde_json.workspace = true
+language = { path = "../language", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }
+
+serde_json.workspace = true
ctor.workspace = true
env_logger.workspace = true
@@ -1,10 +1,10 @@
+use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::PathMatch;
use gpui::{
actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
-use settings::Settings;
use std::{
path::Path,
sync::{
@@ -12,7 +12,8 @@ use std::{
Arc,
},
};
-use util::{post_inc, ResultExt};
+use text::Point;
+use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::Workspace;
pub type FileFinder = Picker<FileFinderDelegate>;
@@ -23,11 +24,12 @@ pub struct FileFinderDelegate {
search_count: usize,
latest_search_id: usize,
latest_search_did_cancel: bool,
- latest_search_query: String,
- relative_to: Option<Arc<Path>>,
+ latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
+ currently_opened_path: Option<ProjectPath>,
matches: Vec<PathMatch>,
selected: Option<(usize, Arc<Path>)>,
cancel_flag: Arc<AtomicBool>,
+ history_items: Vec<ProjectPath>,
}
actions!(file_finder, [Toggle]);
@@ -37,17 +39,26 @@ pub fn init(cx: &mut AppContext) {
FileFinder::init(cx);
}
+const MAX_RECENT_SELECTIONS: usize = 20;
+
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
- let relative_to = workspace
+ let history_items = workspace.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx);
+ let currently_opened_path = workspace
.active_item(cx)
- .and_then(|item| item.project_path(cx))
- .map(|project_path| project_path.path.clone());
+ .and_then(|item| item.project_path(cx));
+
let project = workspace.project().clone();
let workspace = cx.handle().downgrade();
let finder = cx.add_view(|cx| {
Picker::new(
- FileFinderDelegate::new(workspace, project, relative_to, cx),
+ FileFinderDelegate::new(
+ workspace,
+ project,
+ currently_opened_path,
+ history_items,
+ cx,
+ ),
cx,
)
});
@@ -60,6 +71,21 @@ pub enum Event {
Dismissed,
}
+#[derive(Debug, Clone)]
+struct FileSearchQuery {
+ raw_query: String,
+ file_query_end: Option<usize>,
+}
+
+impl FileSearchQuery {
+ fn path_query(&self) -> &str {
+ match self.file_query_end {
+ Some(file_path_end) => &self.raw_query[..file_path_end],
+ None => &self.raw_query,
+ }
+ }
+}
+
impl FileFinderDelegate {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path;
@@ -90,7 +116,8 @@ impl FileFinderDelegate {
pub fn new(
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
- relative_to: Option<Arc<Path>>,
+ currently_opened_path: Option<ProjectPath>,
+ history_items: Vec<ProjectPath>,
cx: &mut ViewContext<FileFinder>,
) -> Self {
cx.observe(&project, |picker, _, cx| {
@@ -103,16 +130,24 @@ impl FileFinderDelegate {
search_count: 0,
latest_search_id: 0,
latest_search_did_cancel: false,
- latest_search_query: String::new(),
- relative_to,
+ latest_search_query: None,
+ currently_opened_path,
matches: Vec::new(),
selected: None,
cancel_flag: Arc::new(AtomicBool::new(false)),
+ history_items,
}
}
- fn spawn_search(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
- let relative_to = self.relative_to.clone();
+ fn spawn_search(
+ &mut self,
+ query: PathLikeWithPosition<FileSearchQuery>,
+ cx: &mut ViewContext<FileFinder>,
+ ) -> Task<()> {
+ let relative_to = self
+ .currently_opened_path
+ .as_ref()
+ .map(|project_path| Arc::clone(&project_path.path));
let worktrees = self
.project
.read(cx)
@@ -140,7 +175,7 @@ impl FileFinderDelegate {
cx.spawn(|picker, mut cx| async move {
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
- &query,
+ query.path_like.path_query(),
relative_to,
false,
100,
@@ -163,18 +198,24 @@ impl FileFinderDelegate {
&mut self,
search_id: usize,
did_cancel: bool,
- query: String,
+ query: PathLikeWithPosition<FileSearchQuery>,
matches: Vec<PathMatch>,
cx: &mut ViewContext<FileFinder>,
) {
if search_id >= self.latest_search_id {
self.latest_search_id = search_id;
- if self.latest_search_did_cancel && query == self.latest_search_query {
+ if self.latest_search_did_cancel
+ && Some(query.path_like.path_query())
+ == self
+ .latest_search_query
+ .as_ref()
+ .map(|query| query.path_like.path_query())
+ {
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
} else {
self.matches = matches;
}
- self.latest_search_query = query;
+ self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel;
cx.notify();
}
@@ -209,13 +250,42 @@ impl PickerDelegate for FileFinderDelegate {
cx.notify();
}
- fn update_matches(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
- if query.is_empty() {
+ fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
+ if raw_query.is_empty() {
self.latest_search_id = post_inc(&mut self.search_count);
self.matches.clear();
+
+ self.matches = self
+ .currently_opened_path
+ .iter() // if exists, bubble the currently opened path to the top
+ .chain(self.history_items.iter().filter(|history_item| {
+ Some(*history_item) != self.currently_opened_path.as_ref()
+ }))
+ .enumerate()
+ .map(|(i, history_item)| PathMatch {
+ score: i as f64,
+ positions: Vec::new(),
+ worktree_id: history_item.worktree_id.to_usize(),
+ path: Arc::clone(&history_item.path),
+ path_prefix: "".into(),
+ distance_to_relative_ancestor: usize::MAX,
+ })
+ .collect();
cx.notify();
Task::ready(())
} else {
+ let raw_query = &raw_query;
+ let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
+ Ok::<_, std::convert::Infallible>(FileSearchQuery {
+ raw_query: raw_query.to_owned(),
+ file_query_end: if path_like_str == raw_query {
+ None
+ } else {
+ Some(path_like_str.len())
+ },
+ })
+ })
+ .expect("infallible");
self.spawn_search(query, cx)
}
}
@@ -227,13 +297,48 @@ impl PickerDelegate for FileFinderDelegate {
worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(),
};
+ let open_task = workspace.update(cx, |workspace, cx| {
+ workspace.open_path(project_path.clone(), None, true, cx)
+ });
- workspace.update(cx, |workspace, cx| {
+ let workspace = workspace.downgrade();
+
+ let row = self
+ .latest_search_query
+ .as_ref()
+ .and_then(|query| query.row)
+ .map(|row| row.saturating_sub(1));
+ let col = self
+ .latest_search_query
+ .as_ref()
+ .and_then(|query| query.column)
+ .unwrap_or(0)
+ .saturating_sub(1);
+ cx.spawn(|_, mut cx| async move {
+ let item = open_task.await.log_err()?;
+ if let Some(row) = row {
+ if let Some(active_editor) = item.downcast::<Editor>() {
+ active_editor
+ .downgrade()
+ .update(&mut cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx).display_snapshot;
+ let point = snapshot
+ .buffer_snapshot
+ .clip_point(Point::new(row, col), Bias::Left);
+ editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+ s.select_ranges([point..point])
+ });
+ })
+ .log_err();
+ }
+ }
workspace
- .open_path(project_path.clone(), None, true, cx)
- .detach_and_log_err(cx);
- workspace.dismiss_modal(cx);
+ .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
+ .log_err();
+
+ Some(())
})
+ .detach();
}
}
}
@@ -248,8 +353,8 @@ impl PickerDelegate for FileFinderDelegate {
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
let path_match = &self.matches[ix];
- let settings = cx.global::<Settings>();
- let style = settings.theme.picker.item.style_for(mouse_state, selected);
+ let theme = theme::current(cx);
+ let style = theme.picker.item.style_for(mouse_state, selected);
let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match);
Flex::column()
@@ -268,8 +373,11 @@ impl PickerDelegate for FileFinderDelegate {
#[cfg(test)]
mod tests {
+ use std::{assert_eq, collections::HashMap, time::Duration};
+
use super::*;
use editor::Editor;
+ use gpui::{TestAppContext, ViewHandle};
use menu::{Confirm, SelectNext};
use serde_json::json;
use workspace::{AppState, Workspace};
@@ -282,13 +390,8 @@ mod tests {
}
#[gpui::test]
- async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
- let app_state = cx.update(|cx| {
- super::init(cx);
- editor::init(cx);
- AppState::test(cx)
- });
-
+ async fn test_matching_paths(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -338,8 +441,174 @@ mod tests {
}
#[gpui::test]
- async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) {
- let app_state = cx.update(AppState::test);
+ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+
+ let first_file_name = "first.rs";
+ let first_file_contents = "// First Rust file";
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "test": {
+ first_file_name: first_file_contents,
+ "second.rs": "// Second Rust file",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+ cx.dispatch_action(window_id, Toggle);
+ let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+
+ let file_query = &first_file_name[..3];
+ let file_row = 1;
+ let file_column = 3;
+ assert!(file_column <= first_file_contents.len());
+ let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
+ finder
+ .update(cx, |finder, cx| {
+ finder
+ .delegate_mut()
+ .update_matches(query_inside_file.to_string(), cx)
+ })
+ .await;
+ finder.read_with(cx, |finder, _| {
+ let finder = finder.delegate();
+ assert_eq!(finder.matches.len(), 1);
+ let latest_search_query = finder
+ .latest_search_query
+ .as_ref()
+ .expect("Finder should have a query after the update_matches call");
+ assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
+ assert_eq!(
+ latest_search_query.path_like.file_query_end,
+ Some(file_query.len())
+ );
+ assert_eq!(latest_search_query.row, Some(file_row));
+ assert_eq!(latest_search_query.column, Some(file_column as u32));
+ });
+
+ let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+ cx.dispatch_action(window_id, SelectNext);
+ cx.dispatch_action(window_id, Confirm);
+ active_pane
+ .condition(cx, |pane, _| pane.active_item().is_some())
+ .await;
+ let editor = cx.update(|cx| {
+ let active_item = active_pane.read(cx).active_item().unwrap();
+ active_item.downcast::<Editor>().unwrap()
+ });
+ cx.foreground().advance_clock(Duration::from_secs(2));
+ cx.foreground().start_waiting();
+ cx.foreground().finish_waiting();
+ editor.update(cx, |editor, cx| {
+ let all_selections = editor.selections.all_adjusted(cx);
+ assert_eq!(
+ all_selections.len(),
+ 1,
+ "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
+ );
+ let caret_selection = all_selections.into_iter().next().unwrap();
+ assert_eq!(caret_selection.start, caret_selection.end,
+ "Caret selection should have its start and end at the same position");
+ assert_eq!(file_row, caret_selection.start.row + 1,
+ "Query inside file should get caret with the same focus row");
+ assert_eq!(file_column, caret_selection.start.column as usize + 1,
+ "Query inside file should get caret with the same focus column");
+ });
+ }
+
+ #[gpui::test]
+ async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+
+ let first_file_name = "first.rs";
+ let first_file_contents = "// First Rust file";
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "test": {
+ first_file_name: first_file_contents,
+ "second.rs": "// Second Rust file",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+ cx.dispatch_action(window_id, Toggle);
+ let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+
+ let file_query = &first_file_name[..3];
+ let file_row = 200;
+ let file_column = 300;
+ assert!(file_column > first_file_contents.len());
+ let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
+ finder
+ .update(cx, |finder, cx| {
+ finder
+ .delegate_mut()
+ .update_matches(query_outside_file.to_string(), cx)
+ })
+ .await;
+ finder.read_with(cx, |finder, _| {
+ let finder = finder.delegate();
+ assert_eq!(finder.matches.len(), 1);
+ let latest_search_query = finder
+ .latest_search_query
+ .as_ref()
+ .expect("Finder should have a query after the update_matches call");
+ assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
+ assert_eq!(
+ latest_search_query.path_like.file_query_end,
+ Some(file_query.len())
+ );
+ assert_eq!(latest_search_query.row, Some(file_row));
+ assert_eq!(latest_search_query.column, Some(file_column as u32));
+ });
+
+ let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+ cx.dispatch_action(window_id, SelectNext);
+ cx.dispatch_action(window_id, Confirm);
+ active_pane
+ .condition(cx, |pane, _| pane.active_item().is_some())
+ .await;
+ let editor = cx.update(|cx| {
+ let active_item = active_pane.read(cx).active_item().unwrap();
+ active_item.downcast::<Editor>().unwrap()
+ });
+ cx.foreground().advance_clock(Duration::from_secs(2));
+ cx.foreground().start_waiting();
+ cx.foreground().finish_waiting();
+ editor.update(cx, |editor, cx| {
+ let all_selections = editor.selections.all_adjusted(cx);
+ assert_eq!(
+ all_selections.len(),
+ 1,
+ "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
+ );
+ let caret_selection = all_selections.into_iter().next().unwrap();
+ assert_eq!(caret_selection.start, caret_selection.end,
+ "Caret selection should have its start and end at the same position");
+ assert_eq!(0, caret_selection.start.row,
+ "Excessive rows (as in query outside file borders) should get trimmed to last file row");
+ assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
+ "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
+ });
+ }
+
+ #[gpui::test]
+ async fn test_matching_cancellation(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -365,13 +634,14 @@ mod tests {
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
+ Vec::new(),
cx,
),
cx,
)
});
- let query = "hi".to_string();
+ let query = test_path_like("hi");
finder
.update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
.await;
@@ -407,8 +677,8 @@ mod tests {
}
#[gpui::test]
- async fn test_ignored_files(cx: &mut gpui::TestAppContext) {
- let app_state = cx.update(AppState::test);
+ async fn test_ignored_files(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -449,20 +719,23 @@ mod tests {
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
+ Vec::new(),
cx,
),
cx,
)
});
finder
- .update(cx, |f, cx| f.delegate_mut().spawn_search("hi".into(), cx))
+ .update(cx, |f, cx| {
+ f.delegate_mut().spawn_search(test_path_like("hi"), cx)
+ })
.await;
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
}
#[gpui::test]
- async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) {
- let app_state = cx.update(AppState::test);
+ async fn test_single_file_worktrees(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -482,6 +755,7 @@ mod tests {
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
+ Vec::new(),
cx,
),
cx,
@@ -491,7 +765,9 @@ mod tests {
// Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file.
finder
- .update(cx, |f, cx| f.delegate_mut().spawn_search("thf".into(), cx))
+ .update(cx, |f, cx| {
+ f.delegate_mut().spawn_search(test_path_like("thf"), cx)
+ })
.await;
cx.read(|cx| {
let finder = finder.read(cx);
@@ -509,16 +785,16 @@ mod tests {
// Since the worktree root is a file, searching for its name followed by a slash does
// not match anything.
finder
- .update(cx, |f, cx| f.delegate_mut().spawn_search("thf/".into(), cx))
+ .update(cx, |f, cx| {
+ f.delegate_mut().spawn_search(test_path_like("thf/"), cx)
+ })
.await;
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
}
#[gpui::test]
- async fn test_multiple_matches_with_same_relative_path(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
-
- let app_state = cx.update(AppState::test);
+ async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -545,6 +821,7 @@ mod tests {
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
+ Vec::new(),
cx,
),
cx,
@@ -553,7 +830,9 @@ mod tests {
// Run a search that matches two files with the same relative path.
finder
- .update(cx, |f, cx| f.delegate_mut().spawn_search("a.t".into(), cx))
+ .update(cx, |f, cx| {
+ f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
+ })
.await;
// Can switch between different matches with the same relative path.
@@ -569,10 +848,8 @@ mod tests {
}
#[gpui::test]
- async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
-
- let app_state = cx.update(AppState::test);
+ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -590,17 +867,26 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let worktree_id = cx.read(|cx| {
+ let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ WorktreeId::from_usize(worktrees[0].id())
+ });
// When workspace has an active item, sort items which are closer to that item
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
// so that one should be sorted earlier
- let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
+ let b_path = Some(ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("/root/dir2/b.txt")),
+ });
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
b_path,
+ Vec::new(),
cx,
),
cx,
@@ -609,7 +895,7 @@ mod tests {
finder
.update(cx, |f, cx| {
- f.delegate_mut().spawn_search("a.txt".into(), cx)
+ f.delegate_mut().spawn_search(test_path_like("a.txt"), cx)
})
.await;
@@ -621,8 +907,8 @@ mod tests {
}
#[gpui::test]
- async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
- let app_state = cx.update(AppState::test);
+ async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -645,17 +931,288 @@ mod tests {
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
+ Vec::new(),
cx,
),
cx,
)
});
finder
- .update(cx, |f, cx| f.delegate_mut().spawn_search("dir".into(), cx))
+ .update(cx, |f, cx| {
+ f.delegate_mut().spawn_search(test_path_like("dir"), cx)
+ })
.await;
cx.read(|cx| {
let finder = finder.read(cx);
assert_eq!(finder.delegate().matches.len(), 0);
});
}
+
+ #[gpui::test]
+ async fn test_query_history(
+ deterministic: Arc<gpui::executor::Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let app_state = init_test(cx);
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "test": {
+ "first.rs": "// First Rust file",
+ "second.rs": "// Second Rust file",
+ "third.rs": "// Third Rust file",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let worktree_id = cx.read(|cx| {
+ let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ WorktreeId::from_usize(worktrees[0].id())
+ });
+
+ // Open and close panels, getting their history items afterwards.
+ // Ensure history items get populated with opened items, and items are kept in a certain order.
+ // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
+ //
+ // TODO: without closing, the opened items do not propagate their history changes for some reason
+ // it does work in real app though, only tests do not propagate.
+
+ let initial_history = open_close_queried_buffer(
+ "fir",
+ 1,
+ "first.rs",
+ window_id,
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+ assert!(
+ initial_history.is_empty(),
+ "Should have no history before opening any files"
+ );
+
+ let history_after_first = open_close_queried_buffer(
+ "sec",
+ 1,
+ "second.rs",
+ window_id,
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+ assert_eq!(
+ history_after_first,
+ vec![ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/first.rs")),
+ }],
+ "Should show 1st opened item in the history when opening the 2nd item"
+ );
+
+ let history_after_second = open_close_queried_buffer(
+ "thi",
+ 1,
+ "third.rs",
+ window_id,
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+ assert_eq!(
+ history_after_second,
+ vec![
+ ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/second.rs")),
+ },
+ ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/first.rs")),
+ },
+ ],
+ "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
+2nd item should be the first in the history, as the last opened."
+ );
+
+ let history_after_third = open_close_queried_buffer(
+ "sec",
+ 1,
+ "second.rs",
+ window_id,
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+ assert_eq!(
+ history_after_third,
+ vec![
+ ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/third.rs")),
+ },
+ ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/second.rs")),
+ },
+ ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/first.rs")),
+ },
+ ],
+ "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
+3rd item should be the first in the history, as the last opened."
+ );
+
+ let history_after_second_again = open_close_queried_buffer(
+ "thi",
+ 1,
+ "third.rs",
+ window_id,
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+ assert_eq!(
+ history_after_second_again,
+ vec![
+ ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/second.rs")),
+ },
+ ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/third.rs")),
+ },
+ ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("test/first.rs")),
+ },
+ ],
+ "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
+2nd item, as the last opened, 3rd item should go next as it was opened right before."
+ );
+ }
+
+ async fn open_close_queried_buffer(
+ input: &str,
+ expected_matches: usize,
+ expected_editor_title: &str,
+ window_id: usize,
+ workspace: &ViewHandle<Workspace>,
+ deterministic: &gpui::executor::Deterministic,
+ cx: &mut gpui::TestAppContext,
+ ) -> Vec<ProjectPath> {
+ cx.dispatch_action(window_id, Toggle);
+ let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+ finder
+ .update(cx, |finder, cx| {
+ finder.delegate_mut().update_matches(input.to_string(), cx)
+ })
+ .await;
+ let history_items = finder.read_with(cx, |finder, _| {
+ assert_eq!(
+ finder.delegate().matches.len(),
+ expected_matches,
+ "Unexpected number of matches found for query {input}"
+ );
+ finder.delegate().history_items.clone()
+ });
+
+ let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+ cx.dispatch_action(window_id, SelectNext);
+ cx.dispatch_action(window_id, Confirm);
+ deterministic.run_until_parked();
+ active_pane
+ .condition(cx, |pane, _| pane.active_item().is_some())
+ .await;
+ cx.read(|cx| {
+ let active_item = active_pane.read(cx).active_item().unwrap();
+ let active_editor_title = active_item
+ .as_any()
+ .downcast_ref::<Editor>()
+ .unwrap()
+ .read(cx)
+ .title(cx);
+ assert_eq!(
+ expected_editor_title, active_editor_title,
+ "Unexpected editor title for query {input}"
+ );
+ });
+
+ let mut original_items = HashMap::new();
+ cx.read(|cx| {
+ for pane in workspace.read(cx).panes() {
+ let pane_id = pane.id();
+ let pane = pane.read(cx);
+ let insertion_result = original_items.insert(pane_id, pane.items().count());
+ assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
+ }
+ });
+ active_pane
+ .update(cx, |pane, cx| {
+ pane.close_active_item(&workspace::CloseActiveItem, cx)
+ .unwrap()
+ })
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+ cx.read(|cx| {
+ for pane in workspace.read(cx).panes() {
+ let pane_id = pane.id();
+ let pane = pane.read(cx);
+ match original_items.remove(&pane_id) {
+ Some(original_items) => {
+ assert_eq!(
+ pane.items().count(),
+ original_items.saturating_sub(1),
+ "Pane id {pane_id} should have item closed"
+ );
+ }
+ None => panic!("Pane id {pane_id} not found in original items"),
+ }
+ }
+ });
+
+ history_items
+ }
+
+ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| {
+ let state = AppState::test(cx);
+ theme::init((), cx);
+ language::init(cx);
+ super::init(cx);
+ editor::init(cx);
+ workspace::init_settings(cx);
+ state
+ })
+ }
+
+ fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
+ PathLikeWithPosition::parse_str(test_str, |path_like_str| {
+ Ok::<_, std::convert::Infallible>(FileSearchQuery {
+ raw_query: test_str.to_owned(),
+ file_query_end: if path_like_str == test_str {
+ None
+ } else {
+ Some(path_like_str.len())
+ },
+ })
+ })
+ .unwrap()
+ }
}
@@ -13,6 +13,7 @@ gpui = { path = "../gpui" }
lsp = { path = "../lsp" }
rope = { path = "../rope" }
util = { path = "../util" }
+sum_tree = { path = "../sum_tree" }
anyhow.workspace = true
async-trait.workspace = true
futures.workspace = true
@@ -27,7 +27,7 @@ use util::ResultExt;
#[cfg(any(test, feature = "test-support"))]
use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))]
-use repository::FakeGitRepositoryState;
+use repository::{FakeGitRepositoryState, GitFileStatus};
#[cfg(any(test, feature = "test-support"))]
use std::sync::Weak;
@@ -572,15 +572,15 @@ impl FakeFs {
Ok(())
}
- pub async fn pause_events(&self) {
+ pub fn pause_events(&self) {
self.state.lock().events_paused = true;
}
- pub async fn buffered_event_count(&self) -> usize {
+ pub fn buffered_event_count(&self) -> usize {
self.state.lock().buffered_events.len()
}
- pub async fn flush_events(&self, count: usize) {
+ pub fn flush_events(&self, count: usize) {
self.state.lock().flush_events(count);
}
@@ -654,6 +654,17 @@ impl FakeFs {
});
}
+ pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) {
+ self.with_git_state(dot_git, |state| {
+ state.worktree_statuses.clear();
+ state.worktree_statuses.extend(
+ statuses
+ .iter()
+ .map(|(path, content)| ((**path).into(), content.clone())),
+ );
+ });
+ }
+
pub fn paths(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
@@ -821,14 +832,16 @@ impl Fs for FakeFs {
let old_path = normalize_path(old_path);
let new_path = normalize_path(new_path);
+
let mut state = self.state.lock();
let moved_entry = state.write_path(&old_path, |e| {
if let btree_map::Entry::Occupied(e) = e {
- Ok(e.remove())
+ Ok(e.get().clone())
} else {
Err(anyhow!("path does not exist: {}", &old_path.display()))
}
})?;
+
state.write_path(&new_path, |e| {
match e {
btree_map::Entry::Occupied(mut e) => {
@@ -844,6 +857,17 @@ impl Fs for FakeFs {
}
Ok(())
})?;
+
+ state
+ .write_path(&old_path, |e| {
+ if let btree_map::Entry::Occupied(e) = e {
+ Ok(e.remove())
+ } else {
+ unreachable!()
+ }
+ })
+ .unwrap();
+
state.emit_event(&[old_path, new_path]);
Ok(())
}
@@ -1,10 +1,15 @@
use anyhow::Result;
use collections::HashMap;
use parking_lot::Mutex;
+use serde_derive::{Deserialize, Serialize};
use std::{
+ cmp::Ordering,
+ ffi::OsStr,
+ os::unix::prelude::OsStrExt,
path::{Component, Path, PathBuf},
sync::Arc,
};
+use sum_tree::{MapSeekTarget, TreeMap};
use util::ResultExt;
pub use git2::Repository as LibGitRepository;
@@ -16,6 +21,10 @@ pub trait GitRepository: Send {
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
fn branch_name(&self) -> Option<String>;
+
+ fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
+
+ fn status(&self, path: &RepoPath) -> Option<GitFileStatus>;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -61,6 +70,48 @@ impl GitRepository for LibGitRepository {
let branch = String::from_utf8_lossy(head.shorthand_bytes());
Some(branch.to_string())
}
+
+ fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
+ let statuses = self.statuses(None).log_err()?;
+
+ let mut map = TreeMap::default();
+
+ for status in statuses
+ .iter()
+ .filter(|status| !status.status().contains(git2::Status::IGNORED))
+ {
+ let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
+ let Some(status) = read_status(status.status()) else {
+ continue
+ };
+
+ map.insert(path, status)
+ }
+
+ Some(map)
+ }
+
+ fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
+ let status = self.status_file(path).log_err()?;
+ read_status(status)
+ }
+}
+
+fn read_status(status: git2::Status) -> Option<GitFileStatus> {
+ if status.contains(git2::Status::CONFLICTED) {
+ Some(GitFileStatus::Conflict)
+ } else if status.intersects(
+ git2::Status::WT_MODIFIED
+ | git2::Status::WT_RENAMED
+ | git2::Status::INDEX_MODIFIED
+ | git2::Status::INDEX_RENAMED,
+ ) {
+ Some(GitFileStatus::Modified)
+ } else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) {
+ Some(GitFileStatus::Added)
+ } else {
+ None
+ }
}
#[derive(Debug, Clone, Default)]
@@ -71,6 +122,7 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone, Default)]
pub struct FakeGitRepositoryState {
pub index_contents: HashMap<PathBuf, String>,
+ pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
pub branch_name: Option<String>,
}
@@ -93,6 +145,20 @@ impl GitRepository for FakeGitRepository {
let state = self.state.lock();
state.branch_name.clone()
}
+
+ fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
+ let state = self.state.lock();
+ let mut map = TreeMap::default();
+ for (repo_path, status) in state.worktree_statuses.iter() {
+ map.insert(repo_path.to_owned(), status.to_owned());
+ }
+ Some(map)
+ }
+
+ fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
+ let state = self.state.lock();
+ state.worktree_statuses.get(path).cloned()
+ }
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@@ -123,3 +189,66 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
_ => Ok(()),
}
}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum GitFileStatus {
+ Added,
+ Modified,
+ Conflict,
+}
+
+#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
+pub struct RepoPath(PathBuf);
+
+impl RepoPath {
+ pub fn new(path: PathBuf) -> Self {
+ debug_assert!(path.is_relative(), "Repo paths must be relative");
+
+ RepoPath(path)
+ }
+}
+
+impl From<&Path> for RepoPath {
+ fn from(value: &Path) -> Self {
+ RepoPath::new(value.to_path_buf())
+ }
+}
+
+impl From<PathBuf> for RepoPath {
+ fn from(value: PathBuf) -> Self {
+ RepoPath::new(value)
+ }
+}
+
+impl Default for RepoPath {
+ fn default() -> Self {
+ RepoPath(PathBuf::new())
+ }
+}
+
+impl AsRef<Path> for RepoPath {
+ fn as_ref(&self) -> &Path {
+ self.0.as_ref()
+ }
+}
+
+impl std::ops::Deref for RepoPath {
+ type Target = PathBuf;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+#[derive(Debug)]
+pub struct RepoPathDescendants<'a>(pub &'a Path);
+
+impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
+ fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
+ if key.starts_with(&self.0) {
+ Ordering::Greater
+ } else {
+ self.0.cmp(key)
+ }
+ }
+}
@@ -1,4 +1,4 @@
-use std::ops::Range;
+use std::{iter, ops::Range};
use sum_tree::SumTree;
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
@@ -75,18 +75,17 @@ impl BufferDiff {
&'a self,
range: Range<u32>,
buffer: &'a BufferSnapshot,
- reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = buffer.anchor_before(Point::new(range.start, 0));
let end = buffer.anchor_after(Point::new(range.end, 0));
- self.hunks_intersecting_range(start..end, buffer, reversed)
+
+ self.hunks_intersecting_range(start..end, buffer)
}
pub fn hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
buffer: &'a BufferSnapshot,
- reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
@@ -94,15 +93,51 @@ impl BufferDiff {
!before_start && !after_end
});
- std::iter::from_fn(move || {
- if reversed {
- cursor.prev(buffer);
+ let anchor_iter = std::iter::from_fn(move || {
+ cursor.next(buffer);
+ cursor.item()
+ })
+ .flat_map(move |hunk| {
+ [
+ (&hunk.buffer_range.start, hunk.diff_base_byte_range.start),
+ (&hunk.buffer_range.end, hunk.diff_base_byte_range.end),
+ ]
+ .into_iter()
+ });
+
+ let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
+ iter::from_fn(move || {
+ let (start_point, start_base) = summaries.next()?;
+ let (end_point, end_base) = summaries.next()?;
+
+ let end_row = if end_point.column > 0 {
+ end_point.row + 1
} else {
- cursor.next(buffer);
- }
+ end_point.row
+ };
- let hunk = cursor.item()?;
+ Some(DiffHunk {
+ buffer_range: start_point.row..end_row,
+ diff_base_byte_range: start_base..end_base,
+ })
+ })
+ }
+ pub fn hunks_intersecting_range_rev<'a>(
+ &'a self,
+ range: Range<Anchor>,
+ buffer: &'a BufferSnapshot,
+ ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+ let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
+ let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
+ let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
+ !before_start && !after_end
+ });
+
+ std::iter::from_fn(move || {
+ cursor.prev(buffer);
+
+ let hunk = cursor.item()?;
let range = hunk.buffer_range.to_point(buffer);
let end_row = if range.end.column > 0 {
range.end.row + 1
@@ -151,7 +186,7 @@ impl BufferDiff {
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = text.anchor_before(Point::new(0, 0));
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
- self.hunks_intersecting_range(start..end, text, false)
+ self.hunks_intersecting_range(start..end, text)
}
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
@@ -279,6 +314,8 @@ pub fn assert_hunks<Iter>(
#[cfg(test)]
mod tests {
+ use std::assert_eq;
+
use super::*;
use text::Buffer;
use unindent::Unindent as _;
@@ -365,7 +402,7 @@ mod tests {
assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks(
- diff.hunks_in_row_range(7..12, &buffer, false),
+ diff.hunks_in_row_range(7..12, &buffer),
&buffer,
&diff_base,
&[
@@ -16,3 +16,8 @@ settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
postage.workspace = true
+theme = { path = "../theme" }
+util = { path = "../util" }
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -1,13 +1,13 @@
use std::sync::Arc;
-use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
+use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity,
View, ViewContext, ViewHandle,
};
use menu::{Cancel, Confirm};
-use settings::Settings;
use text::{Bias, Point};
+use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::{Modal, Workspace};
actions!(go_to_line, [Toggle]);
@@ -75,15 +75,16 @@ impl GoToLine {
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
self.prev_scroll_position.take();
- self.active_editor.update(cx, |active_editor, cx| {
- if let Some(rows) = active_editor.highlighted_rows() {
+ if let Some(point) = self.point_from_query(cx) {
+ self.active_editor.update(cx, |active_editor, cx| {
let snapshot = active_editor.snapshot(cx).display_snapshot;
- let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
+ let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
- s.select_ranges([position..position])
+ s.select_ranges([point..point])
});
- }
- });
+ });
+ }
+
cx.emit(Event::Dismissed);
}
@@ -96,16 +97,7 @@ impl GoToLine {
match event {
editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::BufferEdited { .. } => {
- let line_editor = self.line_editor.read(cx).text(cx);
- let mut components = line_editor.trim().split(&[',', ':'][..]);
- let row = components.next().and_then(|row| row.parse::<u32>().ok());
- let column = components.next().and_then(|row| row.parse::<u32>().ok());
- if let Some(point) = row.map(|row| {
- Point::new(
- row.saturating_sub(1),
- column.map(|column| column.saturating_sub(1)).unwrap_or(0),
- )
- }) {
+ if let Some(point) = self.point_from_query(cx) {
self.active_editor.update(cx, |active_editor, cx| {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
@@ -120,6 +112,20 @@ impl GoToLine {
_ => {}
}
}
+
+ fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
+ let line_editor = self.line_editor.read(cx).text(cx);
+ let mut components = line_editor
+ .splitn(2, FILE_ROW_COLUMN_DELIMITER)
+ .map(str::trim)
+ .fuse();
+ let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
+ let column = components.next().and_then(|col| col.parse::<u32>().ok());
+ Some(Point::new(
+ row.saturating_sub(1),
+ column.unwrap_or(0).saturating_sub(1),
+ ))
+ }
}
impl Entity for GoToLine {
@@ -144,10 +150,10 @@ impl View for GoToLine {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &cx.global::<Settings>().theme.picker;
+ let theme = &theme::current(cx).picker;
let label = format!(
- "{},{} of {} lines",
+ "{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines",
self.cursor_point.row + 1,
self.cursor_point.column + 1,
self.max_point.row + 1
@@ -48,7 +48,7 @@ smallvec.workspace = true
smol.workspace = true
time.workspace = true
tiny-skia = "0.5"
-usvg = "0.14"
+usvg = { version = "0.14", features = [] }
uuid = { version = "1.1.2", features = ["v4"] }
waker-fn = "1.1.0"
@@ -72,7 +72,7 @@ cocoa = "0.24"
core-foundation = { version = "0.9.3", features = ["with-uuid"] }
core-graphics = "0.22.3"
core-text = "19.2"
-font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }
+font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" }
foreign-types = "0.3"
log.workspace = true
metal = "0.21.0"
@@ -1174,7 +1174,7 @@ impl AppContext {
this.notify_global(type_id);
result
} else {
- panic!("No global added for {}", std::any::type_name::<T>());
+ panic!("no global added for {}", std::any::type_name::<T>());
}
}
@@ -1182,6 +1182,15 @@ impl AppContext {
self.globals.clear();
}
+ pub fn remove_global<T: 'static>(&mut self) -> T {
+ *self
+ .globals
+ .remove(&TypeId::of::<T>())
+ .unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::<T>()))
+ .downcast()
+ .unwrap()
+ }
+
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
@@ -270,7 +270,7 @@ impl TestAppContext {
.borrow_mut()
.pop_front()
.expect("prompt was not called");
- let _ = done_tx.try_send(answer);
+ done_tx.try_send(answer).ok();
}
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
@@ -42,7 +42,7 @@ impl Color {
}
pub fn yellow() -> Self {
- Self(ColorU::from_u32(0x00ffffff))
+ Self(ColorU::from_u32(0xffff00ff))
}
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
@@ -576,6 +576,15 @@ pub struct ComponentHost<V: View, C: Component<V>> {
view_type: PhantomData<V>,
}
+impl<V: View, C: Component<V>> ComponentHost<V, C> {
+ pub fn new(c: C) -> Self {
+ Self {
+ component: c,
+ view_type: PhantomData,
+ }
+ }
+}
+
impl<V: View, C: Component<V>> Deref for ComponentHost<V, C> {
type Target = C;
@@ -477,6 +477,14 @@ impl Deterministic {
state.rng = StdRng::seed_from_u64(state.seed);
}
+ pub fn allow_parking(&self) {
+ use rand::prelude::*;
+
+ let mut state = self.state.lock();
+ state.forbid_parking = false;
+ state.rng = StdRng::seed_from_u64(state.seed);
+ }
+
pub async fn simulate_random_delay(&self) {
use rand::prelude::*;
use smol::future::yield_now;
@@ -698,6 +706,14 @@ impl Foreground {
}
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn allow_parking(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.allow_parking(),
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn advance_clock(&self, duration: Duration) {
match self {
@@ -11,6 +11,19 @@ pub struct Binding {
context_predicate: Option<KeymapContextPredicate>,
}
+impl std::fmt::Debug for Binding {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "Binding {{ keystrokes: {:?}, action: {}::{}, context_predicate: {:?} }}",
+ self.keystrokes,
+ self.action.namespace(),
+ self.action.name(),
+ self.context_predicate
+ )
+ }
+}
+
impl Clone for Binding {
fn clone(&self) -> Self {
Self {
@@ -755,7 +755,7 @@ impl platform::Window for Window {
let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap());
}
});
-
+ let block = block.copy();
let native_window = self.0.borrow().native_window;
self.0
.borrow()
@@ -13,9 +13,15 @@ editor = { path = "../editor" }
gpui = { path = "../gpui" }
util = { path = "../util" }
workspace = { path = "../workspace" }
+settings = { path = "../settings" }
+
anyhow.workspace = true
chrono = "0.4"
dirs = "4.0"
+serde.workspace = true
+schemars.workspace = true
log.workspace = true
-settings = { path = "../settings" }
shellexpand = "2.1.0"
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -1,7 +1,9 @@
+use anyhow::Result;
use chrono::{Datelike, Local, NaiveTime, Timelike};
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{actions, AppContext};
-use settings::{HourFormat, Settings};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
use std::{
fs::OpenOptions,
path::{Path, PathBuf},
@@ -11,13 +13,48 @@ use workspace::AppState;
actions!(journal, [NewJournalEntry]);
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+pub struct JournalSettings {
+ pub path: Option<String>,
+ pub hour_format: Option<HourFormat>,
+}
+
+impl Default for JournalSettings {
+ fn default() -> Self {
+ Self {
+ path: Some("~".into()),
+ hour_format: Some(Default::default()),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum HourFormat {
+ #[default]
+ Hour12,
+ Hour24,
+}
+
+impl settings::Setting for JournalSettings {
+ const KEY: Option<&'static str> = Some("journal");
+
+ type FileContent = Self;
+
+ fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
+
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+ settings::register::<JournalSettings>(cx);
+
cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx));
}
pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
- let settings = cx.global::<Settings>();
- let journal_dir = match journal_dir(&settings) {
+ let settings = settings::get::<JournalSettings>(cx);
+ let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) {
Some(journal_dir) => journal_dir,
None => {
log::error!("Can't determine journal directory");
@@ -31,8 +68,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
.join(format!("{:02}", now.month()));
let entry_path = month_dir.join(format!("{:02}.md", now.day()));
let now = now.time();
- let hour_format = &settings.journal_overrides.hour_format;
- let entry_heading = heading_entry(now, &hour_format);
+ let entry_heading = heading_entry(now, &settings.hour_format);
let create_entry = cx.background().spawn(async move {
std::fs::create_dir_all(month_dir)?;
@@ -76,14 +112,8 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
.detach_and_log_err(cx);
}
-fn journal_dir(settings: &Settings) -> Option<PathBuf> {
- let journal_dir = settings
- .journal_overrides
- .path
- .as_ref()
- .unwrap_or(settings.journal_defaults.path.as_ref()?);
-
- let expanded_journal_dir = shellexpand::full(&journal_dir) //TODO handle this better
+fn journal_dir(path: &str) -> Option<PathBuf> {
+ let expanded_journal_dir = shellexpand::full(path) //TODO handle this better
.ok()
.map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"));
@@ -36,16 +36,19 @@ sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
theme = { path = "../theme" }
util = { path = "../util" }
+
anyhow.workspace = true
async-broadcast = "0.4"
async-trait.workspace = true
futures.workspace = true
+globset.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true
+schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
@@ -5,6 +5,7 @@ pub use crate::{
};
use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
+ language_settings::{language_settings, LanguageSettings},
outline::OutlineItem,
syntax_map::{
SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint,
@@ -18,7 +19,6 @@ use futures::FutureExt as _;
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
use lsp::LanguageServerId;
use parking_lot::Mutex;
-use settings::Settings;
use similar::{ChangeTag, TextDiff};
use smallvec::SmallVec;
use smol::future::yield_now;
@@ -1827,11 +1827,11 @@ impl BufferSnapshot {
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
let language_name = self.language_at(position).map(|language| language.name());
- let settings = cx.global::<Settings>();
- if settings.hard_tabs(language_name.as_deref()) {
+ let settings = language_settings(language_name.as_deref(), cx);
+ if settings.hard_tabs {
IndentSize::tab()
} else {
- IndentSize::spaces(settings.tab_size(language_name.as_deref()).get())
+ IndentSize::spaces(settings.tab_size.get())
}
}
@@ -2146,6 +2146,15 @@ impl BufferSnapshot {
.or(self.language.as_ref())
}
+ pub fn settings_at<'a, D: ToOffset>(
+ &self,
+ position: D,
+ cx: &'a AppContext,
+ ) -> &'a LanguageSettings {
+ let language = self.language_at(position);
+ language_settings(language.map(|l| l.name()).as_deref(), cx)
+ }
+
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
let offset = position.to_offset(self);
@@ -2500,18 +2509,22 @@ impl BufferSnapshot {
pub fn git_diff_hunks_in_row_range<'a>(
&'a self,
range: Range<u32>,
- reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
- self.git_diff.hunks_in_row_range(range, self, reversed)
+ self.git_diff.hunks_in_row_range(range, self)
}
pub fn git_diff_hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
- reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
- self.git_diff
- .hunks_intersecting_range(range, self, reversed)
+ self.git_diff.hunks_intersecting_range(range, self)
+ }
+
+ pub fn git_diff_hunks_intersecting_range_rev<'a>(
+ &'a self,
+ range: Range<Anchor>,
+ ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
+ self.git_diff.hunks_intersecting_range_rev(range, self)
}
pub fn diagnostics_in_range<'a, T, O>(
@@ -1,3 +1,7 @@
+use crate::language_settings::{
+ AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent,
+};
+
use super::*;
use clock::ReplicaId;
use collections::BTreeMap;
@@ -7,7 +11,7 @@ use indoc::indoc;
use proto::deserialize_operation;
use rand::prelude::*;
use regex::RegexBuilder;
-use settings::Settings;
+use settings::SettingsStore;
use std::{
cell::RefCell,
env,
@@ -36,7 +40,8 @@ fn init_logger() {
#[gpui::test]
fn test_line_endings(cx: &mut gpui::AppContext) {
- cx.set_global(Settings::test(cx));
+ init_settings(cx, |_| {});
+
cx.add_model(|cx| {
let mut buffer =
Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx);
@@ -862,8 +867,7 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
- let settings = Settings::test(cx);
- cx.set_global(settings);
+ init_settings(cx, |_| {});
cx.add_model(|cx| {
let text = "fn a() {}";
@@ -903,9 +907,9 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
- let mut settings = Settings::test(cx);
- settings.editor_overrides.hard_tabs = Some(true);
- cx.set_global(settings);
+ init_settings(cx, |settings| {
+ settings.defaults.hard_tabs = Some(true);
+ });
cx.add_model(|cx| {
let text = "fn a() {}";
@@ -945,8 +949,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) {
- let settings = Settings::test(cx);
- cx.set_global(settings);
+ init_settings(cx, |_| {});
cx.add_model(|cx| {
let mut buffer = Buffer::new(
@@ -1082,8 +1085,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
#[gpui::test]
fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) {
- let settings = Settings::test(cx);
- cx.set_global(settings);
+ init_settings(cx, |_| {});
cx.add_model(|cx| {
let mut buffer = Buffer::new(
@@ -1145,7 +1147,8 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap
#[gpui::test]
fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
- cx.set_global(Settings::test(cx));
+ init_settings(cx, |_| {});
+
cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
@@ -1201,7 +1204,8 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
- cx.set_global(Settings::test(cx));
+ init_settings(cx, |_| {});
+
cx.add_model(|cx| {
let text = "a\nb";
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
@@ -1217,7 +1221,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
- cx.set_global(Settings::test(cx));
+ init_settings(cx, |_| {});
+
cx.add_model(|cx| {
let text = "
const a: usize = 1;
@@ -1257,7 +1262,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_block_mode(cx: &mut AppContext) {
- cx.set_global(Settings::test(cx));
+ init_settings(cx, |_| {});
+
cx.add_model(|cx| {
let text = r#"
fn a() {
@@ -1339,7 +1345,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) {
- cx.set_global(Settings::test(cx));
+ init_settings(cx, |_| {});
+
cx.add_model(|cx| {
let text = r#"
fn a() {
@@ -1417,7 +1424,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
#[gpui::test]
fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
- cx.set_global(Settings::test(cx));
+ init_settings(cx, |_| {});
+
cx.add_model(|cx| {
let text = "
* one
@@ -1460,25 +1468,23 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
- cx.set_global({
- let mut settings = Settings::test(cx);
- settings.language_overrides.extend([
+ init_settings(cx, |settings| {
+ settings.languages.extend([
(
"HTML".into(),
- settings::EditorSettings {
+ LanguageSettingsContent {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
),
(
"JavaScript".into(),
- settings::EditorSettings {
+ LanguageSettingsContent {
tab_size: Some(8.try_into().unwrap()),
..Default::default()
},
),
- ]);
- settings
+ ])
});
let html_language = Arc::new(
@@ -1574,9 +1580,10 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
#[gpui::test]
fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
- let mut settings = Settings::test(cx);
- settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
- cx.set_global(settings);
+ init_settings(cx, |settings| {
+ settings.defaults.tab_size = Some(2.try_into().unwrap());
+ });
+
cx.add_model(|cx| {
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx);
@@ -1617,7 +1624,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
#[gpui::test]
fn test_language_config_at(cx: &mut AppContext) {
- cx.set_global(Settings::test(cx));
+ init_settings(cx, |_| {});
+
cx.add_model(|cx| {
let language = Language::new(
LanguageConfig {
@@ -2199,7 +2207,6 @@ fn assert_bracket_pairs(
language: Language,
cx: &mut AppContext,
) {
- cx.set_global(Settings::test(cx));
let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
let buffer = cx.add_model(|cx| {
Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx)
@@ -2222,3 +2229,11 @@ fn assert_bracket_pairs(
bracket_pairs
);
}
+
+fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) {
+ cx.set_global(SettingsStore::test(cx));
+ crate::init(cx);
+ cx.update_global::<SettingsStore, _, _>(|settings, cx| {
+ settings.update_user_settings::<AllLanguageSettings>(cx, f);
+ });
+}
@@ -1,6 +1,7 @@
mod buffer;
mod diagnostic_set;
mod highlight_map;
+pub mod language_settings;
mod outline;
pub mod proto;
mod syntax_map;
@@ -58,6 +59,10 @@ pub use lsp::LanguageServerId;
pub use outline::{Outline, OutlineItem};
pub use tree_sitter::{Parser, Tree};
+pub fn init(cx: &mut AppContext) {
+ language_settings::init(cx);
+}
+
thread_local! {
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
}
@@ -0,0 +1,338 @@
+use anyhow::Result;
+use collections::HashMap;
+use globset::GlobMatcher;
+use gpui::AppContext;
+use schemars::{
+ schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
+ JsonSchema,
+};
+use serde::{Deserialize, Serialize};
+use std::{num::NonZeroU32, path::Path, sync::Arc};
+
+pub fn init(cx: &mut AppContext) {
+ settings::register::<AllLanguageSettings>(cx);
+}
+
+pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
+ settings::get::<AllLanguageSettings>(cx).language(language)
+}
+
+pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings {
+ settings::get::<AllLanguageSettings>(cx)
+}
+
+#[derive(Debug, Clone)]
+pub struct AllLanguageSettings {
+ pub copilot: CopilotSettings,
+ defaults: LanguageSettings,
+ languages: HashMap<Arc<str>, LanguageSettings>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageSettings {
+ pub tab_size: NonZeroU32,
+ pub hard_tabs: bool,
+ pub soft_wrap: SoftWrap,
+ pub preferred_line_length: u32,
+ pub format_on_save: FormatOnSave,
+ pub remove_trailing_whitespace_on_save: bool,
+ pub ensure_final_newline_on_save: bool,
+ pub formatter: Formatter,
+ pub enable_language_server: bool,
+ pub show_copilot_suggestions: bool,
+ pub show_whitespaces: ShowWhitespaceSetting,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct CopilotSettings {
+ pub feature_enabled: bool,
+ pub disabled_globs: Vec<GlobMatcher>,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct AllLanguageSettingsContent {
+ #[serde(default)]
+ pub features: Option<FeaturesContent>,
+ #[serde(default)]
+ pub copilot: Option<CopilotSettingsContent>,
+ #[serde(flatten)]
+ pub defaults: LanguageSettingsContent,
+ #[serde(default, alias = "language_overrides")]
+ pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct LanguageSettingsContent {
+ #[serde(default)]
+ pub tab_size: Option<NonZeroU32>,
+ #[serde(default)]
+ pub hard_tabs: Option<bool>,
+ #[serde(default)]
+ pub soft_wrap: Option<SoftWrap>,
+ #[serde(default)]
+ pub preferred_line_length: Option<u32>,
+ #[serde(default)]
+ pub format_on_save: Option<FormatOnSave>,
+ #[serde(default)]
+ pub remove_trailing_whitespace_on_save: Option<bool>,
+ #[serde(default)]
+ pub ensure_final_newline_on_save: Option<bool>,
+ #[serde(default)]
+ pub formatter: Option<Formatter>,
+ #[serde(default)]
+ pub enable_language_server: Option<bool>,
+ #[serde(default)]
+ pub show_copilot_suggestions: Option<bool>,
+ #[serde(default)]
+ pub show_whitespaces: Option<ShowWhitespaceSetting>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct CopilotSettingsContent {
+ #[serde(default)]
+ pub disabled_globs: Option<Vec<String>>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct FeaturesContent {
+ pub copilot: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SoftWrap {
+ None,
+ EditorWidth,
+ PreferredLineLength,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum FormatOnSave {
+ On,
+ Off,
+ LanguageServer,
+ External {
+ command: Arc<str>,
+ arguments: Arc<[String]>,
+ },
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowWhitespaceSetting {
+ Selection,
+ None,
+ All,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Formatter {
+ LanguageServer,
+ External {
+ command: Arc<str>,
+ arguments: Arc<[String]>,
+ },
+}
+
+impl AllLanguageSettings {
+ pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
+ if let Some(name) = language_name {
+ if let Some(overrides) = self.languages.get(name) {
+ return overrides;
+ }
+ }
+ &self.defaults
+ }
+
+ pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
+ !self
+ .copilot
+ .disabled_globs
+ .iter()
+ .any(|glob| glob.is_match(path))
+ }
+
+ pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
+ if !self.copilot.feature_enabled {
+ return false;
+ }
+
+ if let Some(path) = path {
+ if !self.copilot_enabled_for_path(path) {
+ return false;
+ }
+ }
+
+ self.language(language_name).show_copilot_suggestions
+ }
+}
+
+impl settings::Setting for AllLanguageSettings {
+ const KEY: Option<&'static str> = None;
+
+ type FileContent = AllLanguageSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_settings: &[&Self::FileContent],
+ _: &AppContext,
+ ) -> Result<Self> {
+ // A default is provided for all settings.
+ let mut defaults: LanguageSettings =
+ serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
+
+ let mut languages = HashMap::default();
+ for (language_name, settings) in &default_value.languages {
+ let mut language_settings = defaults.clone();
+ merge_settings(&mut language_settings, &settings);
+ languages.insert(language_name.clone(), language_settings);
+ }
+
+ let mut copilot_enabled = default_value
+ .features
+ .as_ref()
+ .and_then(|f| f.copilot)
+ .ok_or_else(Self::missing_default)?;
+ let mut copilot_globs = default_value
+ .copilot
+ .as_ref()
+ .and_then(|c| c.disabled_globs.as_ref())
+ .ok_or_else(Self::missing_default)?;
+
+ for user_settings in user_settings {
+ if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
+ copilot_enabled = copilot;
+ }
+ if let Some(globs) = user_settings
+ .copilot
+ .as_ref()
+ .and_then(|f| f.disabled_globs.as_ref())
+ {
+ copilot_globs = globs;
+ }
+
+ // A user's global settings override the default global settings and
+ // all default language-specific settings.
+ merge_settings(&mut defaults, &user_settings.defaults);
+ for language_settings in languages.values_mut() {
+ merge_settings(language_settings, &user_settings.defaults);
+ }
+
+ // A user's language-specific settings override default language-specific settings.
+ for (language_name, user_language_settings) in &user_settings.languages {
+ merge_settings(
+ languages
+ .entry(language_name.clone())
+ .or_insert_with(|| defaults.clone()),
+ &user_language_settings,
+ );
+ }
+ }
+
+ Ok(Self {
+ copilot: CopilotSettings {
+ feature_enabled: copilot_enabled,
+ disabled_globs: copilot_globs
+ .iter()
+ .filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
+ .collect(),
+ },
+ defaults,
+ languages,
+ })
+ }
+
+ fn json_schema(
+ generator: &mut schemars::gen::SchemaGenerator,
+ params: &settings::SettingsJsonSchemaParams,
+ _: &AppContext,
+ ) -> schemars::schema::RootSchema {
+ let mut root_schema = generator.root_schema_for::<Self::FileContent>();
+
+ // Create a schema for a 'languages overrides' object, associating editor
+ // settings with specific langauges.
+ assert!(root_schema
+ .definitions
+ .contains_key("LanguageSettingsContent"));
+
+ let languages_object_schema = SchemaObject {
+ instance_type: Some(InstanceType::Object.into()),
+ object: Some(Box::new(ObjectValidation {
+ properties: params
+ .language_names
+ .iter()
+ .map(|name| {
+ (
+ name.clone(),
+ Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
+ )
+ })
+ .collect(),
+ ..Default::default()
+ })),
+ ..Default::default()
+ };
+
+ root_schema
+ .definitions
+ .extend([("Languages".into(), languages_object_schema.into())]);
+
+ root_schema
+ .schema
+ .object
+ .as_mut()
+ .unwrap()
+ .properties
+ .extend([
+ (
+ "languages".to_owned(),
+ Schema::new_ref("#/definitions/Languages".into()),
+ ),
+ // For backward compatibility
+ (
+ "language_overrides".to_owned(),
+ Schema::new_ref("#/definitions/Languages".into()),
+ ),
+ ]);
+
+ root_schema
+ }
+}
+
+fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
+ merge(&mut settings.tab_size, src.tab_size);
+ merge(&mut settings.hard_tabs, src.hard_tabs);
+ merge(&mut settings.soft_wrap, src.soft_wrap);
+ merge(
+ &mut settings.preferred_line_length,
+ src.preferred_line_length,
+ );
+ merge(&mut settings.formatter, src.formatter.clone());
+ merge(&mut settings.format_on_save, src.format_on_save.clone());
+ merge(
+ &mut settings.remove_trailing_whitespace_on_save,
+ src.remove_trailing_whitespace_on_save,
+ );
+ merge(
+ &mut settings.ensure_final_newline_on_save,
+ src.ensure_final_newline_on_save,
+ );
+ merge(
+ &mut settings.enable_language_server,
+ src.enable_language_server,
+ );
+ merge(
+ &mut settings.show_copilot_suggestions,
+ src.show_copilot_suggestions,
+ );
+ merge(&mut settings.show_whitespaces, src.show_whitespaces);
+
+ fn merge<T>(target: &mut T, value: Option<T>) {
+ if let Some(value) = value {
+ *target = value;
+ }
+ }
+}
@@ -1114,6 +1114,8 @@ fn get_injections(
let mut query_cursor = QueryCursorHandle::new();
let mut prev_match = None;
+ // Ensure that a `ParseStep` is created for every combined injection language, even
+ // if there currently no matches for that injection.
combined_injection_ranges.clear();
for pattern in &config.patterns {
if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
@@ -1174,8 +1176,8 @@ fn get_injections(
if let Some(language) = language {
if combined {
combined_injection_ranges
- .get_mut(&language.clone())
- .unwrap()
+ .entry(language.clone())
+ .or_default()
.extend(content_ranges);
} else {
queue.push(ParseStep {
@@ -20,3 +20,6 @@ settings = { path = "../settings" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -4,7 +4,6 @@ use gpui::{
platform::{CursorStyle, MouseButton},
Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
-use settings::Settings;
use std::sync::Arc;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
@@ -55,7 +54,7 @@ impl View for ActiveBufferLanguage {
};
MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
- let theme = &cx.global::<Settings>().theme.workspace.status_bar;
+ let theme = &theme::current(cx).workspace.status_bar;
let style = theme.active_language.style_for(state, false);
Label::new(active_language_text, style.text.clone())
.contained()
@@ -8,7 +8,6 @@ use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContex
use language::{Buffer, LanguageRegistry};
use picker::{Picker, PickerDelegate, PickerEvent};
use project::Project;
-use settings::Settings;
use std::sync::Arc;
use util::ResultExt;
use workspace::Workspace;
@@ -179,8 +178,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
selected: bool,
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
- let settings = cx.global::<Settings>();
- let theme = &settings.theme;
+ let theme = theme::current(cx);
let mat = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected);
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
@@ -24,6 +24,7 @@ serde.workspace = true
anyhow.workspace = true
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
unindent.workspace = true
@@ -13,7 +13,6 @@ use gpui::{
};
use language::{Buffer, LanguageServerId, LanguageServerName};
use project::{Project, WorktreeId};
-use settings::Settings;
use std::{borrow::Cow, sync::Arc};
use theme::{ui, Theme};
use workspace::{
@@ -304,7 +303,7 @@ impl View for LspLogToolbarItemView {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
let project = self.project.read(cx);
let log_view = log_view.read(cx);
@@ -16,7 +16,12 @@ language = { path = "../language" }
picker = { path = "../picker" }
settings = { path = "../settings" }
text = { path = "../text" }
+theme = { path = "../theme" }
workspace = { path = "../workspace" }
+
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -10,7 +10,6 @@ use gpui::{
use language::Outline;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::Settings;
use std::{
cmp::{self, Reverse},
sync::Arc,
@@ -34,7 +33,7 @@ pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Worksp
.buffer()
.read(cx)
.snapshot(cx)
- .outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
+ .outline(Some(theme::current(cx).editor.syntax.as_ref()));
if let Some(outline) = outline {
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| {
@@ -204,9 +203,9 @@ impl PickerDelegate for OutlineViewDelegate {
selected: bool,
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
- let settings = cx.global::<Settings>();
+ let theme = theme::current(cx);
+ let style = theme.picker.item.style_for(mouse_state, selected);
let string_match = &self.matches[ix];
- let style = settings.theme.picker.item.style_for(mouse_state, selected);
let outline_item = &self.outline.items[string_match.candidate_id];
Text::new(outline_item.text.clone(), style.label.text.clone())
@@ -20,6 +20,7 @@ workspace = { path = "../workspace" }
parking_lot.workspace = true
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
serde_json.workspace = true
workspace = { path = "../workspace", features = ["test-support"] }
@@ -57,7 +57,7 @@ impl<D: PickerDelegate> View for Picker<D> {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = (self.theme.lock())(&cx.global::<settings::Settings>().theme);
+ let theme = (self.theme.lock())(theme::current(cx).as_ref());
let query = self.query(cx);
let match_count = self.delegate.match_count();
@@ -42,7 +42,7 @@ anyhow.workspace = true
async-trait.workspace = true
backtrace = "0.3"
futures.workspace = true
-glob.workspace = true
+globset.workspace = true
ignore = "0.4"
lazy_static.workspace = true
log.workspace = true
@@ -50,6 +50,7 @@ parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
regex.workspace = true
+schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
@@ -57,7 +58,7 @@ sha2 = "0.10"
similar = "1.3"
smol.workspace = true
thiserror.workspace = true
-toml = "0.5"
+toml.workspace = true
itertools = "0.10"
[dev-dependencies]
@@ -74,5 +75,6 @@ lsp = { path = "../lsp", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
+git2 = { version = "0.15", default-features = false }
tempdir.workspace = true
unindent.workspace = true
@@ -1,121 +0,0 @@
-use anyhow::{anyhow, Result};
-use std::path::Path;
-
-#[derive(Default)]
-pub struct LspGlobSet {
- patterns: Vec<glob::Pattern>,
-}
-
-impl LspGlobSet {
- pub fn clear(&mut self) {
- self.patterns.clear();
- }
-
- /// Add a pattern to the glob set.
- ///
- /// LSP's glob syntax supports bash-style brace expansion. For example,
- /// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
- /// This is not a part of the standard libc glob syntax, and isn't supported
- /// by the `glob` crate. So we pre-process the glob patterns, producing a
- /// separate glob `Pattern` object for each part of a brace expansion.
- pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
- // Find all of the ranges of `pattern` that contain matched curly braces.
- let mut expansion_ranges = Vec::new();
- let mut expansion_start_ix = None;
- for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
- match c {
- "{" => {
- if expansion_start_ix.is_some() {
- return Err(anyhow!("nested braces in glob patterns aren't supported"));
- }
- expansion_start_ix = Some(ix);
- }
- "}" => {
- if let Some(start_ix) = expansion_start_ix {
- expansion_ranges.push(start_ix..ix + 1);
- }
- expansion_start_ix = None;
- }
- _ => {}
- }
- }
-
- // Starting with a single pattern, process each brace expansion by cloning
- // the pattern once per element of the expansion.
- let mut unexpanded_patterns = vec![];
- let mut expanded_patterns = vec![pattern.to_string()];
-
- for outer_range in expansion_ranges.into_iter().rev() {
- let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
- std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
- for unexpanded_pattern in unexpanded_patterns.drain(..) {
- for part in unexpanded_pattern[inner_range.clone()].split(',') {
- let mut expanded_pattern = unexpanded_pattern.clone();
- expanded_pattern.replace_range(outer_range.clone(), part);
- expanded_patterns.push(expanded_pattern);
- }
- }
- }
-
- // Parse the final glob patterns and add them to the set.
- for pattern in expanded_patterns {
- let pattern = glob::Pattern::new(&pattern)?;
- self.patterns.push(pattern);
- }
-
- Ok(())
- }
-
- pub fn matches(&self, path: &Path) -> bool {
- self.patterns
- .iter()
- .any(|pattern| pattern.matches_path(path))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_glob_set() {
- let mut watch = LspGlobSet::default();
- watch.add_pattern("/a/**/*.rs").unwrap();
- watch.add_pattern("/a/**/Cargo.toml").unwrap();
-
- assert!(watch.matches("/a/b.rs".as_ref()));
- assert!(watch.matches("/a/b/c.rs".as_ref()));
-
- assert!(!watch.matches("/b/c.rs".as_ref()));
- assert!(!watch.matches("/a/b.ts".as_ref()));
- }
-
- #[test]
- fn test_brace_expansion() {
- let mut watch = LspGlobSet::default();
- watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
-
- assert!(watch.matches("/a/one.js".as_ref()));
- assert!(watch.matches("/a/two.ts".as_ref()));
- assert!(watch.matches("/a/three.tsx".as_ref()));
-
- assert!(!watch.matches("/a/one.j".as_ref()));
- assert!(!watch.matches("/a/two.s".as_ref()));
- assert!(!watch.matches("/a/three.t".as_ref()));
- assert!(!watch.matches("/a/four.t".as_ref()));
- assert!(!watch.matches("/a/five.xt".as_ref()));
- }
-
- #[test]
- fn test_multiple_brace_expansion() {
- let mut watch = LspGlobSet::default();
- watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
-
- assert!(watch.matches("/a/one.bic".as_ref()));
- assert!(watch.matches("/a/two.dole".as_ref()));
- assert!(watch.matches("/a/three.deeee".as_ref()));
-
- assert!(!watch.matches("/a/four.bic".as_ref()));
- assert!(!watch.matches("/a/one.be".as_ref()));
- }
-}
@@ -1,6 +1,6 @@
mod ignore;
mod lsp_command;
-mod lsp_glob_set;
+mod project_settings;
pub mod search;
pub mod terminals;
pub mod worktree;
@@ -18,11 +18,13 @@ use futures::{
future::{try_join_all, Shared},
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
+use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
ModelHandle, Task, WeakModelHandle,
};
use language::{
+ language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter},
point_to_lsp,
proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -39,12 +41,12 @@ use lsp::{
DocumentHighlightKind, LanguageServer, LanguageServerId,
};
use lsp_command::*;
-use lsp_glob_set::LspGlobSet;
use postage::watch;
+use project_settings::ProjectSettings;
use rand::prelude::*;
use search::SearchQuery;
use serde::Serialize;
-use settings::{FormatOnSave, Formatter, Settings};
+use settings::SettingsStore;
use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff};
use std::{
@@ -64,9 +66,7 @@ use std::{
},
time::{Duration, Instant, SystemTime},
};
-
use terminals::Terminals;
-
use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
pub use fs::*;
@@ -122,6 +122,8 @@ pub struct Project {
loading_local_worktrees:
HashMap<Arc<Path>, Shared<Task<Result<ModelHandle<Worktree>, Arc<anyhow::Error>>>>>,
opened_buffers: HashMap<u64, OpenBuffer>,
+ local_buffer_ids_by_path: HashMap<ProjectPath, u64>,
+ local_buffer_ids_by_entry_id: HashMap<ProjectEntryId, u64>,
/// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it.
/// Used for re-issuing buffer requests when peers temporarily disconnect
incomplete_remote_buffers: HashMap<u64, Option<ModelHandle<Buffer>>>,
@@ -210,6 +212,7 @@ pub enum Event {
RemoteIdChanged(Option<u64>),
DisconnectedFromHost,
Closed,
+ DeletedEntry(ProjectEntryId),
CollaboratorUpdated {
old_peer_id: proto::PeerId,
new_peer_id: proto::PeerId,
@@ -223,7 +226,7 @@ pub enum LanguageServerState {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
- watched_paths: LspGlobSet,
+ watched_paths: HashMap<WorktreeId, GlobSet>,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
},
}
@@ -386,7 +389,13 @@ impl FormatTrigger {
}
impl Project {
- pub fn init(client: &Arc<Client>) {
+ pub fn init_settings(cx: &mut AppContext) {
+ settings::register::<ProjectSettings>(cx);
+ }
+
+ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
+ Self::init_settings(cx);
+
client.add_model_message_handler(Self::handle_add_collaborator);
client.add_model_message_handler(Self::handle_update_project_collaborator);
client.add_model_message_handler(Self::handle_remove_collaborator);
@@ -449,12 +458,16 @@ impl Project {
incomplete_remote_buffers: Default::default(),
loading_buffers_by_path: Default::default(),
loading_local_worktrees: Default::default(),
+ local_buffer_ids_by_path: Default::default(),
+ local_buffer_ids_by_entry_id: Default::default(),
buffer_snapshots: Default::default(),
join_project_response_message_id: 0,
client_state: None,
opened_buffer: watch::channel(),
client_subscriptions: Vec::new(),
- _subscriptions: vec![cx.observe_global::<Settings, _>(Self::on_settings_changed)],
+ _subscriptions: vec![
+ cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
+ ],
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
active_entry: None,
@@ -517,6 +530,8 @@ impl Project {
shared_buffers: Default::default(),
incomplete_remote_buffers: Default::default(),
loading_local_worktrees: Default::default(),
+ local_buffer_ids_by_path: Default::default(),
+ local_buffer_ids_by_entry_id: Default::default(),
active_entry: None,
collaborators: Default::default(),
join_project_response_message_id: response.message_id,
@@ -595,12 +610,6 @@ impl Project {
root_paths: impl IntoIterator<Item = &Path>,
cx: &mut gpui::TestAppContext,
) -> ModelHandle<Project> {
- if !cx.read(|cx| cx.has_global::<Settings>()) {
- cx.update(|cx| {
- cx.set_global(Settings::test(cx));
- });
- }
-
let mut languages = LanguageRegistry::test();
languages.set_executor(cx.background());
let http_client = util::http::FakeHttpClient::with_404_response();
@@ -622,7 +631,7 @@ impl Project {
}
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
- let settings = cx.global::<Settings>();
+ let settings = all_language_settings(cx);
let mut language_servers_to_start = Vec::new();
for buffer in self.opened_buffers.values() {
@@ -630,7 +639,10 @@ impl Project {
let buffer = buffer.read(cx);
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language())
{
- if settings.enable_language_server(Some(&language.name())) {
+ if settings
+ .language(Some(&language.name()))
+ .enable_language_server
+ {
let worktree = file.worktree.read(cx);
language_servers_to_start.push((
worktree.id(),
@@ -645,7 +657,10 @@ impl Project {
let mut language_servers_to_stop = Vec::new();
for language in self.languages.to_vec() {
for lsp_adapter in language.lsp_adapters() {
- if !settings.enable_language_server(Some(&language.name())) {
+ if !settings
+ .language(Some(&language.name()))
+ .enable_language_server
+ {
let lsp_name = &lsp_adapter.name;
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
if lsp_name == started_lsp_name {
@@ -962,6 +977,9 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Option<Task<Result<()>>> {
let worktree = self.worktree_for_entry(entry_id, cx)?;
+
+ cx.emit(Event::DeletedEntry(entry_id));
+
if self.is_local() {
worktree.update(cx, |worktree, cx| {
worktree.as_local_mut().unwrap().delete_entry(entry_id, cx)
@@ -1628,6 +1646,21 @@ impl Project {
})
.detach();
+ if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
+ if file.is_local {
+ self.local_buffer_ids_by_path.insert(
+ ProjectPath {
+ worktree_id: file.worktree_id(cx),
+ path: file.path.clone(),
+ },
+ remote_id,
+ );
+
+ self.local_buffer_ids_by_entry_id
+ .insert(file.entry_id, remote_id);
+ }
+ }
+
self.detect_language_for_buffer(buffer, cx);
self.register_buffer_with_language_servers(buffer, cx);
self.register_buffer_with_copilot(buffer, cx);
@@ -2101,7 +2134,7 @@ impl Project {
let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx);
- let settings_observation = cx.observe_global::<Settings, _>(move |_, _| {
+ let settings_observation = cx.observe_global::<SettingsStore, _>(move |_, _| {
*settings_changed_tx.borrow_mut() = ();
});
cx.spawn_weak(|this, mut cx| async move {
@@ -2178,10 +2211,7 @@ impl Project {
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
- if !cx
- .global::<Settings>()
- .enable_language_server(Some(&language.name()))
- {
+ if !language_settings(Some(&language.name()), cx).enable_language_server {
return;
}
@@ -2202,7 +2232,9 @@ impl Project {
None => continue,
};
- let lsp = &cx.global::<Settings>().lsp.get(&adapter.name.0);
+ let lsp = settings::get::<ProjectSettings>(cx)
+ .lsp
+ .get(&adapter.name.0);
let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
let mut initialization_options = adapter.initialization_options.clone();
@@ -2838,10 +2870,37 @@ impl Project {
if let Some(LanguageServerState::Running { watched_paths, .. }) =
self.language_servers.get_mut(&language_server_id)
{
- watched_paths.clear();
+ let mut builders = HashMap::default();
for watcher in params.watchers {
- watched_paths.add_pattern(&watcher.glob_pattern).log_err();
+ for worktree in &self.worktrees {
+ if let Some(worktree) = worktree.upgrade(cx) {
+ let worktree = worktree.read(cx);
+ if let Some(abs_path) = worktree.abs_path().to_str() {
+ if let Some(suffix) = watcher
+ .glob_pattern
+ .strip_prefix(abs_path)
+ .and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
+ {
+ if let Some(glob) = Glob::new(suffix).log_err() {
+ builders
+ .entry(worktree.id())
+ .or_insert_with(|| GlobSetBuilder::new())
+ .add(glob);
+ }
+ break;
+ }
+ }
+ }
+ }
}
+
+ watched_paths.clear();
+ for (worktree_id, builder) in builders {
+ if let Ok(globset) = builder.build() {
+ watched_paths.insert(worktree_id, globset);
+ }
+ }
+
cx.notify();
}
}
@@ -3228,24 +3287,17 @@ impl Project {
let mut project_transaction = ProjectTransaction::default();
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
- let (
- format_on_save,
- remove_trailing_whitespace,
- ensure_final_newline,
- formatter,
- tab_size,
- ) = buffer.read_with(&cx, |buffer, cx| {
- let settings = cx.global::<Settings>();
+ let settings = buffer.read_with(&cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name());
- (
- settings.format_on_save(language_name.as_deref()),
- settings.remove_trailing_whitespace_on_save(language_name.as_deref()),
- settings.ensure_final_newline_on_save(language_name.as_deref()),
- settings.formatter(language_name.as_deref()),
- settings.tab_size(language_name.as_deref()),
- )
+ language_settings(language_name.as_deref(), cx).clone()
});
+ let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
+ let ensure_final_newline = settings.ensure_final_newline_on_save;
+ let format_on_save = settings.format_on_save.clone();
+ let formatter = settings.formatter.clone();
+ let tab_size = settings.tab_size;
+
// First, format buffer's whitespace according to the settings.
let trailing_whitespace_diff = if remove_trailing_whitespace {
Some(
@@ -4536,7 +4588,7 @@ impl Project {
if worktree.read(cx).is_local() {
cx.subscribe(worktree, |this, worktree, event, cx| match event {
worktree::Event::UpdatedEntries(changes) => {
- this.update_local_worktree_buffers(&worktree, cx);
+ this.update_local_worktree_buffers(&worktree, &changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
}
worktree::Event::UpdatedGitRepositories(updated_repos) => {
@@ -4570,80 +4622,106 @@ impl Project {
fn update_local_worktree_buffers(
&mut self,
worktree_handle: &ModelHandle<Worktree>,
+ changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
cx: &mut ModelContext<Self>,
) {
let snapshot = worktree_handle.read(cx).snapshot();
- let mut buffers_to_delete = Vec::new();
let mut renamed_buffers = Vec::new();
+ for (path, entry_id) in changes.keys() {
+ let worktree_id = worktree_handle.read(cx).id();
+ let project_path = ProjectPath {
+ worktree_id,
+ path: path.clone(),
+ };
- for (buffer_id, buffer) in &self.opened_buffers {
- if let Some(buffer) = buffer.upgrade(cx) {
- buffer.update(cx, |buffer, cx| {
- if let Some(old_file) = File::from_dyn(buffer.file()) {
- if old_file.worktree != *worktree_handle {
- return;
- }
+ let buffer_id = match self.local_buffer_ids_by_entry_id.get(entry_id) {
+ Some(&buffer_id) => buffer_id,
+ None => match self.local_buffer_ids_by_path.get(&project_path) {
+ Some(&buffer_id) => buffer_id,
+ None => continue,
+ },
+ };
- let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id)
- {
- File {
- is_local: true,
- entry_id: entry.id,
- mtime: entry.mtime,
- path: entry.path.clone(),
- worktree: worktree_handle.clone(),
- is_deleted: false,
- }
- } else if let Some(entry) =
- snapshot.entry_for_path(old_file.path().as_ref())
- {
- File {
- is_local: true,
- entry_id: entry.id,
- mtime: entry.mtime,
- path: entry.path.clone(),
- worktree: worktree_handle.clone(),
- is_deleted: false,
- }
- } else {
- File {
- is_local: true,
- entry_id: old_file.entry_id,
- path: old_file.path().clone(),
- mtime: old_file.mtime(),
- worktree: worktree_handle.clone(),
- is_deleted: true,
- }
- };
+ let open_buffer = self.opened_buffers.get(&buffer_id);
+ let buffer = if let Some(buffer) = open_buffer.and_then(|buffer| buffer.upgrade(cx)) {
+ buffer
+ } else {
+ self.opened_buffers.remove(&buffer_id);
+ self.local_buffer_ids_by_path.remove(&project_path);
+ self.local_buffer_ids_by_entry_id.remove(entry_id);
+ continue;
+ };
- let old_path = old_file.abs_path(cx);
- if new_file.abs_path(cx) != old_path {
- renamed_buffers.push((cx.handle(), old_file.clone()));
+ buffer.update(cx, |buffer, cx| {
+ if let Some(old_file) = File::from_dyn(buffer.file()) {
+ if old_file.worktree != *worktree_handle {
+ return;
+ }
+
+ let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
+ File {
+ is_local: true,
+ entry_id: entry.id,
+ mtime: entry.mtime,
+ path: entry.path.clone(),
+ worktree: worktree_handle.clone(),
+ is_deleted: false,
+ }
+ } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
+ File {
+ is_local: true,
+ entry_id: entry.id,
+ mtime: entry.mtime,
+ path: entry.path.clone(),
+ worktree: worktree_handle.clone(),
+ is_deleted: false,
}
+ } else {
+ File {
+ is_local: true,
+ entry_id: old_file.entry_id,
+ path: old_file.path().clone(),
+ mtime: old_file.mtime(),
+ worktree: worktree_handle.clone(),
+ is_deleted: true,
+ }
+ };
- if new_file != *old_file {
- if let Some(project_id) = self.remote_id() {
- self.client
- .send(proto::UpdateBufferFile {
- project_id,
- buffer_id: *buffer_id as u64,
- file: Some(new_file.to_proto()),
- })
- .log_err();
- }
+ let old_path = old_file.abs_path(cx);
+ if new_file.abs_path(cx) != old_path {
+ renamed_buffers.push((cx.handle(), old_file.clone()));
+ self.local_buffer_ids_by_path.remove(&project_path);
+ self.local_buffer_ids_by_path.insert(
+ ProjectPath {
+ worktree_id,
+ path: path.clone(),
+ },
+ buffer_id,
+ );
+ }
- buffer.file_updated(Arc::new(new_file), cx).detach();
- }
+ if new_file.entry_id != *entry_id {
+ self.local_buffer_ids_by_entry_id.remove(entry_id);
+ self.local_buffer_ids_by_entry_id
+ .insert(new_file.entry_id, buffer_id);
}
- });
- } else {
- buffers_to_delete.push(*buffer_id);
- }
- }
- for buffer_id in buffers_to_delete {
- self.opened_buffers.remove(&buffer_id);
+ if new_file != *old_file {
+ if let Some(project_id) = self.remote_id() {
+ self.client
+ .send(proto::UpdateBufferFile {
+ project_id,
+ buffer_id: buffer_id as u64,
+ file: Some(new_file.to_proto()),
+ })
+ .log_err();
+ }
+
+ buffer.file_updated(Arc::new(new_file), cx).detach();
+ }
+ }
+ });
}
for (buffer, old_file) in renamed_buffers {
@@ -4656,28 +4734,42 @@ impl Project {
fn update_local_worktree_language_servers(
&mut self,
worktree_handle: &ModelHandle<Worktree>,
- changes: &HashMap<Arc<Path>, PathChange>,
+ changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
cx: &mut ModelContext<Self>,
) {
+ if changes.is_empty() {
+ return;
+ }
+
let worktree_id = worktree_handle.read(cx).id();
+ let mut language_server_ids = self
+ .language_server_ids
+ .iter()
+ .filter_map(|((server_worktree_id, _), server_id)| {
+ (*server_worktree_id == worktree_id).then_some(*server_id)
+ })
+ .collect::<Vec<_>>();
+ language_server_ids.sort();
+ language_server_ids.dedup();
+
let abs_path = worktree_handle.read(cx).abs_path();
- for ((server_worktree_id, _), server_id) in &self.language_server_ids {
- if *server_worktree_id == worktree_id {
- if let Some(server) = self.language_servers.get(server_id) {
- if let LanguageServerState::Running {
- server,
- watched_paths,
- ..
- } = server
- {
+ for server_id in &language_server_ids {
+ if let Some(server) = self.language_servers.get(server_id) {
+ if let LanguageServerState::Running {
+ server,
+ watched_paths,
+ ..
+ } = server
+ {
+ if let Some(watched_paths) = watched_paths.get(&worktree_id) {
let params = lsp::DidChangeWatchedFilesParams {
changes: changes
.iter()
- .filter_map(|(path, change)| {
- let path = abs_path.join(path);
- if watched_paths.matches(&path) {
+ .filter_map(|((path, _), change)| {
+ if watched_paths.is_match(&path) {
Some(lsp::FileEvent {
- uri: lsp::Url::from_file_path(path).unwrap(),
+ uri: lsp::Url::from_file_path(abs_path.join(path))
+ .unwrap(),
typ: match change {
PathChange::Added => lsp::FileChangeType::CREATED,
PathChange::Removed => lsp::FileChangeType::DELETED,
@@ -5098,6 +5190,9 @@ impl Project {
mut cx: AsyncAppContext,
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+
+ this.update(&mut cx, |_, cx| cx.emit(Event::DeletedEntry(entry_id)));
+
let worktree = this.read_with(&cx, |this, cx| {
this.worktree_for_entry(entry_id, cx)
.ok_or_else(|| anyhow!("worktree not found"))
@@ -0,0 +1,31 @@
+use collections::HashMap;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+use std::sync::Arc;
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ProjectSettings {
+ #[serde(default)]
+ pub lsp: HashMap<Arc<str>, LspSettings>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct LspSettings {
+ pub initialization_options: Option<serde_json::Value>,
+}
+
+impl Setting for ProjectSettings {
+ const KEY: Option<&'static str> = None;
+
+ type FileContent = Self;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &gpui::AppContext,
+ ) -> anyhow::Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
@@ -1,10 +1,10 @@
use crate::{worktree::WorktreeHandle, Event, *};
-use fs::LineEnding;
-use fs::{FakeFs, RealFs};
+use fs::{FakeFs, LineEnding, RealFs};
use futures::{future, StreamExt};
-use gpui::AppContext;
-use gpui::{executor::Deterministic, test::subscribe};
+use globset::Glob;
+use gpui::{executor::Deterministic, test::subscribe, AppContext};
use language::{
+ language_settings::{AllLanguageSettings, LanguageSettingsContent},
tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
OffsetRangeExt, Point, ToPoint,
};
@@ -26,6 +26,9 @@ fn init_logger() {
#[gpui::test]
async fn test_symlinks(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ cx.foreground().allow_parking();
+
let dir = temp_tree(json!({
"root": {
"apple": "",
@@ -65,7 +68,7 @@ async fn test_managing_language_servers(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let mut rust_language = Language::new(
LanguageConfig {
@@ -451,7 +454,7 @@ async fn test_managing_language_servers(
#[gpui::test]
async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let mut language = Language::new(
LanguageConfig {
@@ -503,7 +506,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
register_options: serde_json::to_value(
lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![lsp::FileSystemWatcher {
- glob_pattern: "*.{rs,c}".to_string(),
+ glob_pattern: "/the-root/*.{rs,c}".to_string(),
kind: None,
}],
},
@@ -556,7 +559,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
#[gpui::test]
async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -648,7 +651,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -719,7 +722,7 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let progress_token = "the-progress-token";
let mut language = Language::new(
@@ -847,7 +850,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let progress_token = "the-progress-token";
let mut language = Language::new(
@@ -925,7 +928,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
#[gpui::test]
async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let mut language = Language::new(
LanguageConfig {
@@ -973,11 +976,8 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
}
#[gpui::test]
-async fn test_toggling_enable_language_server(
- deterministic: Arc<Deterministic>,
- cx: &mut gpui::TestAppContext,
-) {
- deterministic.forbid_parking();
+async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
let mut rust = Language::new(
LanguageConfig {
@@ -1051,14 +1051,16 @@ async fn test_toggling_enable_language_server(
// Disable Rust language server, ensuring only that server gets stopped.
cx.update(|cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.language_overrides.insert(
- Arc::from("Rust"),
- settings::EditorSettings {
- enable_language_server: Some(false),
- ..Default::default()
- },
- );
+ cx.update_global(|settings: &mut SettingsStore, cx| {
+ settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.languages.insert(
+ Arc::from("Rust"),
+ LanguageSettingsContent {
+ enable_language_server: Some(false),
+ ..Default::default()
+ },
+ );
+ });
})
});
fake_rust_server_1
@@ -1068,21 +1070,23 @@ async fn test_toggling_enable_language_server(
// Enable Rust and disable JavaScript language servers, ensuring that the
// former gets started again and that the latter stops.
cx.update(|cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.language_overrides.insert(
- Arc::from("Rust"),
- settings::EditorSettings {
- enable_language_server: Some(true),
- ..Default::default()
- },
- );
- settings.language_overrides.insert(
- Arc::from("JavaScript"),
- settings::EditorSettings {
- enable_language_server: Some(false),
- ..Default::default()
- },
- );
+ cx.update_global(|settings: &mut SettingsStore, cx| {
+ settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.languages.insert(
+ Arc::from("Rust"),
+ LanguageSettingsContent {
+ enable_language_server: Some(true),
+ ..Default::default()
+ },
+ );
+ settings.languages.insert(
+ Arc::from("JavaScript"),
+ LanguageSettingsContent {
+ enable_language_server: Some(false),
+ ..Default::default()
+ },
+ );
+ });
})
});
let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
@@ -1102,7 +1106,7 @@ async fn test_toggling_enable_language_server(
#[gpui::test]
async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let mut language = Language::new(
LanguageConfig {
@@ -1388,7 +1392,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let text = concat!(
"let one = ;\n", //
@@ -1457,9 +1461,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
- println!("hello from stdout");
- eprintln!("hello from stderr");
- cx.foreground().forbid_parking();
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
@@ -1515,7 +1517,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
#[gpui::test]
async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let mut language = Language::new(
LanguageConfig {
@@ -1673,7 +1675,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let text = "
use a::b;
@@ -1781,7 +1783,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp
#[gpui::test]
async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let text = "
use a::b;
@@ -1902,6 +1904,8 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
#[gpui::test(iterations = 10)]
async fn test_definition(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
@@ -2001,6 +2005,8 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
@@ -2085,6 +2091,8 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
@@ -2138,6 +2146,8 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
@@ -2254,6 +2264,8 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_save_file(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
@@ -2284,6 +2296,8 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
@@ -2313,6 +2327,8 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_save_as(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({})).await;
@@ -2373,6 +2389,9 @@ async fn test_rescan_and_remote_updates(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
+ init_test(cx);
+ cx.foreground().allow_parking();
+
let dir = temp_tree(json!({
"a": {
"file1": "",
@@ -2529,6 +2548,8 @@ async fn test_buffer_identity_across_renames(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
@@ -2577,6 +2598,8 @@ async fn test_buffer_identity_across_renames(
#[gpui::test]
async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
@@ -2621,6 +2644,8 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
@@ -2765,6 +2790,8 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let initial_contents = "aaa\nbbbbb\nc\n";
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -2844,6 +2871,8 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
@@ -2904,7 +2933,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -3146,7 +3175,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_rename(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
+ init_test(cx);
let mut language = Language::new(
LanguageConfig {
@@ -3284,6 +3313,8 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_search(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
@@ -3339,6 +3370,8 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let search_query = "file";
let fs = FakeFs::new(cx.background());
@@ -3361,7 +3394,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
- vec![glob::Pattern::new("*.odd").unwrap()],
+ vec![Glob::new("*.odd").unwrap().compile_matcher()],
Vec::new()
),
cx
@@ -3379,7 +3412,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
- vec![glob::Pattern::new("*.rs").unwrap()],
+ vec![Glob::new("*.rs").unwrap().compile_matcher()],
Vec::new()
),
cx
@@ -3401,8 +3434,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false,
true,
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap(),
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher(),
],
Vec::new()
),
@@ -3425,9 +3458,9 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false,
true,
vec![
- glob::Pattern::new("*.rs").unwrap(),
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap(),
+ Glob::new("*.rs").unwrap().compile_matcher(),
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher(),
],
Vec::new()
),
@@ -3447,6 +3480,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let search_query = "file";
let fs = FakeFs::new(cx.background());
@@ -3470,7 +3505,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
false,
true,
Vec::new(),
- vec![glob::Pattern::new("*.odd").unwrap()],
+ vec![Glob::new("*.odd").unwrap().compile_matcher()],
),
cx
)
@@ -3493,7 +3528,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
false,
true,
Vec::new(),
- vec![glob::Pattern::new("*.rs").unwrap()],
+ vec![Glob::new("*.rs").unwrap().compile_matcher()],
),
cx
)
@@ -3515,8 +3550,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true,
Vec::new(),
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap(),
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher(),
],
),
cx
@@ -3539,9 +3574,9 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true,
Vec::new(),
vec![
- glob::Pattern::new("*.rs").unwrap(),
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap(),
+ Glob::new("*.rs").unwrap().compile_matcher(),
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher(),
],
),
cx
@@ -3554,6 +3589,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let search_query = "file";
let fs = FakeFs::new(cx.background());
@@ -3576,8 +3613,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
- vec![glob::Pattern::new("*.odd").unwrap()],
- vec![glob::Pattern::new("*.odd").unwrap()],
+ vec![Glob::new("*.odd").unwrap().compile_matcher()],
+ vec![Glob::new("*.odd").unwrap().compile_matcher()],
),
cx
)
@@ -3594,8 +3631,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
- vec![glob::Pattern::new("*.ts").unwrap()],
- vec![glob::Pattern::new("*.ts").unwrap()],
+ vec![Glob::new("*.ts").unwrap().compile_matcher()],
+ vec![Glob::new("*.ts").unwrap().compile_matcher()],
),
cx
)
@@ -3613,12 +3650,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false,
true,
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap()
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher()
],
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap()
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher()
],
),
cx
@@ -3637,12 +3674,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false,
true,
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap()
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher()
],
vec![
- glob::Pattern::new("*.rs").unwrap(),
- glob::Pattern::new("*.odd").unwrap()
+ Glob::new("*.rs").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher()
],
),
cx
@@ -3680,3 +3717,13 @@ async fn search(
})
.collect())
}
+
+fn init_test(cx: &mut gpui::TestAppContext) {
+ cx.foreground().forbid_parking();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ language::init(cx);
+ Project::init_settings(cx);
+ });
+}
@@ -1,6 +1,7 @@
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
use anyhow::Result;
use client::proto;
+use globset::{Glob, GlobMatcher};
use itertools::Itertools;
use language::{char_kind, Rope};
use regex::{Regex, RegexBuilder};
@@ -19,8 +20,8 @@ pub enum SearchQuery {
query: Arc<str>,
whole_word: bool,
case_sensitive: bool,
- files_to_include: Vec<glob::Pattern>,
- files_to_exclude: Vec<glob::Pattern>,
+ files_to_include: Vec<GlobMatcher>,
+ files_to_exclude: Vec<GlobMatcher>,
},
Regex {
regex: Regex,
@@ -28,8 +29,8 @@ pub enum SearchQuery {
multiline: bool,
whole_word: bool,
case_sensitive: bool,
- files_to_include: Vec<glob::Pattern>,
- files_to_exclude: Vec<glob::Pattern>,
+ files_to_include: Vec<GlobMatcher>,
+ files_to_exclude: Vec<GlobMatcher>,
},
}
@@ -38,8 +39,8 @@ impl SearchQuery {
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
- files_to_include: Vec<glob::Pattern>,
- files_to_exclude: Vec<glob::Pattern>,
+ files_to_include: Vec<GlobMatcher>,
+ files_to_exclude: Vec<GlobMatcher>,
) -> Self {
let query = query.to_string();
let search = AhoCorasickBuilder::new()
@@ -60,8 +61,8 @@ impl SearchQuery {
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
- files_to_include: Vec<glob::Pattern>,
- files_to_exclude: Vec<glob::Pattern>,
+ files_to_include: Vec<GlobMatcher>,
+ files_to_exclude: Vec<GlobMatcher>,
) -> Result<Self> {
let mut query = query.to_string();
let initial_query = Arc::from(query.as_str());
@@ -95,40 +96,16 @@ impl SearchQuery {
message.query,
message.whole_word,
message.case_sensitive,
- message
- .files_to_include
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()?,
- message
- .files_to_exclude
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()?,
+ deserialize_globs(&message.files_to_include)?,
+ deserialize_globs(&message.files_to_exclude)?,
)
} else {
Ok(Self::text(
message.query,
message.whole_word,
message.case_sensitive,
- message
- .files_to_include
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()?,
- message
- .files_to_exclude
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()?,
+ deserialize_globs(&message.files_to_include)?,
+ deserialize_globs(&message.files_to_exclude)?,
))
}
}
@@ -143,12 +120,12 @@ impl SearchQuery {
files_to_include: self
.files_to_include()
.iter()
- .map(ToString::to_string)
+ .map(|g| g.glob().to_string())
.join(","),
files_to_exclude: self
.files_to_exclude()
.iter()
- .map(ToString::to_string)
+ .map(|g| g.glob().to_string())
.join(","),
}
}
@@ -289,7 +266,7 @@ impl SearchQuery {
matches!(self, Self::Regex { .. })
}
- pub fn files_to_include(&self) -> &[glob::Pattern] {
+ pub fn files_to_include(&self) -> &[GlobMatcher] {
match self {
Self::Text {
files_to_include, ..
@@ -300,7 +277,7 @@ impl SearchQuery {
}
}
- pub fn files_to_exclude(&self) -> &[glob::Pattern] {
+ pub fn files_to_exclude(&self) -> &[GlobMatcher] {
match self {
Self::Text {
files_to_exclude, ..
@@ -317,14 +294,23 @@ impl SearchQuery {
!self
.files_to_exclude()
.iter()
- .any(|exclude_glob| exclude_glob.matches_path(file_path))
+ .any(|exclude_glob| exclude_glob.is_match(file_path))
&& (self.files_to_include().is_empty()
|| self
.files_to_include()
.iter()
- .any(|include_glob| include_glob.matches_path(file_path)))
+ .any(|include_glob| include_glob.is_match(file_path)))
}
None => self.files_to_include().is_empty(),
}
}
}
+
+fn deserialize_globs(glob_set: &str) -> Result<Vec<GlobMatcher>> {
+ glob_set
+ .split(',')
+ .map(str::trim)
+ .filter(|glob_str| !glob_str.is_empty())
+ .map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher()))
+ .collect()
+}
@@ -1,10 +1,7 @@
-use std::path::PathBuf;
-
-use gpui::{ModelContext, ModelHandle, WeakModelHandle};
-use settings::Settings;
-use terminal::{Terminal, TerminalBuilder};
-
use crate::Project;
+use gpui::{ModelContext, ModelHandle, WeakModelHandle};
+use std::path::PathBuf;
+use terminal::{Terminal, TerminalBuilder, TerminalSettings};
pub struct Terminals {
pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
@@ -22,17 +19,14 @@ impl Project {
"creating terminals as a guest is not supported yet"
));
} else {
- let settings = cx.global::<Settings>();
- let shell = settings.terminal_shell();
- let envs = settings.terminal_env();
- let scroll = settings.terminal_scroll();
+ let settings = settings::get::<TerminalSettings>(cx);
let terminal = TerminalBuilder::new(
working_directory.clone(),
- shell,
- envs,
- settings.terminal_overrides.blinking.clone(),
- scroll,
+ settings.shell.clone(),
+ settings.env.clone(),
+ Some(settings.blinking.clone()),
+ settings.alternate_scroll,
window_id,
)
.map(|builder| {
@@ -6,7 +6,10 @@ use anyhow::{anyhow, Context, Result};
use client::{proto, Client};
use clock::ReplicaId;
use collections::{HashMap, VecDeque};
-use fs::{repository::GitRepository, Fs, LineEnding};
+use fs::{
+ repository::{GitFileStatus, GitRepository, RepoPath, RepoPathDescendants},
+ Fs, LineEnding,
+};
use futures::{
channel::{
mpsc::{self, UnboundedSender},
@@ -52,7 +55,7 @@ use std::{
time::{Duration, SystemTime},
};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
-use util::{paths::HOME, ResultExt, TryFutureExt};
+use util::{paths::HOME, ResultExt, TakeUntilExt, TryFutureExt};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);
@@ -117,10 +120,19 @@ pub struct Snapshot {
completed_scan_id: usize,
}
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepositoryEntry {
pub(crate) work_directory: WorkDirectoryEntry,
pub(crate) branch: Option<Arc<str>>,
+ pub(crate) statuses: TreeMap<RepoPath, GitFileStatus>,
+}
+
+fn read_git_status(git_status: i32) -> Option<GitFileStatus> {
+ proto::GitStatus::from_i32(git_status).map(|status| match status {
+ proto::GitStatus::Added => GitFileStatus::Added,
+ proto::GitStatus::Modified => GitFileStatus::Modified,
+ proto::GitStatus::Conflict => GitFileStatus::Conflict,
+ })
}
impl RepositoryEntry {
@@ -138,8 +150,101 @@ impl RepositoryEntry {
.map(|entry| RepositoryWorkDirectory(entry.path.clone()))
}
- pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool {
- self.work_directory.contains(snapshot, path)
+ pub fn status_for_path(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
+ self.work_directory
+ .relativize(snapshot, path)
+ .and_then(|repo_path| {
+ self.statuses
+ .iter_from(&repo_path)
+ .take_while(|(key, _)| key.starts_with(&repo_path))
+ // Short circut once we've found the highest level
+ .take_until(|(_, status)| status == &&GitFileStatus::Conflict)
+ .map(|(_, status)| status)
+ .reduce(
+ |status_first, status_second| match (status_first, status_second) {
+ (GitFileStatus::Conflict, _) | (_, GitFileStatus::Conflict) => {
+ &GitFileStatus::Conflict
+ }
+ (GitFileStatus::Modified, _) | (_, GitFileStatus::Modified) => {
+ &GitFileStatus::Modified
+ }
+ _ => &GitFileStatus::Added,
+ },
+ )
+ .copied()
+ })
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
+ self.work_directory
+ .relativize(snapshot, path)
+ .and_then(|repo_path| (&self.statuses).get(&repo_path))
+ .cloned()
+ }
+
+ pub fn build_update(&self, other: &Self) -> proto::RepositoryEntry {
+ let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
+ let mut removed_statuses: Vec<String> = Vec::new();
+
+ let mut self_statuses = self.statuses.iter().peekable();
+ let mut other_statuses = other.statuses.iter().peekable();
+ loop {
+ match (self_statuses.peek(), other_statuses.peek()) {
+ (Some((self_repo_path, self_status)), Some((other_repo_path, other_status))) => {
+ match Ord::cmp(self_repo_path, other_repo_path) {
+ Ordering::Less => {
+ updated_statuses.push(make_status_entry(self_repo_path, self_status));
+ self_statuses.next();
+ }
+ Ordering::Equal => {
+ if self_status != other_status {
+ updated_statuses
+ .push(make_status_entry(self_repo_path, self_status));
+ }
+
+ self_statuses.next();
+ other_statuses.next();
+ }
+ Ordering::Greater => {
+ removed_statuses.push(make_repo_path(other_repo_path));
+ other_statuses.next();
+ }
+ }
+ }
+ (Some((self_repo_path, self_status)), None) => {
+ updated_statuses.push(make_status_entry(self_repo_path, self_status));
+ self_statuses.next();
+ }
+ (None, Some((other_repo_path, _))) => {
+ removed_statuses.push(make_repo_path(other_repo_path));
+ other_statuses.next();
+ }
+ (None, None) => break,
+ }
+ }
+
+ proto::RepositoryEntry {
+ work_directory_id: self.work_directory_id().to_proto(),
+ branch: self.branch.as_ref().map(|str| str.to_string()),
+ removed_repo_paths: removed_statuses,
+ updated_statuses,
+ }
+ }
+}
+
+fn make_repo_path(path: &RepoPath) -> String {
+ path.as_os_str().to_string_lossy().to_string()
+}
+
+fn make_status_entry(path: &RepoPath, status: &GitFileStatus) -> proto::StatusEntry {
+ proto::StatusEntry {
+ repo_path: make_repo_path(path),
+ status: match status {
+ GitFileStatus::Added => proto::GitStatus::Added.into(),
+ GitFileStatus::Modified => proto::GitStatus::Modified.into(),
+ GitFileStatus::Conflict => proto::GitStatus::Conflict.into(),
+ },
}
}
@@ -148,6 +253,12 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry {
proto::RepositoryEntry {
work_directory_id: value.work_directory.to_proto(),
branch: value.branch.as_ref().map(|str| str.to_string()),
+ updated_statuses: value
+ .statuses
+ .iter()
+ .map(|(repo_path, status)| make_status_entry(repo_path, status))
+ .collect(),
+ removed_repo_paths: Default::default(),
}
}
}
@@ -162,23 +273,21 @@ impl Default for RepositoryWorkDirectory {
}
}
+impl AsRef<Path> for RepositoryWorkDirectory {
+ fn as_ref(&self) -> &Path {
+ self.0.as_ref()
+ }
+}
+
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct WorkDirectoryEntry(ProjectEntryId);
impl WorkDirectoryEntry {
- // Note that these paths should be relative to the worktree root.
- pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool {
- snapshot
- .entry_for_id(self.0)
- .map(|entry| path.starts_with(&entry.path))
- .unwrap_or(false)
- }
-
pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option<RepoPath> {
worktree.entry_for_id(self.0).and_then(|entry| {
path.strip_prefix(&entry.path)
.ok()
- .map(move |path| RepoPath(path.to_owned()))
+ .map(move |path| path.into())
})
}
}
@@ -197,43 +306,30 @@ impl<'a> From<ProjectEntryId> for WorkDirectoryEntry {
}
}
-#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
-pub struct RepoPath(PathBuf);
-
-impl AsRef<Path> for RepoPath {
- fn as_ref(&self) -> &Path {
- self.0.as_ref()
- }
-}
-
-impl Deref for RepoPath {
- type Target = PathBuf;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl AsRef<Path> for RepositoryWorkDirectory {
- fn as_ref(&self) -> &Path {
- self.0.as_ref()
- }
-}
-
#[derive(Debug, Clone)]
pub struct LocalSnapshot {
- ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
- // The ProjectEntryId corresponds to the entry for the .git dir
- // work_directory_id
+ snapshot: Snapshot,
+ /// All of the gitignore files in the worktree, indexed by their relative path.
+ /// The boolean indicates whether the gitignore needs to be updated.
+ ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
+ /// All of the git repositories in the worktree, indexed by the project entry
+ /// id of their parent directory.
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
+}
+
+pub struct LocalMutableSnapshot {
+ snapshot: LocalSnapshot,
+ /// The ids of all of the entries that were removed from the snapshot
+ /// as part of the current update. These entry ids may be re-used
+ /// if the same inode is discovered at a new path, or if the given
+ /// path is re-created after being deleted.
removed_entry_ids: HashMap<u64, ProjectEntryId>,
- next_entry_id: Arc<AtomicUsize>,
- snapshot: Snapshot,
}
#[derive(Debug, Clone)]
pub struct LocalRepositoryEntry {
pub(crate) scan_id: usize,
+ pub(crate) full_scan_id: usize,
pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
/// Path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file
@@ -261,11 +357,25 @@ impl DerefMut for LocalSnapshot {
}
}
+impl Deref for LocalMutableSnapshot {
+ type Target = LocalSnapshot;
+
+ fn deref(&self) -> &Self::Target {
+ &self.snapshot
+ }
+}
+
+impl DerefMut for LocalMutableSnapshot {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.snapshot
+ }
+}
+
enum ScanState {
Started,
Updated {
snapshot: LocalSnapshot,
- changes: HashMap<Arc<Path>, PathChange>,
+ changes: HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
barrier: Option<barrier::Sender>,
scanning: bool,
},
@@ -279,7 +389,7 @@ struct ShareState {
}
pub enum Event {
- UpdatedEntries(HashMap<Arc<Path>, PathChange>),
+ UpdatedEntries(HashMap<(Arc<Path>, ProjectEntryId), PathChange>),
UpdatedGitRepositories(HashMap<Arc<Path>, LocalRepositoryEntry>),
}
@@ -311,9 +421,7 @@ impl Worktree {
let mut snapshot = LocalSnapshot {
ignores_by_parent_abs_path: Default::default(),
- removed_entry_ids: Default::default(),
git_repositories: Default::default(),
- next_entry_id,
snapshot: Snapshot {
id: WorktreeId::from_usize(cx.model_id()),
abs_path: abs_path.clone(),
@@ -332,7 +440,7 @@ impl Worktree {
Entry::new(
Arc::from(Path::new("")),
&metadata,
- &snapshot.next_entry_id,
+ &next_entry_id,
snapshot.root_char_bag,
),
fs.as_ref(),
@@ -376,6 +484,7 @@ impl Worktree {
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
BackgroundScanner::new(
snapshot,
+ next_entry_id,
fs,
scan_states_tx,
background,
@@ -796,7 +905,7 @@ impl LocalWorktree {
let mut index_task = None;
- if let Some(repo) = snapshot.repo_for(&path) {
+ if let Some(repo) = snapshot.repository_for_path(&path) {
let repo_path = repo.work_directory.relativize(self, &path).unwrap();
if let Some(repo) = self.git_repositories.get(&*repo.work_directory) {
let repo = repo.repo_ptr.to_owned();
@@ -1123,8 +1232,6 @@ impl LocalWorktree {
let mut share_tx = Some(share_tx);
let mut prev_snapshot = LocalSnapshot {
ignores_by_parent_abs_path: Default::default(),
- removed_entry_ids: Default::default(),
- next_entry_id: Default::default(),
git_repositories: Default::default(),
snapshot: Snapshot {
id: WorktreeId(worktree_id as usize),
@@ -1424,13 +1531,41 @@ impl Snapshot {
});
for repository in update.updated_repositories {
- let repository = RepositoryEntry {
- work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(),
- branch: repository.branch.map(Into::into),
- };
- if let Some(entry) = self.entry_for_id(repository.work_directory_id()) {
- self.repository_entries
- .insert(RepositoryWorkDirectory(entry.path.clone()), repository)
+ let work_directory_entry: WorkDirectoryEntry =
+ ProjectEntryId::from_proto(repository.work_directory_id).into();
+
+ if let Some(entry) = self.entry_for_id(*work_directory_entry) {
+ let mut statuses = TreeMap::default();
+ for status_entry in repository.updated_statuses {
+ let Some(git_file_status) = read_git_status(status_entry.status) else {
+ continue;
+ };
+
+ let repo_path = RepoPath::new(status_entry.repo_path.into());
+ statuses.insert(repo_path, git_file_status);
+ }
+
+ let work_directory = RepositoryWorkDirectory(entry.path.clone());
+ if self.repository_entries.get(&work_directory).is_some() {
+ self.repository_entries.update(&work_directory, |repo| {
+ repo.branch = repository.branch.map(Into::into);
+ repo.statuses.insert_tree(statuses);
+
+ for repo_path in repository.removed_repo_paths {
+ let repo_path = RepoPath::new(repo_path.into());
+ repo.statuses.remove(&repo_path);
+ }
+ });
+ } else {
+ self.repository_entries.insert(
+ work_directory,
+ RepositoryEntry {
+ work_directory: work_directory_entry,
+ branch: repository.branch.map(Into::into),
+ statuses,
+ },
+ )
+ }
} else {
log::error!("no work directory entry for repository {:?}", repository)
}
@@ -1498,8 +1633,63 @@ impl Snapshot {
self.traverse_from_offset(true, include_ignored, 0)
}
- pub fn repositories(&self) -> impl Iterator<Item = &RepositoryEntry> {
- self.repository_entries.values()
+ pub fn repositories(&self) -> impl Iterator<Item = (&Arc<Path>, &RepositoryEntry)> {
+ self.repository_entries
+ .iter()
+ .map(|(path, entry)| (&path.0, entry))
+ }
+
+ /// Get the repository whose work directory contains the given path.
+ pub fn repository_for_work_directory(&self, path: &Path) -> Option<RepositoryEntry> {
+ self.repository_entries
+ .get(&RepositoryWorkDirectory(path.into()))
+ .cloned()
+ }
+
+ /// Get the repository whose work directory contains the given path.
+ pub fn repository_for_path(&self, path: &Path) -> Option<RepositoryEntry> {
+ let mut max_len = 0;
+ let mut current_candidate = None;
+ for (work_directory, repo) in (&self.repository_entries).iter() {
+ if path.starts_with(&work_directory.0) {
+ if work_directory.0.as_os_str().len() >= max_len {
+ current_candidate = Some(repo);
+ max_len = work_directory.0.as_os_str().len();
+ } else {
+ break;
+ }
+ }
+ }
+
+ current_candidate.cloned()
+ }
+
+ /// Given an ordered iterator of entries, returns an iterator of those entries,
+ /// along with their containing git repository.
+ pub fn entries_with_repositories<'a>(
+ &'a self,
+ entries: impl 'a + Iterator<Item = &'a Entry>,
+ ) -> impl 'a + Iterator<Item = (&'a Entry, Option<&'a RepositoryEntry>)> {
+ let mut containing_repos = Vec::<(&Arc<Path>, &RepositoryEntry)>::new();
+ let mut repositories = self.repositories().peekable();
+ entries.map(move |entry| {
+ while let Some((repo_path, _)) = containing_repos.last() {
+ if !entry.path.starts_with(repo_path) {
+ containing_repos.pop();
+ } else {
+ break;
+ }
+ }
+ while let Some((repo_path, _)) = repositories.peek() {
+ if entry.path.starts_with(repo_path) {
+ containing_repos.push(repositories.next().unwrap());
+ } else {
+ break;
+ }
+ }
+ let repo = containing_repos.last().map(|(_, repo)| *repo);
+ (entry, repo)
+ })
}
pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
@@ -1524,6 +1714,30 @@ impl Snapshot {
}
}
+ fn descendent_entries<'a>(
+ &'a self,
+ include_dirs: bool,
+ include_ignored: bool,
+ parent_path: &'a Path,
+ ) -> DescendentEntriesIter<'a> {
+ let mut cursor = self.entries_by_path.cursor();
+ cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &());
+ let mut traversal = Traversal {
+ cursor,
+ include_dirs,
+ include_ignored,
+ };
+
+ if traversal.end_offset() == traversal.start_offset() {
+ traversal.advance();
+ }
+
+ DescendentEntriesIter {
+ traversal,
+ parent_path,
+ }
+ }
+
pub fn root_entry(&self) -> Option<&Entry> {
self.entry_for_path("")
}
@@ -1570,32 +1784,17 @@ impl Snapshot {
}
impl LocalSnapshot {
- pub(crate) fn repo_for(&self, path: &Path) -> Option<RepositoryEntry> {
- let mut max_len = 0;
- let mut current_candidate = None;
- for (work_directory, repo) in (&self.repository_entries).iter() {
- if repo.contains(self, path) {
- if work_directory.0.as_os_str().len() >= max_len {
- current_candidate = Some(repo);
- max_len = work_directory.0.as_os_str().len();
- } else {
- break;
- }
- }
- }
-
- current_candidate.map(|entry| entry.to_owned())
+ pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> {
+ self.git_repositories.get(&repo.work_directory.0)
}
pub(crate) fn repo_for_metadata(
&self,
path: &Path,
- ) -> Option<(ProjectEntryId, Arc<Mutex<dyn GitRepository>>)> {
- let (entry_id, local_repo) = self
- .git_repositories
+ ) -> Option<(&ProjectEntryId, &LocalRepositoryEntry)> {
+ self.git_repositories
.iter()
- .find(|(_, repo)| repo.in_dot_git(path))?;
- Some((*entry_id, local_repo.repo_ptr.to_owned()))
+ .find(|(_, repo)| repo.in_dot_git(path))
}
#[cfg(test)]
@@ -1685,7 +1884,7 @@ impl LocalSnapshot {
}
Ordering::Equal => {
if self_repo != other_repo {
- updated_repositories.push((*self_repo).into());
+ updated_repositories.push(self_repo.build_update(other_repo));
}
self_repos.next();
@@ -1728,10 +1927,8 @@ impl LocalSnapshot {
let abs_path = self.abs_path.join(&entry.path);
match smol::block_on(build_gitignore(&abs_path, fs)) {
Ok(ignore) => {
- self.ignores_by_parent_abs_path.insert(
- abs_path.parent().unwrap().into(),
- (Arc::new(ignore), self.scan_id),
- );
+ self.ignores_by_parent_abs_path
+ .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true));
}
Err(error) => {
log::error!(
@@ -1743,8 +1940,6 @@ impl LocalSnapshot {
}
}
- self.reuse_entry_id(&mut entry);
-
if entry.kind == EntryKind::PendingDir {
if let Some(existing_entry) =
self.entries_by_path.get(&PathKey(entry.path.clone()), &())
@@ -1773,62 +1968,6 @@ impl LocalSnapshot {
entry
}
- fn populate_dir(
- &mut self,
- parent_path: Arc<Path>,
- entries: impl IntoIterator<Item = Entry>,
- ignore: Option<Arc<Gitignore>>,
- fs: &dyn Fs,
- ) {
- let mut parent_entry = if let Some(parent_entry) =
- self.entries_by_path.get(&PathKey(parent_path.clone()), &())
- {
- parent_entry.clone()
- } else {
- log::warn!(
- "populating a directory {:?} that has been removed",
- parent_path
- );
- return;
- };
-
- match parent_entry.kind {
- EntryKind::PendingDir => {
- parent_entry.kind = EntryKind::Dir;
- }
- EntryKind::Dir => {}
- _ => return,
- }
-
- if let Some(ignore) = ignore {
- self.ignores_by_parent_abs_path.insert(
- self.abs_path.join(&parent_path).into(),
- (ignore, self.scan_id),
- );
- }
-
- if parent_path.file_name() == Some(&DOT_GIT) {
- self.build_repo(parent_path, fs);
- }
-
- let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
- let mut entries_by_id_edits = Vec::new();
-
- for mut entry in entries {
- self.reuse_entry_id(&mut entry);
- entries_by_id_edits.push(Edit::Insert(PathEntry {
- id: entry.id,
- path: entry.path.clone(),
- is_ignored: entry.is_ignored,
- scan_id: self.scan_id,
- }));
- entries_by_path_edits.push(Edit::Insert(entry));
- }
-
- self.entries_by_path.edit(entries_by_path_edits, &());
- self.entries_by_id.edit(entries_by_id_edits, &());
- }
-
fn build_repo(&mut self, parent_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
let abs_path = self.abs_path.join(&parent_path);
let work_dir: Arc<Path> = parent_path.parent().unwrap().into();
@@ -1852,11 +1991,13 @@ impl LocalSnapshot {
let scan_id = self.scan_id;
let repo_lock = repo.lock();
+
self.repository_entries.insert(
work_directory,
RepositoryEntry {
work_directory: work_dir_id.into(),
branch: repo_lock.branch_name().map(Into::into),
+ statuses: repo_lock.statuses().unwrap_or_default(),
},
);
drop(repo_lock);
@@ -1865,6 +2006,7 @@ impl LocalSnapshot {
work_dir_id,
LocalRepositoryEntry {
scan_id,
+ full_scan_id: scan_id,
repo_ptr: repo,
git_dir_path: parent_path.clone(),
},
@@ -1873,46 +2015,6 @@ impl LocalSnapshot {
Some(())
}
- fn reuse_entry_id(&mut self, entry: &mut Entry) {
- if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
- entry.id = removed_entry_id;
- } else if let Some(existing_entry) = self.entry_for_path(&entry.path) {
- entry.id = existing_entry.id;
- }
- }
-
- fn remove_path(&mut self, path: &Path) {
- let mut new_entries;
- let removed_entries;
- {
- let mut cursor = self.entries_by_path.cursor::<TraversalProgress>();
- new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &());
- removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &());
- new_entries.push_tree(cursor.suffix(&()), &());
- }
- self.entries_by_path = new_entries;
-
- let mut entries_by_id_edits = Vec::new();
- for entry in removed_entries.cursor::<()>() {
- let removed_entry_id = self
- .removed_entry_ids
- .entry(entry.inode)
- .or_insert(entry.id);
- *removed_entry_id = cmp::max(*removed_entry_id, entry.id);
- entries_by_id_edits.push(Edit::Remove(entry.id));
- }
- self.entries_by_id.edit(entries_by_id_edits, &());
-
- if path.file_name() == Some(&GITIGNORE) {
- let abs_parent_path = self.abs_path.join(path.parent().unwrap());
- if let Some((_, scan_id)) = self
- .ignores_by_parent_abs_path
- .get_mut(abs_parent_path.as_path())
- {
- *scan_id = self.snapshot.scan_id;
- }
- }
- }
fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet<u64> {
let mut inodes = TreeSet::default();
@@ -1952,12 +2054,115 @@ impl LocalSnapshot {
}
}
-async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
- let contents = fs.load(abs_path).await?;
- let parent = abs_path.parent().unwrap_or_else(|| Path::new("/"));
- let mut builder = GitignoreBuilder::new(parent);
- for line in contents.lines() {
- builder.add_line(Some(abs_path.into()), line)?;
+impl LocalMutableSnapshot {
+ fn reuse_entry_id(&mut self, entry: &mut Entry) {
+ if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
+ entry.id = removed_entry_id;
+ } else if let Some(existing_entry) = self.entry_for_path(&entry.path) {
+ entry.id = existing_entry.id;
+ }
+ }
+
+ fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
+ self.reuse_entry_id(&mut entry);
+ self.snapshot.insert_entry(entry, fs)
+ }
+
+ fn populate_dir(
+ &mut self,
+ parent_path: Arc<Path>,
+ entries: impl IntoIterator<Item = Entry>,
+ ignore: Option<Arc<Gitignore>>,
+ fs: &dyn Fs,
+ ) {
+ let mut parent_entry = if let Some(parent_entry) =
+ self.entries_by_path.get(&PathKey(parent_path.clone()), &())
+ {
+ parent_entry.clone()
+ } else {
+ log::warn!(
+ "populating a directory {:?} that has been removed",
+ parent_path
+ );
+ return;
+ };
+
+ match parent_entry.kind {
+ EntryKind::PendingDir => {
+ parent_entry.kind = EntryKind::Dir;
+ }
+ EntryKind::Dir => {}
+ _ => return,
+ }
+
+ if let Some(ignore) = ignore {
+ let abs_parent_path = self.abs_path.join(&parent_path).into();
+ self.ignores_by_parent_abs_path
+ .insert(abs_parent_path, (ignore, false));
+ }
+
+ if parent_path.file_name() == Some(&DOT_GIT) {
+ self.build_repo(parent_path, fs);
+ }
+
+ let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
+ let mut entries_by_id_edits = Vec::new();
+
+ for mut entry in entries {
+ self.reuse_entry_id(&mut entry);
+ entries_by_id_edits.push(Edit::Insert(PathEntry {
+ id: entry.id,
+ path: entry.path.clone(),
+ is_ignored: entry.is_ignored,
+ scan_id: self.scan_id,
+ }));
+ entries_by_path_edits.push(Edit::Insert(entry));
+ }
+
+ self.entries_by_path.edit(entries_by_path_edits, &());
+ self.entries_by_id.edit(entries_by_id_edits, &());
+ }
+
+ fn remove_path(&mut self, path: &Path) {
+ let mut new_entries;
+ let removed_entries;
+ {
+ let mut cursor = self.entries_by_path.cursor::<TraversalProgress>();
+ new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &());
+ removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &());
+ new_entries.push_tree(cursor.suffix(&()), &());
+ }
+ self.entries_by_path = new_entries;
+
+ let mut entries_by_id_edits = Vec::new();
+ for entry in removed_entries.cursor::<()>() {
+ let removed_entry_id = self
+ .removed_entry_ids
+ .entry(entry.inode)
+ .or_insert(entry.id);
+ *removed_entry_id = cmp::max(*removed_entry_id, entry.id);
+ entries_by_id_edits.push(Edit::Remove(entry.id));
+ }
+ self.entries_by_id.edit(entries_by_id_edits, &());
+
+ if path.file_name() == Some(&GITIGNORE) {
+ let abs_parent_path = self.abs_path.join(path.parent().unwrap());
+ if let Some((_, needs_update)) = self
+ .ignores_by_parent_abs_path
+ .get_mut(abs_parent_path.as_path())
+ {
+ *needs_update = true;
+ }
+ }
+ }
+}
+
+async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
+ let contents = fs.load(abs_path).await?;
+ let parent = abs_path.parent().unwrap_or_else(|| Path::new("/"));
+ let mut builder = GitignoreBuilder::new(parent);
+ for line in contents.lines() {
+ builder.add_line(Some(abs_path.into()), line)?;
}
Ok(builder.build()?)
}
@@ -2394,18 +2599,25 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
}
struct BackgroundScanner {
- snapshot: Mutex<LocalSnapshot>,
+ snapshot: Mutex<LocalMutableSnapshot>,
fs: Arc<dyn Fs>,
status_updates_tx: UnboundedSender<ScanState>,
executor: Arc<executor::Background>,
refresh_requests_rx: channel::Receiver<(Vec<PathBuf>, barrier::Sender)>,
- prev_state: Mutex<(Snapshot, Vec<Arc<Path>>)>,
+ prev_state: Mutex<BackgroundScannerState>,
+ next_entry_id: Arc<AtomicUsize>,
finished_initial_scan: bool,
}
+struct BackgroundScannerState {
+ snapshot: Snapshot,
+ event_paths: Vec<Arc<Path>>,
+}
+
impl BackgroundScanner {
fn new(
snapshot: LocalSnapshot,
+ next_entry_id: Arc<AtomicUsize>,
fs: Arc<dyn Fs>,
status_updates_tx: UnboundedSender<ScanState>,
executor: Arc<executor::Background>,
@@ -2416,8 +2628,15 @@ impl BackgroundScanner {
status_updates_tx,
executor,
refresh_requests_rx,
- prev_state: Mutex::new((snapshot.snapshot.clone(), Vec::new())),
- snapshot: Mutex::new(snapshot),
+ next_entry_id,
+ prev_state: Mutex::new(BackgroundScannerState {
+ snapshot: snapshot.snapshot.clone(),
+ event_paths: Default::default(),
+ }),
+ snapshot: Mutex::new(LocalMutableSnapshot {
+ snapshot,
+ removed_entry_ids: Default::default(),
+ }),
finished_initial_scan: false,
}
}
@@ -2444,7 +2663,7 @@ impl BackgroundScanner {
self.snapshot
.lock()
.ignores_by_parent_abs_path
- .insert(ancestor.into(), (ignore.into(), 0));
+ .insert(ancestor.into(), (ignore.into(), false));
}
}
{
@@ -2497,7 +2716,7 @@ impl BackgroundScanner {
// these before handling changes reported by the filesystem.
request = self.refresh_requests_rx.recv().fuse() => {
let Ok((paths, barrier)) = request else { break };
- if !self.process_refresh_request(paths, barrier).await {
+ if !self.process_refresh_request(paths.clone(), barrier).await {
return;
}
}
@@ -2508,25 +2727,37 @@ impl BackgroundScanner {
while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) {
paths.extend(more_events.into_iter().map(|e| e.path));
}
- self.process_events(paths).await;
+ self.process_events(paths.clone()).await;
}
}
}
}
async fn process_refresh_request(&self, paths: Vec<PathBuf>, barrier: barrier::Sender) -> bool {
- self.reload_entries_for_paths(paths, None).await;
+ if let Some(mut paths) = self.reload_entries_for_paths(paths, None).await {
+ paths.sort_unstable();
+ util::extend_sorted(
+ &mut self.prev_state.lock().event_paths,
+ paths,
+ usize::MAX,
+ Ord::cmp,
+ );
+ }
self.send_status_update(false, Some(barrier))
}
async fn process_events(&mut self, paths: Vec<PathBuf>) {
let (scan_job_tx, scan_job_rx) = channel::unbounded();
- if let Some(mut paths) = self
+ let paths = self
.reload_entries_for_paths(paths, Some(scan_job_tx.clone()))
- .await
- {
- paths.sort_unstable();
- util::extend_sorted(&mut self.prev_state.lock().1, paths, usize::MAX, Ord::cmp);
+ .await;
+ if let Some(paths) = &paths {
+ util::extend_sorted(
+ &mut self.prev_state.lock().event_paths,
+ paths.iter().cloned(),
+ usize::MAX,
+ Ord::cmp,
+ );
}
drop(scan_job_tx);
self.scan_dirs(false, scan_job_rx).await;
@@ -2535,6 +2766,12 @@ impl BackgroundScanner {
let mut snapshot = self.snapshot.lock();
+ if let Some(paths) = paths {
+ for path in paths {
+ self.reload_repo_for_file_path(&path, &mut *snapshot, self.fs.as_ref());
+ }
+ }
+
let mut git_repositories = mem::take(&mut snapshot.git_repositories);
git_repositories.retain(|work_directory_id, _| {
snapshot
@@ -2553,13 +2790,11 @@ impl BackgroundScanner {
.is_some()
});
snapshot.snapshot.repository_entries = git_repository_entries;
-
- snapshot.removed_entry_ids.clear();
snapshot.completed_scan_id = snapshot.scan_id;
-
drop(snapshot);
self.send_status_update(false, None);
+ self.prev_state.lock().event_paths.clear();
}
async fn scan_dirs(
@@ -2637,14 +2872,18 @@ impl BackgroundScanner {
fn send_status_update(&self, scanning: bool, barrier: Option<barrier::Sender>) -> bool {
let mut prev_state = self.prev_state.lock();
- let snapshot = self.snapshot.lock().clone();
- let mut old_snapshot = snapshot.snapshot.clone();
- mem::swap(&mut old_snapshot, &mut prev_state.0);
- let changed_paths = mem::take(&mut prev_state.1);
- let changes = self.build_change_set(&old_snapshot, &snapshot.snapshot, changed_paths);
+ let new_snapshot = self.snapshot.lock().clone();
+ let old_snapshot = mem::replace(&mut prev_state.snapshot, new_snapshot.snapshot.clone());
+
+ let changes = self.build_change_set(
+ &old_snapshot,
+ &new_snapshot.snapshot,
+ &prev_state.event_paths,
+ );
+
self.status_updates_tx
.unbounded_send(ScanState::Updated {
- snapshot,
+ snapshot: new_snapshot,
changes,
scanning,
barrier,
@@ -2662,7 +2901,7 @@ impl BackgroundScanner {
(
snapshot.abs_path().clone(),
snapshot.root_char_bag,
- snapshot.next_entry_id.clone(),
+ self.next_entry_id.clone(),
)
};
let mut child_paths = self.fs.read_dir(&job.abs_path).await?;
@@ -2834,33 +3073,12 @@ impl BackgroundScanner {
let mut fs_entry = Entry::new(
path.clone(),
&metadata,
- snapshot.next_entry_id.as_ref(),
+ self.next_entry_id.as_ref(),
snapshot.root_char_bag,
);
fs_entry.is_ignored = ignore_stack.is_all();
snapshot.insert_entry(fs_entry, self.fs.as_ref());
- let scan_id = snapshot.scan_id;
-
- let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path);
- if let Some((entry_id, repo)) = repo_with_path_in_dotgit {
- let work_dir = snapshot
- .entry_for_id(entry_id)
- .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?;
-
- let repo = repo.lock();
- repo.reload_index();
- let branch = repo.branch_name();
-
- snapshot.git_repositories.update(&entry_id, |entry| {
- entry.scan_id = scan_id;
- });
-
- snapshot
- .repository_entries
- .update(&work_dir, |entry| entry.branch = branch.map(Into::into));
- }
-
if let Some(scan_queue_tx) = &scan_queue_tx {
let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
@@ -21,9 +21,13 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
postage.workspace = true
futures.workspace = true
+schemars.workspace = true
+serde.workspace = true
unicase = "2.6"
[dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
@@ -6,7 +6,7 @@ use gpui::{
actions,
anyhow::{anyhow, Result},
elements::{
- AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
+ AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler,
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
@@ -16,8 +16,13 @@ use gpui::{
ViewHandle, WeakViewHandle, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
-use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
-use settings::{settings_file::SettingsFile, Settings};
+use project::{
+ repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
+ Worktree, WorktreeId,
+};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
use std::{
cmp::Ordering,
collections::{hash_map, HashMap},
@@ -26,7 +31,7 @@ use std::{
path::Path,
sync::Arc,
};
-use theme::ProjectPanelEntry;
+use theme::{ui::FileName, ProjectPanelEntry};
use unicase::UniCase;
use workspace::{
dock::{DockPosition, Panel},
@@ -35,8 +40,41 @@ use workspace::{
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
+#[derive(Deserialize)]
+pub struct ProjectPanelSettings {
+ dock: ProjectPanelDockPosition,
+ default_width: f32,
+}
+
+impl settings::Setting for ProjectPanelSettings {
+ const KEY: Option<&'static str> = Some("project_panel");
+
+ type FileContent = ProjectPanelSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ProjectPanelSettingsContent {
+ dock: Option<ProjectPanelDockPosition>,
+ default_width: Option<f32>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+pub enum ProjectPanelDockPosition {
+ Left,
+ Right,
+}
+
pub struct ProjectPanel {
project: ModelHandle<Project>,
+ fs: Arc<dyn Fs>,
list: UniformListState,
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
last_worktree_root_id: Option<ProjectEntryId>,
@@ -90,6 +128,7 @@ pub struct EntryDetails {
is_editing: bool,
is_processing: bool,
is_cut: bool,
+ git_status: Option<GitFileStatus>,
}
actions!(
@@ -112,6 +151,7 @@ actions!(
);
pub fn init(cx: &mut AppContext) {
+ settings::register::<ProjectPanelSettings>(cx);
cx.add_action(ProjectPanel::expand_selected_entry);
cx.add_action(ProjectPanel::collapse_selected_entry);
cx.add_action(ProjectPanel::select_prev);
@@ -205,6 +245,7 @@ impl ProjectPanel {
let view_id = cx.view_id();
let mut this = Self {
project: project.clone(),
+ fs: workspace.app_state().fs.clone(),
list: Default::default(),
visible_entries: Default::default(),
last_worktree_root_id: Default::default(),
@@ -222,7 +263,7 @@ impl ProjectPanel {
// Update the dock position when the setting changes.
let mut old_dock_position = this.position(cx);
- cx.observe_global::<Settings, _>(move |this, cx| {
+ cx.observe_global::<SettingsStore, _>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
@@ -1027,7 +1068,13 @@ impl ProjectPanel {
.unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
- for entry in &visible_worktree_entries[entry_range] {
+ for (entry, repo) in
+ snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter())
+ {
+ let status = (entry.path.parent().is_some() && !entry.is_ignored)
+ .then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
+ .flatten();
+
let mut details = EntryDetails {
filename: entry
.path
@@ -1048,6 +1095,7 @@ impl ProjectPanel {
is_cut: self
.clipboard_entry
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
+ git_status: status,
};
if let Some(edit_state) = &self.edit_state {
@@ -1116,12 +1164,16 @@ impl ProjectPanel {
.flex(1.0, true)
.into_any()
} else {
- Label::new(details.filename.clone(), style.text.clone())
- .contained()
- .with_margin_left(style.icon_spacing)
- .aligned()
- .left()
- .into_any()
+ ComponentHost::new(FileName::new(
+ details.filename.clone(),
+ details.git_status,
+ FileName::style(style.text.clone(), &theme::current(cx)),
+ ))
+ .contained()
+ .with_margin_left(style.icon_spacing)
+ .aligned()
+ .left()
+ .into_any()
})
.constrained()
.with_height(style.height)
@@ -1225,7 +1277,7 @@ impl ProjectPanel {
let row_container_style = theme.dragged_entry.container;
move |_, cx: &mut ViewContext<Workspace>| {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
Self::render_entry_visual_element(
&details,
None,
@@ -1248,7 +1300,7 @@ impl View for ProjectPanel {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
enum ProjectPanel {}
- let theme = &cx.global::<Settings>().theme.project_panel;
+ let theme = &theme::current(cx).project_panel;
let mut container_style = theme.container;
let padding = std::mem::take(&mut container_style.padding);
let last_worktree_root_id = self.last_worktree_root_id;
@@ -1267,7 +1319,7 @@ impl View for ProjectPanel {
.sum(),
cx,
move |this, range, items, cx| {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let mut dragged_entry_destination =
this.dragged_entry_destination.clone();
this.for_each_visible_entry(range, cx, |id, details, cx| {
@@ -1304,8 +1356,7 @@ impl View for ProjectPanel {
.with_child(
MouseEventHandler::<Self, _>::new(2, cx, {
let button_style = theme.open_project_button.clone();
- let context_menu_item_style =
- cx.global::<Settings>().theme.context_menu.item.clone();
+ let context_menu_item_style = theme::current(cx).context_menu.item.clone();
move |state, cx| {
let button_style = button_style.style_for(state, false).clone();
let context_menu_item =
@@ -1360,10 +1411,9 @@ impl Entity for ProjectPanel {
impl workspace::dock::Panel for ProjectPanel {
fn position(&self, cx: &WindowContext) -> DockPosition {
- let settings = cx.global::<Settings>();
- match settings.project_panel.dock {
- settings::ProjectPanelDockPosition::Left => DockPosition::Left,
- settings::ProjectPanelDockPosition::Right => DockPosition::Right,
+ match settings::get::<ProjectPanelSettings>(cx).dock {
+ ProjectPanelDockPosition::Left => DockPosition::Left,
+ ProjectPanelDockPosition::Right => DockPosition::Right,
}
}
@@ -1372,19 +1422,21 @@ impl workspace::dock::Panel for ProjectPanel {
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
- SettingsFile::update(cx, move |settings| {
- let dock = match position {
- DockPosition::Left | DockPosition::Bottom => {
- settings::ProjectPanelDockPosition::Left
- }
- DockPosition::Right => settings::ProjectPanelDockPosition::Right,
- };
- settings.project_panel.dock = Some(dock);
- })
+ settings::update_settings_file::<ProjectPanelSettings>(
+ self.fs.clone(),
+ cx,
+ move |settings| {
+ let dock = match position {
+ DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
+ DockPosition::Right => ProjectPanelDockPosition::Right,
+ };
+ settings.dock = Some(dock);
+ },
+ );
}
fn default_size(&self, cx: &WindowContext) -> f32 {
- cx.global::<Settings>().project_panel.default_width
+ settings::get::<ProjectPanelSettings>(cx).default_width
}
fn should_zoom_in_on_event(_: &Self::Event) -> bool {
@@ -1459,15 +1511,13 @@ mod tests {
use gpui::{TestAppContext, ViewHandle};
use project::FakeFs;
use serde_json::json;
+ use settings::SettingsStore;
use std::{collections::HashSet, path::Path};
+ use workspace::{pane, AppState};
#[gpui::test]
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
- cx.update(|cx| {
- let settings = Settings::test(cx);
- cx.set_global(settings);
- });
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -1555,11 +1605,7 @@ mod tests {
#[gpui::test(iterations = 30)]
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
- cx.update(|cx| {
- let settings = Settings::test(cx);
- cx.set_global(settings);
- });
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -1875,11 +1921,7 @@ mod tests {
#[gpui::test]
async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
- cx.foreground().forbid_parking();
- cx.update(|cx| {
- let settings = Settings::test(cx);
- cx.set_global(settings);
- });
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -1947,6 +1989,95 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
+ init_test_with_editor(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/src",
+ json!({
+ "test": {
+ "first.rs": "// First Rust file",
+ "second.rs": "// Second Rust file",
+ "third.rs": "// Third Rust file",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+ toggle_expand_dir(&panel, "src/test", cx);
+ select_path(&panel, "src/test/first.rs", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " first.rs <== selected",
+ " second.rs",
+ " third.rs"
+ ]
+ );
+ ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx);
+
+ submit_deletion(window_id, &panel, cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " second.rs",
+ " third.rs"
+ ],
+ "Project panel should have no deleted file, no other file is selected in it"
+ );
+ ensure_no_open_items_and_panes(window_id, &workspace, cx);
+
+ select_path(&panel, "src/test/second.rs", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " second.rs <== selected",
+ " third.rs"
+ ]
+ );
+ ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx);
+
+ cx.update_window(window_id, |cx| {
+ let active_items = workspace
+ .read(cx)
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item())
+ .collect::<Vec<_>>();
+ assert_eq!(active_items.len(), 1);
+ let open_editor = active_items
+ .into_iter()
+ .next()
+ .unwrap()
+ .downcast::<Editor>()
+ .expect("Open item should be an editor");
+ open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
+ });
+ submit_deletion(window_id, &panel, cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["v src", " v test", " third.rs"],
+ "Project panel should have no deleted file, with one last file remaining"
+ );
+ ensure_no_open_items_and_panes(window_id, &workspace, cx);
+ }
+
fn toggle_expand_dir(
panel: &ViewHandle<ProjectPanel>,
path: impl AsRef<Path>,
@@ -2039,4 +2170,105 @@ mod tests {
result
}
+
+ fn init_test(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ language::init(cx);
+ editor::init_settings(cx);
+ workspace::init_settings(cx);
+ });
+ }
+
+ fn init_test_with_editor(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| {
+ let app_state = AppState::test(cx);
+ theme::init((), cx);
+ language::init(cx);
+ editor::init(cx);
+ pane::init(cx);
+ workspace::init(app_state.clone(), cx);
+ });
+ }
+
+ fn ensure_single_file_is_opened(
+ window_id: usize,
+ workspace: &ViewHandle<Workspace>,
+ expected_path: &str,
+ cx: &mut TestAppContext,
+ ) {
+ cx.read_window(window_id, |cx| {
+ let workspace = workspace.read(cx);
+ let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ let worktree_id = WorktreeId::from_usize(worktrees[0].id());
+
+ let open_project_paths = workspace
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+ .collect::<Vec<_>>();
+ assert_eq!(
+ open_project_paths,
+ vec![ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new(expected_path))
+ }],
+ "Should have opened file, selected in project panel"
+ );
+ });
+ }
+
+ fn submit_deletion(
+ window_id: usize,
+ panel: &ViewHandle<ProjectPanel>,
+ cx: &mut TestAppContext,
+ ) {
+ assert!(
+ !cx.has_pending_prompt(window_id),
+ "Should have no prompts before the deletion"
+ );
+ panel.update(cx, |panel, cx| {
+ panel
+ .delete(&Delete, cx)
+ .expect("Deletion start")
+ .detach_and_log_err(cx);
+ });
+ assert!(
+ cx.has_pending_prompt(window_id),
+ "Should have a prompt after the deletion"
+ );
+ cx.simulate_prompt_answer(window_id, 0);
+ assert!(
+ !cx.has_pending_prompt(window_id),
+ "Should have no prompts after prompt was replied to"
+ );
+ cx.foreground().run_until_parked();
+ }
+
+ fn ensure_no_open_items_and_panes(
+ window_id: usize,
+ workspace: &ViewHandle<Workspace>,
+ cx: &mut TestAppContext,
+ ) {
+ assert!(
+ !cx.has_pending_prompt(window_id),
+ "Should have no prompts after deletion operation closes the file"
+ );
+ cx.read_window(window_id, |cx| {
+ let open_project_paths = workspace
+ .read(cx)
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+ .collect::<Vec<_>>();
+ assert!(
+ open_project_paths.is_empty(),
+ "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
+ );
+ });
+ }
}
@@ -17,7 +17,9 @@ project = { path = "../project" }
text = { path = "../text" }
settings = { path = "../settings" }
workspace = { path = "../workspace" }
+theme = { path = "../theme" }
util = { path = "../util" }
+
anyhow.workspace = true
ordered-float.workspace = true
postage.workspace = true
@@ -25,8 +27,11 @@ smol.workspace = true
[dev-dependencies]
futures.workspace = true
+editor = { path = "../editor", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
@@ -9,7 +9,6 @@ use gpui::{
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate, PickerEvent};
use project::{Project, Symbol};
-use settings::Settings;
use std::{borrow::Cow, cmp::Reverse, sync::Arc};
use util::ResultExt;
use workspace::Workspace;
@@ -195,12 +194,13 @@ impl PickerDelegate for ProjectSymbolsDelegate {
selected: bool,
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
- let string_match = &self.matches[ix];
- let settings = cx.global::<Settings>();
- let style = &settings.theme.picker.item;
+ let theme = theme::current(cx);
+ let style = &theme.picker.item;
let current_style = style.style_for(mouse_state, selected);
+
+ let string_match = &self.matches[ix];
let symbol = &self.symbols[string_match.candidate_id];
- let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax);
+ let syntax_runs = styled_runs_for_code_label(&symbol.label, &theme.editor.syntax);
let mut path = symbol.path.path.to_string_lossy();
if self.show_worktree_root_name {
@@ -244,12 +244,12 @@ mod tests {
use gpui::{serde_json::json, TestAppContext};
use language::{FakeLspAdapter, Language, LanguageConfig};
use project::FakeFs;
+ use settings::SettingsStore;
use std::{path::Path, sync::Arc};
#[gpui::test]
async fn test_project_symbols(cx: &mut TestAppContext) {
- cx.foreground().forbid_parking();
- cx.update(|cx| cx.set_global(Settings::test(cx)));
+ init_test(cx);
let mut language = Language::new(
LanguageConfig {
@@ -368,6 +368,17 @@ mod tests {
});
}
+ fn init_test(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ language::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ });
+ }
+
fn symbol(name: &str, path: impl AsRef<Path>) -> lsp::SymbolInformation {
#[allow(deprecated)]
lsp::SymbolInformation {
@@ -18,8 +18,12 @@ picker = { path = "../picker" }
settings = { path = "../settings" }
text = { path = "../text" }
util = { path = "../util"}
+theme = { path = "../theme" }
workspace = { path = "../workspace" }
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -10,7 +10,6 @@ use gpui::{
use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::Settings;
use std::sync::Arc;
use workspace::{
notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
@@ -173,9 +172,10 @@ impl PickerDelegate for RecentProjectsDelegate {
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
- let settings = cx.global::<Settings>();
+ let theme = theme::current(cx);
+ let style = theme.picker.item.style_for(mouse_state, selected);
+
let string_match = &self.matches[ix];
- let style = settings.theme.picker.item.style_for(mouse_state, selected);
let highlighted_location = HighlightedWorkspaceLocation::new(
&string_match,
@@ -986,8 +986,22 @@ message Entry {
message RepositoryEntry {
uint64 work_directory_id = 1;
optional string branch = 2;
+ repeated string removed_repo_paths = 3;
+ repeated StatusEntry updated_statuses = 4;
}
+message StatusEntry {
+ string repo_path = 1;
+ GitStatus status = 2;
+}
+
+enum GitStatus {
+ Added = 0;
+ Modified = 1;
+ Conflict = 2;
+}
+
+
message BufferState {
uint64 id = 1;
optional File file = 2;
@@ -1,6 +1,7 @@
use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope};
use anyhow::{anyhow, Result};
use async_tungstenite::tungstenite::Message as WebSocketMessage;
+use collections::HashMap;
use futures::{SinkExt as _, StreamExt as _};
use prost::Message as _;
use serde::Serialize;
@@ -484,14 +485,21 @@ pub fn split_worktree_update(
mut message: UpdateWorktree,
max_chunk_size: usize,
) -> impl Iterator<Item = UpdateWorktree> {
- let mut done = false;
+ let mut done_files = false;
+
+ let mut repository_map = message
+ .updated_repositories
+ .into_iter()
+ .map(|repo| (repo.work_directory_id, repo))
+ .collect::<HashMap<_, _>>();
+
iter::from_fn(move || {
- if done {
+ if done_files {
return None;
}
let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size);
- let updated_entries = message
+ let updated_entries: Vec<_> = message
.updated_entries
.drain(..updated_entries_chunk_size)
.collect();
@@ -502,22 +510,28 @@ pub fn split_worktree_update(
.drain(..removed_entries_chunk_size)
.collect();
- done = message.updated_entries.is_empty() && message.removed_entries.is_empty();
+ done_files = message.updated_entries.is_empty() && message.removed_entries.is_empty();
- // Wait to send repositories until after we've guaranteed that their associated entries
- // will be read
- let updated_repositories = if done {
- mem::take(&mut message.updated_repositories)
- } else {
- Default::default()
- };
+ let mut updated_repositories = Vec::new();
- let removed_repositories = if done {
+ if !repository_map.is_empty() {
+ for entry in &updated_entries {
+ if let Some(repo) = repository_map.remove(&entry.id) {
+ updated_repositories.push(repo)
+ }
+ }
+ }
+
+ let removed_repositories = if done_files {
mem::take(&mut message.removed_repositories)
} else {
Default::default()
};
+ if done_files {
+ updated_repositories.extend(mem::take(&mut repository_map).into_values());
+ }
+
Some(UpdateWorktree {
project_id: message.project_id,
worktree_id: message.worktree_id,
@@ -526,7 +540,7 @@ pub fn split_worktree_update(
updated_entries,
removed_entries,
scan_id: message.scan_id,
- is_last_update: done && message.is_last_update,
+ is_last_update: done_files && message.is_last_update,
updated_repositories,
removed_repositories,
})
@@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 54;
+pub const PROTOCOL_VERSION: u32 = 55;
@@ -27,9 +27,10 @@ serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
-glob.workspace = true
+globset.workspace = true
[dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
serde_json.workspace = true
@@ -13,7 +13,6 @@ use gpui::{
};
use project::search::SearchQuery;
use serde::Deserialize;
-use settings::Settings;
use std::{any::Any, sync::Arc};
use util::ResultExt;
use workspace::{
@@ -93,7 +92,7 @@ impl View for BufferSearchBar {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let editor_container = if self.query_contains_error {
theme.search.invalid_editor
} else {
@@ -324,16 +323,12 @@ impl BufferSearchBar {
return None;
}
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
let is_active = self.is_search_option_enabled(option);
Some(
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
- let style = cx
- .global::<Settings>()
- .theme
- .search
- .option_button
- .style_for(state, is_active);
+ let theme = theme::current(cx);
+ let style = theme.search.option_button.style_for(state, is_active);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@@ -371,16 +366,12 @@ impl BufferSearchBar {
tooltip = "Select Next Match";
}
};
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
enum NavButton {}
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
- let style = cx
- .global::<Settings>()
- .theme
- .search
- .option_button
- .style_for(state, false);
+ let theme = theme::current(cx);
+ let style = theme.search.option_button.style_for(state, false);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@@ -408,7 +399,7 @@ impl BufferSearchBar {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let tooltip = "Dismiss Buffer Search";
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
enum CloseButton {}
MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
@@ -655,19 +646,11 @@ mod tests {
use editor::{DisplayPoint, Editor};
use gpui::{color::Color, test::EmptyView, TestAppContext};
use language::Buffer;
- use std::sync::Arc;
use unindent::Unindent as _;
#[gpui::test]
async fn test_search_simple(cx: &mut TestAppContext) {
- let fonts = cx.font_cache();
- let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
- theme.search.match_background = Color::red();
- cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.theme = Arc::new(theme);
- cx.set_global(settings)
- });
+ crate::project_search::tests::init_test(cx);
let buffer = cx.add_model(|cx| {
Buffer::new(
@@ -2,12 +2,14 @@ use crate::{
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
ToggleWholeWord,
};
+use anyhow::Result;
use collections::HashMap;
use editor::{
items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
SelectAll, MAX_TAB_TITLE_LEN,
};
use futures::StreamExt;
+use globset::{Glob, GlobMatcher};
use gpui::{
actions,
elements::*,
@@ -17,7 +19,6 @@ use gpui::{
};
use menu::Confirm;
use project::{search::SearchQuery, Project};
-use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
@@ -195,7 +196,7 @@ impl View for ProjectSearchView {
if model.match_ranges.is_empty() {
enum Status {}
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let text = if self.query_editor.read(cx).text(cx).is_empty() {
""
} else if model.pending_search.is_some() {
@@ -572,46 +573,30 @@ impl ProjectSearchView {
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
let text = self.query_editor.read(cx).text(cx);
- let included_files = match self
- .included_files_editor
- .read(cx)
- .text(cx)
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()
- {
- Ok(included_files) => {
- self.panels_with_errors.remove(&InputPanel::Include);
- included_files
- }
- Err(_e) => {
- self.panels_with_errors.insert(InputPanel::Include);
- cx.notify();
- return None;
- }
- };
- let excluded_files = match self
- .excluded_files_editor
- .read(cx)
- .text(cx)
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()
- {
- Ok(excluded_files) => {
- self.panels_with_errors.remove(&InputPanel::Exclude);
- excluded_files
- }
- Err(_e) => {
- self.panels_with_errors.insert(InputPanel::Exclude);
- cx.notify();
- return None;
- }
- };
+ let included_files =
+ match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) {
+ Ok(included_files) => {
+ self.panels_with_errors.remove(&InputPanel::Include);
+ included_files
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Include);
+ cx.notify();
+ return None;
+ }
+ };
+ let excluded_files =
+ match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) {
+ Ok(excluded_files) => {
+ self.panels_with_errors.remove(&InputPanel::Exclude);
+ excluded_files
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Exclude);
+ cx.notify();
+ return None;
+ }
+ };
if self.regex {
match SearchQuery::regex(
text,
@@ -641,6 +626,14 @@ impl ProjectSearchView {
}
}
+ fn load_glob_set(text: &str) -> Result<Vec<GlobMatcher>> {
+ text.split(',')
+ .map(str::trim)
+ .filter(|glob_str| !glob_str.is_empty())
+ .map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher()))
+ .collect()
+ }
+
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
let match_ranges = self.model.read(cx).match_ranges.clone();
@@ -903,16 +896,12 @@ impl ProjectSearchBar {
tooltip = "Select Next Match";
}
};
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
enum NavButton {}
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
- let style = &cx
- .global::<Settings>()
- .theme
- .search
- .option_button
- .style_for(state, false);
+ let theme = theme::current(cx);
+ let style = theme.search.option_button.style_for(state, false);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@@ -939,15 +928,11 @@ impl ProjectSearchBar {
option: SearchOption,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
let is_active = self.is_option_enabled(option, cx);
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
- let style = &cx
- .global::<Settings>()
- .theme
- .search
- .option_button
- .style_for(state, is_active);
+ let theme = theme::current(cx);
+ let style = theme.search.option_button.style_for(state, is_active);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@@ -992,7 +977,7 @@ impl View for ProjectSearchBar {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
theme.search.invalid_editor
} else {
@@ -1146,25 +1131,19 @@ impl ToolbarItemView for ProjectSearchBar {
}
#[cfg(test)]
-mod tests {
+pub mod tests {
use super::*;
use editor::DisplayPoint;
use gpui::{color::Color, executor::Deterministic, TestAppContext};
use project::FakeFs;
use serde_json::json;
+ use settings::SettingsStore;
use std::sync::Arc;
+ use theme::ThemeSettings;
#[gpui::test]
async fn test_project_search(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
- let fonts = cx.font_cache();
- let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
- theme.search.match_background = Color::red();
- cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.theme = Arc::new(theme);
- cx.set_global(settings);
- cx.set_global(ActiveSearches::default());
- });
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -1279,4 +1258,27 @@ mod tests {
);
});
}
+
+ pub fn init_test(cx: &mut TestAppContext) {
+ let fonts = cx.font_cache();
+ let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
+ theme.search.match_background = Color::red();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ cx.set_global(ActiveSearches::default());
+
+ theme::init((), cx);
+ cx.update_global::<SettingsStore, _, _>(|store, _| {
+ let mut settings = store.get::<ThemeSettings>(None).clone();
+ settings.theme = Arc::new(theme);
+ store.override_global(settings)
+ });
+
+ language::init(cx);
+ client::init_settings(cx);
+ editor::init_settings(cx);
+ workspace::init_settings(cx);
+ });
+ }
}
@@ -9,7 +9,7 @@ path = "src/settings.rs"
doctest = false
[features]
-test-support = []
+test-support = ["gpui/test-support", "fs/test-support"]
[dependencies]
assets = { path = "../assets" }
@@ -17,21 +17,20 @@ collections = { path = "../collections" }
gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" }
fs = { path = "../fs" }
-anyhow.workspace = true
-futures.workspace = true
-theme = { path = "../theme" }
staff_mode = { path = "../staff_mode" }
util = { path = "../util" }
-glob.workspace = true
+anyhow.workspace = true
+futures.workspace = true
json_comments = "0.2"
lazy_static.workspace = true
postage.workspace = true
-schemars = "0.8"
+schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
-toml = "0.5"
+smallvec.workspace = true
+toml.workspace = true
tree-sitter = "*"
tree-sitter-json = "*"
@@ -1,4 +1,4 @@
-use crate::{parse_json_with_comments, Settings};
+use crate::settings_store::parse_json_with_comments;
use anyhow::{Context, Result};
use assets::Assets;
use collections::BTreeMap;
@@ -41,20 +41,14 @@ impl JsonSchema for KeymapAction {
struct ActionWithData(Box<str>, Box<RawValue>);
impl KeymapFileContent {
- pub fn load_defaults(cx: &mut AppContext) {
- for path in ["keymaps/default.json", "keymaps/vim.json"] {
- Self::load(path, cx).unwrap();
- }
-
- if let Some(asset_path) = cx.global::<Settings>().base_keymap.asset_path() {
- Self::load(asset_path, cx).log_err();
- }
- }
-
- pub fn load(asset_path: &str, cx: &mut AppContext) -> Result<()> {
+ pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
let content = Assets::get(asset_path).unwrap().data;
let content_str = std::str::from_utf8(content.as_ref()).unwrap();
- parse_json_with_comments::<Self>(content_str)?.add_to_cx(cx)
+ Self::parse(content_str)?.add_to_cx(cx)
+ }
+
+ pub fn parse(content: &str) -> Result<Self> {
+ parse_json_with_comments::<Self>(content)
}
pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
@@ -1,1595 +1,19 @@
mod keymap_file;
-pub mod settings_file;
-pub mod watched_json;
-
-use anyhow::Result;
-use gpui::{
- font_cache::{FamilyId, FontCache},
- fonts, AssetSource,
-};
-use lazy_static::lazy_static;
-use schemars::{
- gen::{SchemaGenerator, SchemaSettings},
- schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
- JsonSchema,
-};
-use serde::{de::DeserializeOwned, Deserialize, Serialize};
-use serde_json::Value;
-use std::{
- borrow::Cow, collections::HashMap, num::NonZeroU32, ops::Range, path::Path, str, sync::Arc,
-};
-use theme::{Theme, ThemeRegistry};
-use tree_sitter::{Query, Tree};
-use util::{RangeExt, ResultExt as _};
+mod settings_file;
+mod settings_store;
+use gpui::AssetSource;
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
-pub use watched_json::watch_files;
+pub use settings_file::*;
+pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
+use std::{borrow::Cow, str};
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
-#[derive(Clone)]
-pub struct Settings {
- pub features: Features,
- pub buffer_font_family_name: String,
- pub buffer_font_features: fonts::Features,
- pub buffer_font_family: FamilyId,
- pub default_buffer_font_size: f32,
- pub buffer_font_size: f32,
- pub active_pane_magnification: f32,
- pub cursor_blink: bool,
- pub confirm_quit: bool,
- pub hover_popover_enabled: bool,
- pub show_completions_on_input: bool,
- pub show_call_status_icon: bool,
- pub vim_mode: bool,
- pub autosave: Autosave,
- pub project_panel: ProjectPanelSettings,
- pub editor_defaults: EditorSettings,
- pub editor_overrides: EditorSettings,
- pub git: GitSettings,
- pub git_overrides: GitSettings,
- pub copilot: CopilotSettings,
- pub journal_defaults: JournalSettings,
- pub journal_overrides: JournalSettings,
- pub terminal_defaults: TerminalSettings,
- pub terminal_overrides: TerminalSettings,
- pub language_defaults: HashMap<Arc<str>, EditorSettings>,
- pub language_overrides: HashMap<Arc<str>, EditorSettings>,
- pub lsp: HashMap<Arc<str>, LspSettings>,
- pub theme: Arc<Theme>,
- pub telemetry_defaults: TelemetrySettings,
- pub telemetry_overrides: TelemetrySettings,
- pub auto_update: bool,
- pub base_keymap: BaseKeymap,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
-pub enum BaseKeymap {
- #[default]
- VSCode,
- JetBrains,
- SublimeText,
- Atom,
- TextMate,
-}
-
-impl BaseKeymap {
- pub const OPTIONS: [(&'static str, Self); 5] = [
- ("VSCode (Default)", Self::VSCode),
- ("Atom", Self::Atom),
- ("JetBrains", Self::JetBrains),
- ("Sublime Text", Self::SublimeText),
- ("TextMate", Self::TextMate),
- ];
-
- pub fn asset_path(&self) -> Option<&'static str> {
- match self {
- BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
- BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
- BaseKeymap::Atom => Some("keymaps/atom.json"),
- BaseKeymap::TextMate => Some("keymaps/textmate.json"),
- BaseKeymap::VSCode => None,
- }
- }
-
- pub fn names() -> impl Iterator<Item = &'static str> {
- Self::OPTIONS.iter().map(|(name, _)| *name)
- }
-
- pub fn from_names(option: &str) -> BaseKeymap {
- Self::OPTIONS
- .iter()
- .copied()
- .find_map(|(name, value)| (name == option).then(|| value))
- .unwrap_or_default()
- }
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct TelemetrySettings {
- diagnostics: Option<bool>,
- metrics: Option<bool>,
-}
-
-impl TelemetrySettings {
- pub fn metrics(&self) -> bool {
- self.metrics.unwrap()
- }
-
- pub fn diagnostics(&self) -> bool {
- self.diagnostics.unwrap()
- }
-
- pub fn set_metrics(&mut self, value: bool) {
- self.metrics = Some(value);
- }
-
- pub fn set_diagnostics(&mut self, value: bool) {
- self.diagnostics = Some(value);
- }
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct CopilotSettings {
- pub disabled_globs: Vec<glob::Pattern>,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct CopilotSettingsContent {
- #[serde(default)]
- pub disabled_globs: Option<Vec<String>>,
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct GitSettings {
- pub git_gutter: Option<GitGutter>,
- pub gutter_debounce: Option<u64>,
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitGutter {
- #[default]
- TrackedFiles,
- Hide,
-}
-
-pub struct GitGutterConfig {}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-pub struct ProjectPanelSettings {
- pub dock: ProjectPanelDockPosition,
- pub default_width: f32,
-}
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct ProjectPanelSettingsContent {
- pub dock: Option<ProjectPanelDockPosition>,
- pub default_width: Option<f32>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum ProjectPanelDockPosition {
- Left,
- Right,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct EditorSettings {
- pub tab_size: Option<NonZeroU32>,
- pub hard_tabs: Option<bool>,
- pub soft_wrap: Option<SoftWrap>,
- pub preferred_line_length: Option<u32>,
- pub format_on_save: Option<FormatOnSave>,
- pub remove_trailing_whitespace_on_save: Option<bool>,
- pub ensure_final_newline_on_save: Option<bool>,
- pub formatter: Option<Formatter>,
- pub enable_language_server: Option<bool>,
- pub show_copilot_suggestions: Option<bool>,
- pub show_whitespaces: Option<ShowWhitespaces>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum SoftWrap {
- None,
- EditorWidth,
- PreferredLineLength,
-}
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum FormatOnSave {
- On,
- Off,
- LanguageServer,
- External {
- command: String,
- arguments: Vec<String>,
- },
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Formatter {
- LanguageServer,
- External {
- command: String,
- arguments: Vec<String>,
- },
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Autosave {
- Off,
- AfterDelay { milliseconds: u64 },
- OnFocusChange,
- OnWindowChange,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-pub struct JournalSettings {
- pub path: Option<String>,
- pub hour_format: Option<HourFormat>,
-}
-
-impl Default for JournalSettings {
- fn default() -> Self {
- Self {
- path: Some("~".into()),
- hour_format: Some(Default::default()),
- }
- }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum HourFormat {
- Hour12,
- Hour24,
-}
-
-impl Default for HourFormat {
- fn default() -> Self {
- Self::Hour12
- }
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct TerminalSettings {
- pub default_width: Option<f32>,
- pub default_height: Option<f32>,
- pub shell: Option<Shell>,
- pub working_directory: Option<WorkingDirectory>,
- pub font_size: Option<f32>,
- pub font_family: Option<String>,
- pub line_height: Option<TerminalLineHeight>,
- pub font_features: Option<fonts::Features>,
- pub env: Option<HashMap<String, String>>,
- pub blinking: Option<TerminalBlink>,
- pub alternate_scroll: Option<AlternateScroll>,
- pub option_as_meta: Option<bool>,
- pub copy_on_select: Option<bool>,
- pub dock: Option<TerminalDockPosition>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum TerminalDockPosition {
- Left,
- Bottom,
- Right,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalLineHeight {
- #[default]
- Comfortable,
- Standard,
- Custom(f32),
-}
-
-impl TerminalLineHeight {
- fn value(&self) -> f32 {
- match self {
- TerminalLineHeight::Comfortable => 1.618,
- TerminalLineHeight::Standard => 1.3,
- TerminalLineHeight::Custom(line_height) => *line_height,
- }
- }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalBlink {
- Off,
- TerminalControlled,
- On,
-}
-
-impl Default for TerminalBlink {
- fn default() -> Self {
- TerminalBlink::TerminalControlled
- }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Shell {
- System,
- Program(String),
- WithArguments { program: String, args: Vec<String> },
-}
-
-impl Default for Shell {
- fn default() -> Self {
- Shell::System
- }
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum AlternateScroll {
- On,
- Off,
-}
-
-impl Default for AlternateScroll {
- fn default() -> Self {
- AlternateScroll::On
- }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum WorkingDirectory {
- CurrentProjectDirectory,
- FirstProjectDirectory,
- AlwaysHome,
- Always { directory: String },
-}
-
-impl Default for WorkingDirectory {
- fn default() -> Self {
- Self::CurrentProjectDirectory
- }
-}
-
-impl TerminalSettings {
- fn line_height(&self) -> Option<f32> {
- self.line_height
- .to_owned()
- .map(|line_height| line_height.value())
- }
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct SettingsFileContent {
- #[serde(default)]
- pub buffer_font_family: Option<String>,
- #[serde(default)]
- pub buffer_font_size: Option<f32>,
- #[serde(default)]
- pub buffer_font_features: Option<fonts::Features>,
- #[serde(default)]
- pub copilot: Option<CopilotSettingsContent>,
- #[serde(default)]
- pub active_pane_magnification: Option<f32>,
- #[serde(default)]
- pub cursor_blink: Option<bool>,
- #[serde(default)]
- pub confirm_quit: Option<bool>,
- #[serde(default)]
- pub hover_popover_enabled: Option<bool>,
- #[serde(default)]
- pub show_completions_on_input: Option<bool>,
- #[serde(default)]
- pub show_call_status_icon: Option<bool>,
- #[serde(default)]
- pub vim_mode: Option<bool>,
- #[serde(default)]
- pub autosave: Option<Autosave>,
- #[serde(flatten)]
- pub editor: EditorSettings,
- #[serde(default)]
- pub project_panel: ProjectPanelSettingsContent,
- #[serde(default)]
- pub journal: JournalSettings,
- #[serde(default)]
- pub terminal: TerminalSettings,
- #[serde(default)]
- pub git: Option<GitSettings>,
- #[serde(default)]
- #[serde(alias = "language_overrides")]
- pub languages: HashMap<Arc<str>, EditorSettings>,
- #[serde(default)]
- pub lsp: HashMap<Arc<str>, LspSettings>,
- #[serde(default)]
- pub theme: Option<String>,
- #[serde(default)]
- pub telemetry: TelemetrySettings,
- #[serde(default)]
- pub auto_update: Option<bool>,
- #[serde(default)]
- pub base_keymap: Option<BaseKeymap>,
- #[serde(default)]
- pub features: FeaturesContent,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub struct LspSettings {
- pub initialization_options: Option<Value>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Features {
- pub copilot: bool,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-pub struct FeaturesContent {
- pub copilot: Option<bool>,
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowWhitespaces {
- #[default]
- Selection,
- None,
- All,
-}
-
-impl Settings {
- pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
- match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
- Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
- Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
- }
- }
-
- /// Fill out the settings corresponding to the default.json file, overrides will be set later
- pub fn defaults(
- assets: impl AssetSource,
- font_cache: &FontCache,
- themes: &ThemeRegistry,
- ) -> Self {
- #[track_caller]
- fn required<T>(value: Option<T>) -> Option<T> {
- assert!(value.is_some(), "missing default setting value");
- value
- }
-
- let defaults: SettingsFileContent = parse_json_with_comments(
- str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
- )
- .unwrap();
-
- let buffer_font_features = defaults.buffer_font_features.unwrap();
- Self {
- buffer_font_family: font_cache
- .load_family(
- &[defaults.buffer_font_family.as_ref().unwrap()],
- &buffer_font_features,
- )
- .unwrap(),
- buffer_font_family_name: defaults.buffer_font_family.unwrap(),
- buffer_font_features,
- buffer_font_size: defaults.buffer_font_size.unwrap(),
- active_pane_magnification: defaults.active_pane_magnification.unwrap(),
- default_buffer_font_size: defaults.buffer_font_size.unwrap(),
- confirm_quit: defaults.confirm_quit.unwrap(),
- cursor_blink: defaults.cursor_blink.unwrap(),
- hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
- show_completions_on_input: defaults.show_completions_on_input.unwrap(),
- show_call_status_icon: defaults.show_call_status_icon.unwrap(),
- vim_mode: defaults.vim_mode.unwrap(),
- autosave: defaults.autosave.unwrap(),
- project_panel: ProjectPanelSettings {
- dock: defaults.project_panel.dock.unwrap(),
- default_width: defaults.project_panel.default_width.unwrap(),
- },
- editor_defaults: EditorSettings {
- tab_size: required(defaults.editor.tab_size),
- hard_tabs: required(defaults.editor.hard_tabs),
- soft_wrap: required(defaults.editor.soft_wrap),
- preferred_line_length: required(defaults.editor.preferred_line_length),
- remove_trailing_whitespace_on_save: required(
- defaults.editor.remove_trailing_whitespace_on_save,
- ),
- ensure_final_newline_on_save: required(
- defaults.editor.ensure_final_newline_on_save,
- ),
- format_on_save: required(defaults.editor.format_on_save),
- formatter: required(defaults.editor.formatter),
- enable_language_server: required(defaults.editor.enable_language_server),
- show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
- show_whitespaces: required(defaults.editor.show_whitespaces),
- },
- editor_overrides: Default::default(),
- copilot: CopilotSettings {
- disabled_globs: defaults
- .copilot
- .unwrap()
- .disabled_globs
- .unwrap()
- .into_iter()
- .map(|s| glob::Pattern::new(&s).unwrap())
- .collect(),
- },
- git: defaults.git.unwrap(),
- git_overrides: Default::default(),
- journal_defaults: defaults.journal,
- journal_overrides: Default::default(),
- terminal_defaults: defaults.terminal,
- terminal_overrides: Default::default(),
- language_defaults: defaults.languages,
- language_overrides: Default::default(),
- lsp: defaults.lsp.clone(),
- theme: themes.get(&defaults.theme.unwrap()).unwrap(),
- telemetry_defaults: defaults.telemetry,
- telemetry_overrides: Default::default(),
- auto_update: defaults.auto_update.unwrap(),
- base_keymap: Default::default(),
- features: Features {
- copilot: defaults.features.copilot.unwrap(),
- },
- }
- }
-
- // Fill out the overrride and etc. settings from the user's settings.json
- pub fn set_user_settings(
- &mut self,
- data: SettingsFileContent,
- theme_registry: &ThemeRegistry,
- font_cache: &FontCache,
- ) {
- let mut family_changed = false;
- if let Some(value) = data.buffer_font_family {
- self.buffer_font_family_name = value;
- family_changed = true;
- }
- if let Some(value) = data.buffer_font_features {
- self.buffer_font_features = value;
- family_changed = true;
- }
- if family_changed {
- if let Some(id) = font_cache
- .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features)
- .log_err()
- {
- self.buffer_font_family = id;
- }
- }
-
- if let Some(value) = &data.theme {
- if let Some(theme) = theme_registry.get(value).log_err() {
- self.theme = theme;
- }
- }
-
- merge(&mut self.buffer_font_size, data.buffer_font_size);
- merge(
- &mut self.active_pane_magnification,
- data.active_pane_magnification,
- );
- merge(&mut self.default_buffer_font_size, data.buffer_font_size);
- merge(&mut self.cursor_blink, data.cursor_blink);
- merge(&mut self.confirm_quit, data.confirm_quit);
- merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
- merge(
- &mut self.show_completions_on_input,
- data.show_completions_on_input,
- );
- merge(&mut self.vim_mode, data.vim_mode);
- merge(&mut self.autosave, data.autosave);
- merge(&mut self.base_keymap, data.base_keymap);
- merge(&mut self.features.copilot, data.features.copilot);
-
- if let Some(copilot) = data.copilot {
- if let Some(disabled_globs) = copilot.disabled_globs {
- self.copilot.disabled_globs = disabled_globs
- .into_iter()
- .filter_map(|s| glob::Pattern::new(&s).ok())
- .collect()
- }
- }
- self.editor_overrides = data.editor;
- merge(&mut self.project_panel.dock, data.project_panel.dock);
- merge(
- &mut self.project_panel.default_width,
- data.project_panel.default_width,
- );
- self.git_overrides = data.git.unwrap_or_default();
- self.journal_overrides = data.journal;
- self.terminal_defaults.font_size = data.terminal.font_size;
- self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
- self.terminal_overrides = data.terminal;
- self.language_overrides = data.languages;
- self.telemetry_overrides = data.telemetry;
- self.lsp = data.lsp;
- merge(&mut self.auto_update, data.auto_update);
- }
-
- pub fn with_language_defaults(
- mut self,
- language_name: impl Into<Arc<str>>,
- overrides: EditorSettings,
- ) -> Self {
- self.language_defaults
- .insert(language_name.into(), overrides);
- self
- }
-
- pub fn features(&self) -> &Features {
- &self.features
- }
-
- pub fn show_copilot_suggestions(&self, language: Option<&str>, path: Option<&Path>) -> bool {
- if !self.features.copilot {
- return false;
- }
-
- if !self.copilot_enabled_for_language(language) {
- return false;
- }
-
- if let Some(path) = path {
- if !self.copilot_enabled_for_path(path) {
- return false;
- }
- }
-
- true
- }
-
- pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
- !self
- .copilot
- .disabled_globs
- .iter()
- .any(|glob| glob.matches_path(path))
- }
-
- pub fn copilot_enabled_for_language(&self, language: Option<&str>) -> bool {
- self.language_setting(language, |settings| settings.show_copilot_suggestions)
- }
-
- pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
- self.language_setting(language, |settings| settings.tab_size)
- }
-
- pub fn show_whitespaces(&self, language: Option<&str>) -> ShowWhitespaces {
- self.language_setting(language, |settings| settings.show_whitespaces)
- }
-
- pub fn hard_tabs(&self, language: Option<&str>) -> bool {
- self.language_setting(language, |settings| settings.hard_tabs)
- }
-
- pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
- self.language_setting(language, |settings| settings.soft_wrap)
- }
-
- pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
- self.language_setting(language, |settings| settings.preferred_line_length)
- }
-
- pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool {
- self.language_setting(language, |settings| {
- settings.remove_trailing_whitespace_on_save.clone()
- })
- }
-
- pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool {
- self.language_setting(language, |settings| {
- settings.ensure_final_newline_on_save.clone()
- })
- }
-
- pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
- self.language_setting(language, |settings| settings.format_on_save.clone())
- }
-
- pub fn formatter(&self, language: Option<&str>) -> Formatter {
- self.language_setting(language, |settings| settings.formatter.clone())
- }
-
- pub fn enable_language_server(&self, language: Option<&str>) -> bool {
- self.language_setting(language, |settings| settings.enable_language_server)
- }
-
- fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
- where
- F: Fn(&EditorSettings) -> Option<R>,
- {
- None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
- .or_else(|| f(&self.editor_overrides))
- .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
- .or_else(|| f(&self.editor_defaults))
- .expect("missing default")
- }
-
- pub fn git_gutter(&self) -> GitGutter {
- self.git_overrides.git_gutter.unwrap_or_else(|| {
- self.git
- .git_gutter
- .expect("git_gutter should be some by setting setup")
- })
- }
-
- pub fn telemetry(&self) -> TelemetrySettings {
- TelemetrySettings {
- diagnostics: Some(self.telemetry_diagnostics()),
- metrics: Some(self.telemetry_metrics()),
- }
- }
-
- pub fn telemetry_diagnostics(&self) -> bool {
- self.telemetry_overrides
- .diagnostics
- .or(self.telemetry_defaults.diagnostics)
- .expect("missing default")
- }
-
- pub fn telemetry_metrics(&self) -> bool {
- self.telemetry_overrides
- .metrics
- .or(self.telemetry_defaults.metrics)
- .expect("missing default")
- }
-
- fn terminal_setting<F, R>(&self, f: F) -> R
- where
- F: Fn(&TerminalSettings) -> Option<R>,
- {
- None.or_else(|| f(&self.terminal_overrides))
- .or_else(|| f(&self.terminal_defaults))
- .expect("missing default")
- }
-
- pub fn terminal_line_height(&self) -> f32 {
- self.terminal_setting(|terminal_setting| terminal_setting.line_height())
- }
-
- pub fn terminal_scroll(&self) -> AlternateScroll {
- self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.to_owned())
- }
-
- pub fn terminal_shell(&self) -> Shell {
- self.terminal_setting(|terminal_setting| terminal_setting.shell.to_owned())
- }
-
- pub fn terminal_env(&self) -> HashMap<String, String> {
- self.terminal_setting(|terminal_setting| terminal_setting.env.to_owned())
- }
-
- pub fn terminal_strategy(&self) -> WorkingDirectory {
- self.terminal_setting(|terminal_setting| terminal_setting.working_directory.to_owned())
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn test(cx: &gpui::AppContext) -> Settings {
- Settings {
- buffer_font_family_name: "Monaco".to_string(),
- buffer_font_features: Default::default(),
- buffer_font_family: cx
- .font_cache()
- .load_family(&["Monaco"], &Default::default())
- .unwrap(),
- buffer_font_size: 14.,
- active_pane_magnification: 1.,
- default_buffer_font_size: 14.,
- confirm_quit: false,
- cursor_blink: true,
- hover_popover_enabled: true,
- show_completions_on_input: true,
- show_call_status_icon: true,
- vim_mode: false,
- autosave: Autosave::Off,
- project_panel: ProjectPanelSettings {
- dock: ProjectPanelDockPosition::Left,
- default_width: 240.,
- },
- editor_defaults: EditorSettings {
- tab_size: Some(4.try_into().unwrap()),
- hard_tabs: Some(false),
- soft_wrap: Some(SoftWrap::None),
- preferred_line_length: Some(80),
- remove_trailing_whitespace_on_save: Some(true),
- ensure_final_newline_on_save: Some(true),
- format_on_save: Some(FormatOnSave::On),
- formatter: Some(Formatter::LanguageServer),
- enable_language_server: Some(true),
- show_copilot_suggestions: Some(true),
- show_whitespaces: Some(ShowWhitespaces::None),
- },
- editor_overrides: Default::default(),
- copilot: Default::default(),
- journal_defaults: Default::default(),
- journal_overrides: Default::default(),
- terminal_defaults: Default::default(),
- terminal_overrides: Default::default(),
- git: Default::default(),
- git_overrides: Default::default(),
- language_defaults: Default::default(),
- language_overrides: Default::default(),
- lsp: Default::default(),
- theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
- telemetry_defaults: TelemetrySettings {
- diagnostics: Some(true),
- metrics: Some(true),
- },
- telemetry_overrides: Default::default(),
- auto_update: true,
- base_keymap: Default::default(),
- features: Features { copilot: true },
- }
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn test_async(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| {
- let settings = Self::test(cx);
- cx.set_global(settings);
- });
- }
-}
-
-pub fn settings_file_json_schema(
- theme_names: Vec<String>,
- language_names: &[String],
-) -> serde_json::Value {
- let settings = SchemaSettings::draft07().with(|settings| {
- settings.option_add_null_type = false;
- });
- let generator = SchemaGenerator::new(settings);
-
- let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
-
- // Create a schema for a theme name.
- let theme_name_schema = SchemaObject {
- instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
- enum_values: Some(theme_names.into_iter().map(Value::String).collect()),
- ..Default::default()
- };
-
- // Create a schema for a 'languages overrides' object, associating editor
- // settings with specific langauges.
- assert!(root_schema.definitions.contains_key("EditorSettings"));
-
- let languages_object_schema = SchemaObject {
- instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
- object: Some(Box::new(ObjectValidation {
- properties: language_names
- .iter()
- .map(|name| {
- (
- name.clone(),
- Schema::new_ref("#/definitions/EditorSettings".into()),
- )
- })
- .collect(),
- ..Default::default()
- })),
- ..Default::default()
- };
-
- // Add these new schemas as definitions, and modify properties of the root
- // schema to reference them.
- root_schema.definitions.extend([
- ("ThemeName".into(), theme_name_schema.into()),
- ("Languages".into(), languages_object_schema.into()),
- ]);
- let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
-
- root_schema_object.properties.extend([
- (
- "theme".to_owned(),
- Schema::new_ref("#/definitions/ThemeName".into()),
- ),
- (
- "languages".to_owned(),
- Schema::new_ref("#/definitions/Languages".into()),
- ),
- // For backward compatibility
- (
- "language_overrides".to_owned(),
- Schema::new_ref("#/definitions/Languages".into()),
- ),
- ]);
-
- serde_json::to_value(root_schema).unwrap()
-}
-
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
- if let Some(value) = value {
- *target = value;
- }
-}
-
-pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
- Ok(serde_json::from_reader(
- json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
- )?)
-}
-
-lazy_static! {
- static ref PAIR_QUERY: Query = Query::new(
- tree_sitter_json::language(),
- "
- (pair
- key: (string) @key
- value: (_) @value)
- ",
- )
- .unwrap();
-}
-
-fn update_object_in_settings_file<'a>(
- old_object: &'a serde_json::Map<String, Value>,
- new_object: &'a serde_json::Map<String, Value>,
- text: &str,
- syntax_tree: &Tree,
- tab_size: usize,
- key_path: &mut Vec<&'a str>,
- edits: &mut Vec<(Range<usize>, String)>,
-) {
- for (key, old_value) in old_object.iter() {
- key_path.push(key);
- let new_value = new_object.get(key).unwrap_or(&Value::Null);
-
- // If the old and new values are both objects, then compare them key by key,
- // preserving the comments and formatting of the unchanged parts. Otherwise,
- // replace the old value with the new value.
- if let (Value::Object(old_sub_object), Value::Object(new_sub_object)) =
- (old_value, new_value)
- {
- update_object_in_settings_file(
- old_sub_object,
- new_sub_object,
- text,
- syntax_tree,
- tab_size,
- key_path,
- edits,
- )
- } else if old_value != new_value {
- let (range, replacement) =
- update_key_in_settings_file(text, syntax_tree, &key_path, tab_size, &new_value);
- edits.push((range, replacement));
- }
-
- key_path.pop();
- }
-}
-
-fn update_key_in_settings_file(
- text: &str,
- syntax_tree: &Tree,
- key_path: &[&str],
- tab_size: usize,
- new_value: impl Serialize,
-) -> (Range<usize>, String) {
- const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
- const LANGUAGES: &'static str = "languages";
-
- let mut cursor = tree_sitter::QueryCursor::new();
-
- let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
-
- let mut depth = 0;
- let mut last_value_range = 0..0;
- let mut first_key_start = None;
- let mut existing_value_range = 0..text.len();
- let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
- for mat in matches {
- if mat.captures.len() != 2 {
- continue;
- }
-
- let key_range = mat.captures[0].node.byte_range();
- let value_range = mat.captures[1].node.byte_range();
-
- // Don't enter sub objects until we find an exact
- // match for the current keypath
- if last_value_range.contains_inclusive(&value_range) {
- continue;
- }
-
- last_value_range = value_range.clone();
-
- if key_range.start > existing_value_range.end {
- break;
- }
-
- first_key_start.get_or_insert_with(|| key_range.start);
-
- let found_key = text
- .get(key_range.clone())
- .map(|key_text| {
- if key_path[depth] == LANGUAGES && has_language_overrides {
- return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
- } else {
- return key_text == format!("\"{}\"", key_path[depth]);
- }
- })
- .unwrap_or(false);
-
- if found_key {
- existing_value_range = value_range;
- // Reset last value range when increasing in depth
- last_value_range = existing_value_range.start..existing_value_range.start;
- depth += 1;
-
- if depth == key_path.len() {
- break;
- } else {
- first_key_start = None;
- }
- }
- }
-
- // We found the exact key we want, insert the new value
- if depth == key_path.len() {
- let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
- (existing_value_range, new_val)
- } else {
- // We have key paths, construct the sub objects
- let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
- LANGUAGE_OVERRIDES
- } else {
- key_path[depth]
- };
-
- // We don't have the key, construct the nested objects
- let mut new_value = serde_json::to_value(new_value).unwrap();
- for key in key_path[(depth + 1)..].iter().rev() {
- if has_language_overrides && key == &LANGUAGES {
- new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
- } else {
- new_value = serde_json::json!({ key.to_string(): new_value });
- }
- }
-
- if let Some(first_key_start) = first_key_start {
- let mut row = 0;
- let mut column = 0;
- for (ix, char) in text.char_indices() {
- if ix == first_key_start {
- break;
- }
- if char == '\n' {
- row += 1;
- column = 0;
- } else {
- column += char.len_utf8();
- }
- }
-
- if row > 0 {
- // depth is 0 based, but division needs to be 1 based.
- let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
- let space = ' ';
- let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
- (first_key_start..first_key_start, content)
- } else {
- let new_val = serde_json::to_string(&new_value).unwrap();
- let mut content = format!(r#""{new_key}": {new_val},"#);
- content.push(' ');
- (first_key_start..first_key_start, content)
- }
- } else {
- new_value = serde_json::json!({ new_key.to_string(): new_value });
- let indent_prefix_len = 4 * depth;
- let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
- if depth == 0 {
- new_val.push('\n');
- }
-
- (existing_value_range, new_val)
- }
- }
-}
-
-fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
- const SPACES: [u8; 32] = [b' '; 32];
-
- debug_assert!(indent_size <= SPACES.len());
- debug_assert!(indent_prefix_len <= SPACES.len());
-
- let mut output = Vec::new();
- let mut ser = serde_json::Serializer::with_formatter(
- &mut output,
- serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
- );
-
- value.serialize(&mut ser).unwrap();
- let text = String::from_utf8(output).unwrap();
-
- let mut adjusted_text = String::new();
- for (i, line) in text.split('\n').enumerate() {
- if i > 0 {
- adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
- }
- adjusted_text.push_str(line);
- adjusted_text.push('\n');
- }
- adjusted_text.pop();
- adjusted_text
-}
-
-/// Update the settings file with the given callback.
-///
-/// Returns a new JSON string and the offset where the first edit occurred.
-fn update_settings_file(
- text: &str,
- mut old_file_content: SettingsFileContent,
- tab_size: NonZeroU32,
- update: impl FnOnce(&mut SettingsFileContent),
-) -> Vec<(Range<usize>, String)> {
- let mut new_file_content = old_file_content.clone();
- update(&mut new_file_content);
-
- if new_file_content.languages.len() != old_file_content.languages.len() {
- for language in new_file_content.languages.keys() {
- old_file_content
- .languages
- .entry(language.clone())
- .or_default();
- }
- for language in old_file_content.languages.keys() {
- new_file_content
- .languages
- .entry(language.clone())
- .or_default();
- }
- }
-
- let mut parser = tree_sitter::Parser::new();
- parser.set_language(tree_sitter_json::language()).unwrap();
- let tree = parser.parse(text, None).unwrap();
-
- let old_object = to_json_object(old_file_content);
- let new_object = to_json_object(new_file_content);
- let mut key_path = Vec::new();
- let mut edits = Vec::new();
- update_object_in_settings_file(
- &old_object,
- &new_object,
- &text,
- &tree,
- tab_size.get() as usize,
- &mut key_path,
- &mut edits,
- );
- edits.sort_unstable_by_key(|e| e.0.start);
- return edits;
-}
-
-fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
- let tmp = serde_json::to_value(settings_file).unwrap();
- match tmp {
- Value::Object(map) => map,
- _ => unreachable!("SettingsFileContent represents a JSON map"),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use unindent::Unindent;
-
- fn assert_new_settings(
- old_json: String,
- update: fn(&mut SettingsFileContent),
- expected_new_json: String,
- ) {
- let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
- let edits = update_settings_file(&old_json, old_content, 4.try_into().unwrap(), update);
- let mut new_json = old_json;
- for (range, replacement) in edits.into_iter().rev() {
- new_json.replace_range(range, &replacement);
- }
- pretty_assertions::assert_eq!(new_json, expected_new_json);
- }
-
- #[test]
- fn test_update_language_overrides_copilot() {
- assert_new_settings(
- r#"
- {
- "language_overrides": {
- "JSON": {
- "show_copilot_suggestions": false
- }
- }
- }
- "#
- .unindent(),
- |settings| {
- settings.languages.insert(
- "Rust".into(),
- EditorSettings {
- show_copilot_suggestions: Some(true),
- ..Default::default()
- },
- );
- },
- r#"
- {
- "language_overrides": {
- "Rust": {
- "show_copilot_suggestions": true
- },
- "JSON": {
- "show_copilot_suggestions": false
- }
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_copilot_globs() {
- assert_new_settings(
- r#"
- {
- }
- "#
- .unindent(),
- |settings| {
- settings.copilot = Some(CopilotSettingsContent {
- disabled_globs: Some(vec![]),
- });
- },
- r#"
- {
- "copilot": {
- "disabled_globs": []
- }
- }
- "#
- .unindent(),
- );
-
- assert_new_settings(
- r#"
- {
- "copilot": {
- "disabled_globs": [
- "**/*.json"
- ]
- }
- }
- "#
- .unindent(),
- |settings| {
- settings
- .copilot
- .get_or_insert(Default::default())
- .disabled_globs
- .as_mut()
- .unwrap()
- .push(".env".into());
- },
- r#"
- {
- "copilot": {
- "disabled_globs": [
- "**/*.json",
- ".env"
- ]
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_copilot() {
- assert_new_settings(
- r#"
- {
- "languages": {
- "JSON": {
- "show_copilot_suggestions": false
- }
- }
- }
- "#
- .unindent(),
- |settings| {
- settings.editor.show_copilot_suggestions = Some(true);
- },
- r#"
- {
- "show_copilot_suggestions": true,
- "languages": {
- "JSON": {
- "show_copilot_suggestions": false
- }
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_language_copilot() {
- assert_new_settings(
- r#"
- {
- "languages": {
- "JSON": {
- "show_copilot_suggestions": false
- }
- }
- }
- "#
- .unindent(),
- |settings| {
- settings.languages.insert(
- "Rust".into(),
- EditorSettings {
- show_copilot_suggestions: Some(true),
- ..Default::default()
- },
- );
- },
- r#"
- {
- "languages": {
- "Rust": {
- "show_copilot_suggestions": true
- },
- "JSON": {
- "show_copilot_suggestions": false
- }
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_telemetry_setting_multiple_fields() {
- assert_new_settings(
- r#"
- {
- "telemetry": {
- "metrics": false,
- "diagnostics": false
- }
- }
- "#
- .unindent(),
- |settings| {
- settings.telemetry.set_diagnostics(true);
- settings.telemetry.set_metrics(true);
- },
- r#"
- {
- "telemetry": {
- "metrics": true,
- "diagnostics": true
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_telemetry_setting_weird_formatting() {
- assert_new_settings(
- r#"{
- "telemetry": { "metrics": false, "diagnostics": true }
- }"#
- .unindent(),
- |settings| settings.telemetry.set_diagnostics(false),
- r#"{
- "telemetry": { "metrics": false, "diagnostics": false }
- }"#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_telemetry_setting_other_fields() {
- assert_new_settings(
- r#"
- {
- "telemetry": {
- "metrics": false,
- "diagnostics": true
- }
- }
- "#
- .unindent(),
- |settings| settings.telemetry.set_diagnostics(false),
- r#"
- {
- "telemetry": {
- "metrics": false,
- "diagnostics": false
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_telemetry_setting_empty_telemetry() {
- assert_new_settings(
- r#"
- {
- "telemetry": {}
- }
- "#
- .unindent(),
- |settings| settings.telemetry.set_diagnostics(false),
- r#"
- {
- "telemetry": {
- "diagnostics": false
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_telemetry_setting_pre_existing() {
- assert_new_settings(
- r#"
- {
- "telemetry": {
- "diagnostics": true
- }
- }
- "#
- .unindent(),
- |settings| settings.telemetry.set_diagnostics(false),
- r#"
- {
- "telemetry": {
- "diagnostics": false
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_telemetry_setting() {
- assert_new_settings(
- "{}".into(),
- |settings| settings.telemetry.set_diagnostics(true),
- r#"
- {
- "telemetry": {
- "diagnostics": true
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_update_object_empty_doc() {
- assert_new_settings(
- "".into(),
- |settings| settings.telemetry.set_diagnostics(true),
- r#"
- {
- "telemetry": {
- "diagnostics": true
- }
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_write_theme_into_settings_with_theme() {
- assert_new_settings(
- r#"
- {
- "theme": "One Dark"
- }
- "#
- .unindent(),
- |settings| settings.theme = Some("summerfruit-light".to_string()),
- r#"
- {
- "theme": "summerfruit-light"
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_write_theme_into_empty_settings() {
- assert_new_settings(
- r#"
- {
- }
- "#
- .unindent(),
- |settings| settings.theme = Some("summerfruit-light".to_string()),
- r#"
- {
- "theme": "summerfruit-light"
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn write_key_no_document() {
- assert_new_settings(
- "".to_string(),
- |settings| settings.theme = Some("summerfruit-light".to_string()),
- r#"
- {
- "theme": "summerfruit-light"
- }
- "#
- .unindent(),
- );
- }
-
- #[test]
- fn test_write_theme_into_single_line_settings_without_theme() {
- assert_new_settings(
- r#"{ "a": "", "ok": true }"#.to_string(),
- |settings| settings.theme = Some("summerfruit-light".to_string()),
- r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#.to_string(),
- );
- }
-
- #[test]
- fn test_write_theme_pre_object_whitespace() {
- assert_new_settings(
- r#" { "a": "", "ok": true }"#.to_string(),
- |settings| settings.theme = Some("summerfruit-light".to_string()),
- r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
- );
- }
-
- #[test]
- fn test_write_theme_into_multi_line_settings_without_theme() {
- assert_new_settings(
- r#"
- {
- "a": "b"
- }
- "#
- .unindent(),
- |settings| settings.theme = Some("summerfruit-light".to_string()),
- r#"
- {
- "theme": "summerfruit-light",
- "a": "b"
- }
- "#
- .unindent(),
- );
+pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
+ match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
+ Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+ Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
}
}
@@ -1,367 +1,137 @@
-use crate::{update_settings_file, watched_json::WatchedJsonFile, Settings, SettingsFileContent};
+use crate::{settings_store::SettingsStore, Setting, DEFAULT_SETTINGS_ASSET_PATH};
use anyhow::Result;
use assets::Assets;
use fs::Fs;
-use gpui::AppContext;
-use std::{io::ErrorKind, ops::Range, path::Path, sync::Arc};
-
-// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
-// And instant updates in the Zed editor
-#[derive(Clone)]
-pub struct SettingsFile {
- path: &'static Path,
- settings_file_content: WatchedJsonFile<SettingsFileContent>,
- fs: Arc<dyn Fs>,
+use futures::{channel::mpsc, StreamExt};
+use gpui::{executor::Background, AppContext, AssetSource};
+use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
+use util::{paths, ResultExt};
+
+pub fn register<T: Setting>(cx: &mut AppContext) {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.register_setting::<T>(cx);
+ });
}
-impl SettingsFile {
- pub fn new(
- path: &'static Path,
- settings_file_content: WatchedJsonFile<SettingsFileContent>,
- fs: Arc<dyn Fs>,
- ) -> Self {
- SettingsFile {
- path,
- settings_file_content,
- fs,
- }
- }
-
- async fn load_settings(path: &Path, fs: &Arc<dyn Fs>) -> Result<String> {
- match fs.load(path).await {
- result @ Ok(_) => result,
- Err(err) => {
- if let Some(e) = err.downcast_ref::<std::io::Error>() {
- if e.kind() == ErrorKind::NotFound {
- return Ok(Settings::initial_user_settings_content(&Assets).to_string());
- }
- }
- return Err(err);
- }
- }
- }
-
- pub fn update_unsaved(
- text: &str,
- cx: &AppContext,
- update: impl FnOnce(&mut SettingsFileContent),
- ) -> Vec<(Range<usize>, String)> {
- let this = cx.global::<SettingsFile>();
- let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
- let current_file_content = this.settings_file_content.current();
- update_settings_file(&text, current_file_content, tab_size, update)
- }
-
- pub fn update(
- cx: &mut AppContext,
- update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
- ) {
- let this = cx.global::<SettingsFile>();
- let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
- let current_file_content = this.settings_file_content.current();
- let fs = this.fs.clone();
- let path = this.path.clone();
-
- cx.background()
- .spawn(async move {
- let old_text = SettingsFile::load_settings(path, &fs).await?;
- let edits = update_settings_file(&old_text, current_file_content, tab_size, update);
- let mut new_text = old_text;
- for (range, replacement) in edits.into_iter().rev() {
- new_text.replace_range(range, &replacement);
- }
- fs.atomic_write(path.to_path_buf(), new_text).await?;
- anyhow::Ok(())
- })
- .detach_and_log_err(cx)
- }
+pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
+ cx.global::<SettingsStore>().get(None)
}
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
- };
- use fs::FakeFs;
- use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext};
- use theme::ThemeRegistry;
-
- struct TestView;
-
- impl Entity for TestView {
- type Event = ();
+pub fn default_settings() -> Cow<'static, str> {
+ match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
+ Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+ Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
}
+}
- impl View for TestView {
- fn ui_name() -> &'static str {
- "TestView"
- }
-
- fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
- Empty::new().into_any()
- }
- }
-
- #[gpui::test]
- async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
- let executor = cx.background();
- let fs = FakeFs::new(executor.clone());
- let font_cache = cx.font_cache();
-
- actions!(test, [A, B]);
- // From the Atom keymap
- actions!(workspace, [ActivatePreviousPane]);
- // From the JetBrains keymap
- actions!(pane, [ActivatePrevItem]);
-
- fs.save(
- "/settings.json".as_ref(),
- &r#"
- {
- "base_keymap": "Atom"
- }
- "#
- .into(),
- Default::default(),
- )
- .await
- .unwrap();
+pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
+
+#[cfg(any(test, feature = "test-support"))]
+pub fn test_settings() -> String {
+ let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
+ default_settings().as_ref(),
+ )
+ .unwrap();
+ util::merge_non_null_json_value_into(
+ serde_json::json!({
+ "buffer_font_family": "Courier",
+ "buffer_font_features": {},
+ "buffer_font_size": 14,
+ "theme": EMPTY_THEME_NAME,
+ }),
+ &mut value,
+ );
+ value.as_object_mut().unwrap().remove("languages");
+ serde_json::to_string(&value).unwrap()
+}
- fs.save(
- "/keymap.json".as_ref(),
- &r#"
- [
- {
- "bindings": {
- "backspace": "test::A"
+pub fn watch_config_file(
+ executor: Arc<Background>,
+ fs: Arc<dyn Fs>,
+ path: PathBuf,
+) -> mpsc::UnboundedReceiver<String> {
+ let (tx, rx) = mpsc::unbounded();
+ executor
+ .spawn(async move {
+ let events = fs.watch(&path, Duration::from_millis(100)).await;
+ futures::pin_mut!(events);
+ loop {
+ if let Ok(contents) = fs.load(&path).await {
+ if !tx.unbounded_send(contents).is_ok() {
+ break;
}
}
- ]
- "#
- .into(),
- Default::default(),
- )
- .await
- .unwrap();
-
- let settings_file =
- WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
- let keymaps_file =
- WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
-
- let default_settings = cx.read(Settings::test);
-
- cx.update(|cx| {
- cx.add_global_action(|_: &A, _cx| {});
- cx.add_global_action(|_: &B, _cx| {});
- cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
- cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
- watch_files(
- default_settings,
- settings_file,
- ThemeRegistry::new((), font_cache),
- keymaps_file,
- cx,
- )
- });
-
- cx.foreground().run_until_parked();
-
- let (window_id, _view) = cx.add_window(|_| TestView);
-
- // Test loading the keymap base at all
- assert_key_bindings_for(
- window_id,
- cx,
- vec![("backspace", &A), ("k", &ActivatePreviousPane)],
- line!(),
- );
-
- // Test modifying the users keymap, while retaining the base keymap
- fs.save(
- "/keymap.json".as_ref(),
- &r#"
- [
- {
- "bindings": {
- "backspace": "test::B"
- }
+ if events.next().await.is_none() {
+ break;
}
- ]
- "#
- .into(),
- Default::default(),
- )
- .await
- .unwrap();
-
- cx.foreground().run_until_parked();
-
- assert_key_bindings_for(
- window_id,
- cx,
- vec![("backspace", &B), ("k", &ActivatePreviousPane)],
- line!(),
- );
-
- // Test modifying the base, while retaining the users keymap
- fs.save(
- "/settings.json".as_ref(),
- &r#"
- {
- "base_keymap": "JetBrains"
}
- "#
- .into(),
- Default::default(),
- )
- .await
- .unwrap();
-
- cx.foreground().run_until_parked();
-
- assert_key_bindings_for(
- window_id,
- cx,
- vec![("backspace", &B), ("[", &ActivatePrevItem)],
- line!(),
- );
- }
+ })
+ .detach();
+ rx
+}
- fn assert_key_bindings_for<'a>(
- window_id: usize,
- cx: &TestAppContext,
- actions: Vec<(&'static str, &'a dyn Action)>,
- line: u32,
- ) {
- for (key, action) in actions {
- // assert that...
- assert!(
- cx.available_actions(window_id, 0)
- .into_iter()
- .any(|(_, bound_action, b)| {
- // action names match...
- bound_action.name() == action.name()
- && bound_action.namespace() == action.namespace()
- // and key strokes contain the given key
- && b.iter()
- .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
- }),
- "On {} Failed to find {} with key binding {}",
- line,
- action.name(),
- key
- );
+pub fn handle_settings_file_changes(
+ mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
+ cx: &mut AppContext,
+) {
+ let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap();
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store
+ .set_user_settings(&user_settings_content, cx)
+ .log_err();
+ });
+ cx.spawn(move |mut cx| async move {
+ while let Some(user_settings_content) = user_settings_file_rx.next().await {
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store
+ .set_user_settings(&user_settings_content, cx)
+ .log_err();
+ });
+ cx.refresh_windows();
+ });
}
- }
-
- #[gpui::test]
- async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
- let executor = cx.background();
- let fs = FakeFs::new(executor.clone());
- let font_cache = cx.font_cache();
+ })
+ .detach();
+}
- fs.save(
- "/settings.json".as_ref(),
- &r#"
- {
- "buffer_font_size": 24,
- "soft_wrap": "editor_width",
- "tab_size": 8,
- "language_overrides": {
- "Markdown": {
- "tab_size": 2,
- "preferred_line_length": 100,
- "soft_wrap": "preferred_line_length"
- }
+async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
+ match fs.load(&paths::SETTINGS).await {
+ result @ Ok(_) => result,
+ Err(err) => {
+ if let Some(e) = err.downcast_ref::<std::io::Error>() {
+ if e.kind() == ErrorKind::NotFound {
+ return Ok(crate::initial_user_settings_content(&Assets).to_string());
}
}
- "#
- .into(),
- Default::default(),
- )
- .await
- .unwrap();
+ return Err(err);
+ }
+ }
+}
- let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
+pub fn update_settings_file<T: Setting>(
+ fs: Arc<dyn Fs>,
+ cx: &mut AppContext,
+ update: impl 'static + Send + FnOnce(&mut T::FileContent),
+) {
+ cx.spawn(|cx| async move {
+ let old_text = cx
+ .background()
+ .spawn({
+ let fs = fs.clone();
+ async move { load_settings(&fs).await }
+ })
+ .await?;
- let default_settings = cx.read(Settings::test).with_language_defaults(
- "JavaScript",
- EditorSettings {
- tab_size: Some(2.try_into().unwrap()),
- ..Default::default()
- },
- );
- cx.update(|cx| {
- watch_settings_file(
- default_settings.clone(),
- source,
- ThemeRegistry::new((), font_cache),
- cx,
- )
+ let new_text = cx.read(|cx| {
+ cx.global::<SettingsStore>()
+ .new_text_for_update::<T>(old_text, update)
});
- cx.foreground().run_until_parked();
- let settings = cx.read(|cx| cx.global::<Settings>().clone());
- assert_eq!(settings.buffer_font_size, 24.0);
-
- assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
- assert_eq!(
- settings.soft_wrap(Some("Markdown")),
- SoftWrap::PreferredLineLength
- );
- assert_eq!(
- settings.soft_wrap(Some("JavaScript")),
- SoftWrap::EditorWidth
- );
-
- assert_eq!(settings.preferred_line_length(None), 80);
- assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
- assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
-
- assert_eq!(settings.tab_size(None).get(), 8);
- assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
- assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
-
- fs.save(
- "/settings.json".as_ref(),
- &"(garbage)".into(),
- Default::default(),
- )
- .await
- .unwrap();
- // fs.remove_file("/settings.json".as_ref(), Default::default())
- // .await
- // .unwrap();
-
- cx.foreground().run_until_parked();
- let settings = cx.read(|cx| cx.global::<Settings>().clone());
- assert_eq!(settings.buffer_font_size, 24.0);
-
- assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
- assert_eq!(
- settings.soft_wrap(Some("Markdown")),
- SoftWrap::PreferredLineLength
- );
- assert_eq!(
- settings.soft_wrap(Some("JavaScript")),
- SoftWrap::EditorWidth
- );
-
- assert_eq!(settings.preferred_line_length(None), 80);
- assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
- assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
-
- assert_eq!(settings.tab_size(None).get(), 8);
- assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
- assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
-
- fs.remove_file("/settings.json".as_ref(), Default::default())
- .await
- .unwrap();
- cx.foreground().run_until_parked();
- let settings = cx.read(|cx| cx.global::<Settings>().clone());
- assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
- }
+ cx.background()
+ .spawn(async move { fs.atomic_write(paths::SETTINGS.clone(), new_text).await })
+ .await?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
}
@@ -0,0 +1,1246 @@
+use anyhow::Result;
+use collections::{btree_map, hash_map, BTreeMap, HashMap};
+use gpui::AppContext;
+use lazy_static::lazy_static;
+use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
+use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
+use smallvec::SmallVec;
+use std::{
+ any::{type_name, Any, TypeId},
+ fmt::Debug,
+ ops::Range,
+ path::Path,
+ str,
+ sync::Arc,
+};
+use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
+
+/// A value that can be defined as a user setting.
+///
+/// Settings can be loaded from a combination of multiple JSON files.
+pub trait Setting: 'static {
+ /// The name of a key within the JSON file from which this setting should
+ /// be deserialized. If this is `None`, then the setting will be deserialized
+ /// from the root object.
+ const KEY: Option<&'static str>;
+
+ /// The type that is stored in an individual JSON file.
+ type FileContent: Clone + Serialize + DeserializeOwned + JsonSchema;
+
+ /// The logic for combining together values from one or more JSON files into the
+ /// final value for this setting.
+ ///
+ /// The user values are ordered from least specific (the global settings file)
+ /// to most specific (the innermost local settings file).
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ cx: &AppContext,
+ ) -> Result<Self>
+ where
+ Self: Sized;
+
+ fn json_schema(
+ generator: &mut SchemaGenerator,
+ _: &SettingsJsonSchemaParams,
+ _: &AppContext,
+ ) -> RootSchema {
+ generator.root_schema_for::<Self::FileContent>()
+ }
+
+ fn json_merge(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ ) -> Result<Self::FileContent> {
+ let mut merged = serde_json::Value::Null;
+ for value in [default_value].iter().chain(user_values) {
+ merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
+ }
+ Ok(serde_json::from_value(merged)?)
+ }
+
+ fn load_via_json_merge(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ ) -> Result<Self>
+ where
+ Self: DeserializeOwned,
+ {
+ let mut merged = serde_json::Value::Null;
+ for value in [default_value].iter().chain(user_values) {
+ merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
+ }
+ Ok(serde_json::from_value(merged)?)
+ }
+
+ fn missing_default() -> anyhow::Error {
+ anyhow::anyhow!("missing default")
+ }
+}
+
+pub struct SettingsJsonSchemaParams<'a> {
+ pub staff_mode: bool,
+ pub language_names: &'a [String],
+}
+
+/// A set of strongly-typed setting values defined via multiple JSON files.
+#[derive(Default)]
+pub struct SettingsStore {
+ setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
+ default_deserialized_settings: Option<serde_json::Value>,
+ user_deserialized_settings: Option<serde_json::Value>,
+ local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
+ tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
+}
+
+#[derive(Debug)]
+struct SettingValue<T> {
+ global_value: Option<T>,
+ local_values: Vec<(Arc<Path>, T)>,
+}
+
+trait AnySettingValue {
+ fn key(&self) -> Option<&'static str>;
+ fn setting_type_name(&self) -> &'static str;
+ fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting>;
+ fn load_setting(
+ &self,
+ default_value: &DeserializedSetting,
+ custom: &[DeserializedSetting],
+ cx: &AppContext,
+ ) -> Result<Box<dyn Any>>;
+ fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
+ fn set_global_value(&mut self, value: Box<dyn Any>);
+ fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
+ fn json_schema(
+ &self,
+ generator: &mut SchemaGenerator,
+ _: &SettingsJsonSchemaParams,
+ cx: &AppContext,
+ ) -> RootSchema;
+}
+
+struct DeserializedSetting(Box<dyn Any>);
+
+impl SettingsStore {
+ /// Add a new type of setting to the store.
+ pub fn register_setting<T: Setting>(&mut self, cx: &AppContext) {
+ let setting_type_id = TypeId::of::<T>();
+ let entry = self.setting_values.entry(setting_type_id);
+ if matches!(entry, hash_map::Entry::Occupied(_)) {
+ return;
+ }
+
+ let setting_value = entry.or_insert(Box::new(SettingValue::<T> {
+ global_value: None,
+ local_values: Vec::new(),
+ }));
+
+ if let Some(default_settings) = &self.default_deserialized_settings {
+ if let Some(default_settings) = setting_value
+ .deserialize_setting(default_settings)
+ .log_err()
+ {
+ let mut user_values_stack = Vec::new();
+
+ if let Some(user_settings) = &self.user_deserialized_settings {
+ if let Some(user_settings) =
+ setting_value.deserialize_setting(user_settings).log_err()
+ {
+ user_values_stack = vec![user_settings];
+ }
+ }
+
+ if let Some(setting) = setting_value
+ .load_setting(&default_settings, &user_values_stack, cx)
+ .log_err()
+ {
+ setting_value.set_global_value(setting);
+ }
+ }
+ }
+ }
+
+ /// Get the value of a setting.
+ ///
+ /// Panics if the given setting type has not been registered, or if there is no
+ /// value for this setting.
+ pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
+ self.setting_values
+ .get(&TypeId::of::<T>())
+ .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+ .value_for_path(path)
+ .downcast_ref::<T>()
+ .expect("no default value for setting type")
+ }
+
+ /// Override the global value for a setting.
+ ///
+ /// The given value will be overwritten if the user settings file changes.
+ pub fn override_global<T: Setting>(&mut self, value: T) {
+ self.setting_values
+ .get_mut(&TypeId::of::<T>())
+ .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+ .set_global_value(Box::new(value))
+ }
+
+ /// Get the user's settings as a raw JSON value.
+ ///
+ /// This is only for debugging and reporting. For user-facing functionality,
+ /// use the typed setting interface.
+ pub fn untyped_user_settings(&self) -> &serde_json::Value {
+ self.user_deserialized_settings
+ .as_ref()
+ .unwrap_or(&serde_json::Value::Null)
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn test(cx: &AppContext) -> Self {
+ let mut this = Self::default();
+ this.set_default_settings(&crate::test_settings(), cx)
+ .unwrap();
+ this.set_user_settings("{}", cx).unwrap();
+ this
+ }
+
+ /// Update the value of a setting in the user's global configuration.
+ ///
+ /// This is only for tests. Normally, settings are only loaded from
+ /// JSON files.
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn update_user_settings<T: Setting>(
+ &mut self,
+ cx: &AppContext,
+ update: impl FnOnce(&mut T::FileContent),
+ ) {
+ if self.user_deserialized_settings.is_none() {
+ self.set_user_settings("{}", cx).unwrap();
+ }
+ let old_text =
+ serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
+ let new_text = self.new_text_for_update::<T>(old_text, update);
+ self.set_user_settings(&new_text, cx).unwrap();
+ }
+
+ /// Update the value of a setting in a JSON file, returning the new text
+ /// for that JSON file.
+ pub fn new_text_for_update<T: Setting>(
+ &self,
+ old_text: String,
+ update: impl FnOnce(&mut T::FileContent),
+ ) -> String {
+ let edits = self.edits_for_update::<T>(&old_text, update);
+ let mut new_text = old_text;
+ for (range, replacement) in edits.into_iter() {
+ new_text.replace_range(range, &replacement);
+ }
+ new_text
+ }
+
+ /// Update the value of a setting in a JSON file, returning a list
+ /// of edits to apply to the JSON file.
+ pub fn edits_for_update<T: Setting>(
+ &self,
+ text: &str,
+ update: impl FnOnce(&mut T::FileContent),
+ ) -> Vec<(Range<usize>, String)> {
+ let setting_type_id = TypeId::of::<T>();
+
+ let old_content = self
+ .setting_values
+ .get(&setting_type_id)
+ .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+ .deserialize_setting(
+ self.user_deserialized_settings
+ .as_ref()
+ .expect("no user settings loaded"),
+ )
+ .unwrap_or_else(|e| {
+ panic!(
+ "could not deserialize setting type {} from user settings: {}",
+ type_name::<T>(),
+ e
+ )
+ })
+ .0
+ .downcast::<T::FileContent>()
+ .unwrap();
+ let mut new_content = old_content.clone();
+ update(&mut new_content);
+
+ let old_value = &serde_json::to_value(&old_content).unwrap();
+ let new_value = serde_json::to_value(new_content).unwrap();
+
+ let mut key_path = Vec::new();
+ if let Some(key) = T::KEY {
+ key_path.push(key);
+ }
+
+ let mut edits = Vec::new();
+ let tab_size = self.json_tab_size();
+ let mut text = text.to_string();
+ update_value_in_json_text(
+ &mut text,
+ &mut key_path,
+ tab_size,
+ &old_value,
+ &new_value,
+ &mut edits,
+ );
+ return edits;
+ }
+
+ /// Configure the tab sized when updating JSON files.
+ pub fn set_json_tab_size_callback<T: Setting>(
+ &mut self,
+ get_tab_size: fn(&T) -> Option<usize>,
+ ) {
+ self.tab_size_callback = Some((
+ TypeId::of::<T>(),
+ Box::new(move |value| get_tab_size(value.downcast_ref::<T>().unwrap())),
+ ));
+ }
+
+ fn json_tab_size(&self) -> usize {
+ const DEFAULT_JSON_TAB_SIZE: usize = 2;
+
+ if let Some((setting_type_id, callback)) = &self.tab_size_callback {
+ let setting_value = self.setting_values.get(setting_type_id).unwrap();
+ let value = setting_value.value_for_path(None);
+ if let Some(value) = callback(value) {
+ return value;
+ }
+ }
+
+ DEFAULT_JSON_TAB_SIZE
+ }
+
+ /// Set the default settings via a JSON string.
+ ///
+ /// The string should contain a JSON object with a default value for every setting.
+ pub fn set_default_settings(
+ &mut self,
+ default_settings_content: &str,
+ cx: &AppContext,
+ ) -> Result<()> {
+ self.default_deserialized_settings =
+ Some(parse_json_with_comments(default_settings_content)?);
+ self.recompute_values(None, cx)?;
+ Ok(())
+ }
+
+ /// Set the user settings via a JSON string.
+ pub fn set_user_settings(
+ &mut self,
+ user_settings_content: &str,
+ cx: &AppContext,
+ ) -> Result<()> {
+ self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?);
+ self.recompute_values(None, cx)?;
+ Ok(())
+ }
+
+ /// Add or remove a set of local settings via a JSON string.
+ pub fn set_local_settings(
+ &mut self,
+ path: Arc<Path>,
+ settings_content: Option<&str>,
+ cx: &AppContext,
+ ) -> Result<()> {
+ if let Some(content) = settings_content {
+ self.local_deserialized_settings
+ .insert(path.clone(), parse_json_with_comments(content)?);
+ } else {
+ self.local_deserialized_settings.remove(&path);
+ }
+ self.recompute_values(Some(&path), cx)?;
+ Ok(())
+ }
+
+ pub fn json_schema(
+ &self,
+ schema_params: &SettingsJsonSchemaParams,
+ cx: &AppContext,
+ ) -> serde_json::Value {
+ use schemars::{
+ gen::SchemaSettings,
+ schema::{Schema, SchemaObject},
+ };
+
+ let settings = SchemaSettings::draft07().with(|settings| {
+ settings.option_add_null_type = false;
+ });
+ let mut generator = SchemaGenerator::new(settings);
+ let mut combined_schema = RootSchema::default();
+
+ for setting_value in self.setting_values.values() {
+ let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx);
+ combined_schema
+ .definitions
+ .extend(setting_schema.definitions);
+
+ let target_schema = if let Some(key) = setting_value.key() {
+ let key_schema = combined_schema
+ .schema
+ .object()
+ .properties
+ .entry(key.to_string())
+ .or_insert_with(|| Schema::Object(SchemaObject::default()));
+ if let Schema::Object(key_schema) = key_schema {
+ key_schema
+ } else {
+ continue;
+ }
+ } else {
+ &mut combined_schema.schema
+ };
+
+ merge_schema(target_schema, setting_schema.schema);
+ }
+
+ fn merge_schema(target: &mut SchemaObject, source: SchemaObject) {
+ if let Some(source) = source.object {
+ let target_properties = &mut target.object().properties;
+ for (key, value) in source.properties {
+ match target_properties.entry(key) {
+ btree_map::Entry::Vacant(e) => {
+ e.insert(value);
+ }
+ btree_map::Entry::Occupied(e) => {
+ if let (Schema::Object(target), Schema::Object(src)) =
+ (e.into_mut(), value)
+ {
+ merge_schema(target, src);
+ }
+ }
+ }
+ }
+ }
+
+ overwrite(&mut target.instance_type, source.instance_type);
+ overwrite(&mut target.string, source.string);
+ overwrite(&mut target.number, source.number);
+ overwrite(&mut target.reference, source.reference);
+ overwrite(&mut target.array, source.array);
+ overwrite(&mut target.enum_values, source.enum_values);
+
+ fn overwrite<T>(target: &mut Option<T>, source: Option<T>) {
+ if let Some(source) = source {
+ *target = Some(source);
+ }
+ }
+ }
+
+ serde_json::to_value(&combined_schema).unwrap()
+ }
+
+ fn recompute_values(
+ &mut self,
+ changed_local_path: Option<&Path>,
+ cx: &AppContext,
+ ) -> Result<()> {
+ // Reload the global and local values for every setting.
+ let mut user_settings_stack = Vec::<DeserializedSetting>::new();
+ let mut paths_stack = Vec::<Option<&Path>>::new();
+ for setting_value in self.setting_values.values_mut() {
+ if let Some(default_settings) = &self.default_deserialized_settings {
+ let default_settings = setting_value.deserialize_setting(default_settings)?;
+
+ user_settings_stack.clear();
+ paths_stack.clear();
+
+ if let Some(user_settings) = &self.user_deserialized_settings {
+ if let Some(user_settings) =
+ setting_value.deserialize_setting(user_settings).log_err()
+ {
+ user_settings_stack.push(user_settings);
+ paths_stack.push(None);
+ }
+ }
+
+ // If the global settings file changed, reload the global value for the field.
+ if changed_local_path.is_none() {
+ setting_value.set_global_value(setting_value.load_setting(
+ &default_settings,
+ &user_settings_stack,
+ cx,
+ )?);
+ }
+
+ // Reload the local values for the setting.
+ for (path, local_settings) in &self.local_deserialized_settings {
+ // Build a stack of all of the local values for that setting.
+ while let Some(prev_path) = paths_stack.last() {
+ if let Some(prev_path) = prev_path {
+ if !path.starts_with(prev_path) {
+ paths_stack.pop();
+ user_settings_stack.pop();
+ continue;
+ }
+ }
+ break;
+ }
+
+ if let Some(local_settings) =
+ setting_value.deserialize_setting(&local_settings).log_err()
+ {
+ paths_stack.push(Some(path.as_ref()));
+ user_settings_stack.push(local_settings);
+
+ // If a local settings file changed, then avoid recomputing local
+ // settings for any path outside of that directory.
+ if changed_local_path.map_or(false, |changed_local_path| {
+ !path.starts_with(changed_local_path)
+ }) {
+ continue;
+ }
+
+ setting_value.set_local_value(
+ path.clone(),
+ setting_value.load_setting(
+ &default_settings,
+ &user_settings_stack,
+ cx,
+ )?,
+ );
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+impl<T: Setting> AnySettingValue for SettingValue<T> {
+ fn key(&self) -> Option<&'static str> {
+ T::KEY
+ }
+
+ fn setting_type_name(&self) -> &'static str {
+ type_name::<T>()
+ }
+
+ fn load_setting(
+ &self,
+ default_value: &DeserializedSetting,
+ user_values: &[DeserializedSetting],
+ cx: &AppContext,
+ ) -> Result<Box<dyn Any>> {
+ let default_value = default_value.0.downcast_ref::<T::FileContent>().unwrap();
+ let values: SmallVec<[&T::FileContent; 6]> = user_values
+ .iter()
+ .map(|value| value.0.downcast_ref().unwrap())
+ .collect();
+ Ok(Box::new(T::load(default_value, &values, cx)?))
+ }
+
+ fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
+ if let Some(key) = T::KEY {
+ json = json.get(key).unwrap_or(&serde_json::Value::Null);
+ }
+ let value = T::FileContent::deserialize(json)?;
+ Ok(DeserializedSetting(Box::new(value)))
+ }
+
+ fn value_for_path(&self, path: Option<&Path>) -> &dyn Any {
+ if let Some(path) = path {
+ for (settings_path, value) in self.local_values.iter().rev() {
+ if path.starts_with(&settings_path) {
+ return value;
+ }
+ }
+ }
+ self.global_value
+ .as_ref()
+ .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name()))
+ }
+
+ fn set_global_value(&mut self, value: Box<dyn Any>) {
+ self.global_value = Some(*value.downcast().unwrap());
+ }
+
+ fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>) {
+ let value = *value.downcast().unwrap();
+ match self.local_values.binary_search_by_key(&&path, |e| &e.0) {
+ Ok(ix) => self.local_values[ix].1 = value,
+ Err(ix) => self.local_values.insert(ix, (path, value)),
+ }
+ }
+
+ fn json_schema(
+ &self,
+ generator: &mut SchemaGenerator,
+ params: &SettingsJsonSchemaParams,
+ cx: &AppContext,
+ ) -> RootSchema {
+ T::json_schema(generator, params, cx)
+ }
+}
+
+// impl Debug for SettingsStore {
+// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+// return f
+// .debug_struct("SettingsStore")
+// .field(
+// "setting_value_sets_by_type",
+// &self
+// .setting_values
+// .values()
+// .map(|set| (set.setting_type_name(), set))
+// .collect::<HashMap<_, _>>(),
+// )
+// .finish_non_exhaustive();
+// }
+// }
+
+fn update_value_in_json_text<'a>(
+ text: &mut String,
+ key_path: &mut Vec<&'a str>,
+ tab_size: usize,
+ old_value: &'a serde_json::Value,
+ new_value: &'a serde_json::Value,
+ edits: &mut Vec<(Range<usize>, String)>,
+) {
+ // If the old and new values are both objects, then compare them key by key,
+ // preserving the comments and formatting of the unchanged parts. Otherwise,
+ // replace the old value with the new value.
+ if let (serde_json::Value::Object(old_object), serde_json::Value::Object(new_object)) =
+ (old_value, new_value)
+ {
+ for (key, old_sub_value) in old_object.iter() {
+ key_path.push(key);
+ let new_sub_value = new_object.get(key).unwrap_or(&serde_json::Value::Null);
+ update_value_in_json_text(
+ text,
+ key_path,
+ tab_size,
+ old_sub_value,
+ new_sub_value,
+ edits,
+ );
+ key_path.pop();
+ }
+ for (key, new_sub_value) in new_object.iter() {
+ key_path.push(key);
+ if !old_object.contains_key(key) {
+ update_value_in_json_text(
+ text,
+ key_path,
+ tab_size,
+ &serde_json::Value::Null,
+ new_sub_value,
+ edits,
+ );
+ }
+ key_path.pop();
+ }
+ } else if old_value != new_value {
+ let (range, replacement) =
+ replace_value_in_json_text(text, &key_path, tab_size, &new_value);
+ text.replace_range(range.clone(), &replacement);
+ edits.push((range, replacement));
+ }
+}
+
+fn replace_value_in_json_text(
+ text: &str,
+ key_path: &[&str],
+ tab_size: usize,
+ new_value: impl Serialize,
+) -> (Range<usize>, String) {
+ const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
+ const LANGUAGES: &'static str = "languages";
+
+ lazy_static! {
+ static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new(
+ tree_sitter_json::language(),
+ "(pair key: (string) @key value: (_) @value)",
+ )
+ .unwrap();
+ }
+
+ let mut parser = tree_sitter::Parser::new();
+ parser.set_language(tree_sitter_json::language()).unwrap();
+ let syntax_tree = parser.parse(text, None).unwrap();
+
+ let mut cursor = tree_sitter::QueryCursor::new();
+
+ let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
+
+ let mut depth = 0;
+ let mut last_value_range = 0..0;
+ let mut first_key_start = None;
+ let mut existing_value_range = 0..text.len();
+ let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
+ for mat in matches {
+ if mat.captures.len() != 2 {
+ continue;
+ }
+
+ let key_range = mat.captures[0].node.byte_range();
+ let value_range = mat.captures[1].node.byte_range();
+
+ // Don't enter sub objects until we find an exact
+ // match for the current keypath
+ if last_value_range.contains_inclusive(&value_range) {
+ continue;
+ }
+
+ last_value_range = value_range.clone();
+
+ if key_range.start > existing_value_range.end {
+ break;
+ }
+
+ first_key_start.get_or_insert_with(|| key_range.start);
+
+ let found_key = text
+ .get(key_range.clone())
+ .map(|key_text| {
+ if key_path[depth] == LANGUAGES && has_language_overrides {
+ return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
+ } else {
+ return key_text == format!("\"{}\"", key_path[depth]);
+ }
+ })
+ .unwrap_or(false);
+
+ if found_key {
+ existing_value_range = value_range;
+ // Reset last value range when increasing in depth
+ last_value_range = existing_value_range.start..existing_value_range.start;
+ depth += 1;
+
+ if depth == key_path.len() {
+ break;
+ } else {
+ first_key_start = None;
+ }
+ }
+ }
+
+ // We found the exact key we want, insert the new value
+ if depth == key_path.len() {
+ let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
+ (existing_value_range, new_val)
+ } else {
+ // We have key paths, construct the sub objects
+ let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
+ LANGUAGE_OVERRIDES
+ } else {
+ key_path[depth]
+ };
+
+ // We don't have the key, construct the nested objects
+ let mut new_value = serde_json::to_value(new_value).unwrap();
+ for key in key_path[(depth + 1)..].iter().rev() {
+ if has_language_overrides && key == &LANGUAGES {
+ new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
+ } else {
+ new_value = serde_json::json!({ key.to_string(): new_value });
+ }
+ }
+
+ if let Some(first_key_start) = first_key_start {
+ let mut row = 0;
+ let mut column = 0;
+ for (ix, char) in text.char_indices() {
+ if ix == first_key_start {
+ break;
+ }
+ if char == '\n' {
+ row += 1;
+ column = 0;
+ } else {
+ column += char.len_utf8();
+ }
+ }
+
+ if row > 0 {
+ // depth is 0 based, but division needs to be 1 based.
+ let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
+ let space = ' ';
+ let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
+ (first_key_start..first_key_start, content)
+ } else {
+ let new_val = serde_json::to_string(&new_value).unwrap();
+ let mut content = format!(r#""{new_key}": {new_val},"#);
+ content.push(' ');
+ (first_key_start..first_key_start, content)
+ }
+ } else {
+ new_value = serde_json::json!({ new_key.to_string(): new_value });
+ let indent_prefix_len = 4 * depth;
+ let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
+ if depth == 0 {
+ new_val.push('\n');
+ }
+
+ (existing_value_range, new_val)
+ }
+ }
+}
+
+fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
+ const SPACES: [u8; 32] = [b' '; 32];
+
+ debug_assert!(indent_size <= SPACES.len());
+ debug_assert!(indent_prefix_len <= SPACES.len());
+
+ let mut output = Vec::new();
+ let mut ser = serde_json::Serializer::with_formatter(
+ &mut output,
+ serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
+ );
+
+ value.serialize(&mut ser).unwrap();
+ let text = String::from_utf8(output).unwrap();
+
+ let mut adjusted_text = String::new();
+ for (i, line) in text.split('\n').enumerate() {
+ if i > 0 {
+ adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
+ }
+ adjusted_text.push_str(line);
+ adjusted_text.push('\n');
+ }
+ adjusted_text.pop();
+ adjusted_text
+}
+
+pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
+ Ok(serde_json::from_reader(
+ json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
+ )?)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_derive::Deserialize;
+ use unindent::Unindent;
+
+ #[gpui::test]
+ fn test_settings_store_basic(cx: &mut AppContext) {
+ let mut store = SettingsStore::default();
+ store.register_setting::<UserSettings>(cx);
+ store.register_setting::<TurboSetting>(cx);
+ store.register_setting::<MultiKeySettings>(cx);
+
+ // error - missing required field in default settings
+ store
+ .set_default_settings(
+ r#"{
+ "user": {
+ "name": "John Doe",
+ "age": 30,
+ "staff": false
+ }
+ }"#,
+ cx,
+ )
+ .unwrap_err();
+
+ // error - type error in default settings
+ store
+ .set_default_settings(
+ r#"{
+ "turbo": "the-wrong-type",
+ "user": {
+ "name": "John Doe",
+ "age": 30,
+ "staff": false
+ }
+ }"#,
+ cx,
+ )
+ .unwrap_err();
+
+ // valid default settings.
+ store
+ .set_default_settings(
+ r#"{
+ "turbo": false,
+ "user": {
+ "name": "John Doe",
+ "age": 30,
+ "staff": false
+ }
+ }"#,
+ cx,
+ )
+ .unwrap();
+
+ assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
+ assert_eq!(
+ store.get::<UserSettings>(None),
+ &UserSettings {
+ name: "John Doe".to_string(),
+ age: 30,
+ staff: false,
+ }
+ );
+ assert_eq!(
+ store.get::<MultiKeySettings>(None),
+ &MultiKeySettings {
+ key1: String::new(),
+ key2: String::new(),
+ }
+ );
+
+ store
+ .set_user_settings(
+ r#"{
+ "turbo": true,
+ "user": { "age": 31 },
+ "key1": "a"
+ }"#,
+ cx,
+ )
+ .unwrap();
+
+ assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(true));
+ assert_eq!(
+ store.get::<UserSettings>(None),
+ &UserSettings {
+ name: "John Doe".to_string(),
+ age: 31,
+ staff: false
+ }
+ );
+
+ store
+ .set_local_settings(
+ Path::new("/root1").into(),
+ Some(r#"{ "user": { "staff": true } }"#),
+ cx,
+ )
+ .unwrap();
+ store
+ .set_local_settings(
+ Path::new("/root1/subdir").into(),
+ Some(r#"{ "user": { "name": "Jane Doe" } }"#),
+ cx,
+ )
+ .unwrap();
+
+ store
+ .set_local_settings(
+ Path::new("/root2").into(),
+ Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
+ cx,
+ )
+ .unwrap();
+
+ assert_eq!(
+ store.get::<UserSettings>(Some(Path::new("/root1/something"))),
+ &UserSettings {
+ name: "John Doe".to_string(),
+ age: 31,
+ staff: true
+ }
+ );
+ assert_eq!(
+ store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))),
+ &UserSettings {
+ name: "Jane Doe".to_string(),
+ age: 31,
+ staff: true
+ }
+ );
+ assert_eq!(
+ store.get::<UserSettings>(Some(Path::new("/root2/something"))),
+ &UserSettings {
+ name: "John Doe".to_string(),
+ age: 42,
+ staff: false
+ }
+ );
+ assert_eq!(
+ store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))),
+ &MultiKeySettings {
+ key1: "a".to_string(),
+ key2: "b".to_string(),
+ }
+ );
+ }
+
+ #[gpui::test]
+ fn test_setting_store_assign_json_before_register(cx: &mut AppContext) {
+ let mut store = SettingsStore::default();
+ store
+ .set_default_settings(
+ r#"{
+ "turbo": true,
+ "user": {
+ "name": "John Doe",
+ "age": 30,
+ "staff": false
+ },
+ "key1": "x"
+ }"#,
+ cx,
+ )
+ .unwrap();
+ store
+ .set_user_settings(r#"{ "turbo": false }"#, cx)
+ .unwrap();
+ store.register_setting::<UserSettings>(cx);
+ store.register_setting::<TurboSetting>(cx);
+
+ assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
+ assert_eq!(
+ store.get::<UserSettings>(None),
+ &UserSettings {
+ name: "John Doe".to_string(),
+ age: 30,
+ staff: false,
+ }
+ );
+
+ store.register_setting::<MultiKeySettings>(cx);
+ assert_eq!(
+ store.get::<MultiKeySettings>(None),
+ &MultiKeySettings {
+ key1: "x".into(),
+ key2: String::new(),
+ }
+ );
+ }
+
+ #[gpui::test]
+ fn test_setting_store_update(cx: &mut AppContext) {
+ let mut store = SettingsStore::default();
+ store.register_setting::<MultiKeySettings>(cx);
+ store.register_setting::<UserSettings>(cx);
+ store.register_setting::<LanguageSettings>(cx);
+
+ // entries added and updated
+ check_settings_update::<LanguageSettings>(
+ &mut store,
+ r#"{
+ "languages": {
+ "JSON": {
+ "is_enabled": true
+ }
+ }
+ }"#
+ .unindent(),
+ |settings| {
+ settings.languages.get_mut("JSON").unwrap().is_enabled = false;
+ settings
+ .languages
+ .insert("Rust".into(), LanguageSettingEntry { is_enabled: true });
+ },
+ r#"{
+ "languages": {
+ "Rust": {
+ "is_enabled": true
+ },
+ "JSON": {
+ "is_enabled": false
+ }
+ }
+ }"#
+ .unindent(),
+ cx,
+ );
+
+ // weird formatting
+ check_settings_update::<UserSettings>(
+ &mut store,
+ r#"{
+ "user": { "age": 36, "name": "Max", "staff": true }
+ }"#
+ .unindent(),
+ |settings| settings.age = Some(37),
+ r#"{
+ "user": { "age": 37, "name": "Max", "staff": true }
+ }"#
+ .unindent(),
+ cx,
+ );
+
+ // single-line formatting, other keys
+ check_settings_update::<MultiKeySettings>(
+ &mut store,
+ r#"{ "one": 1, "two": 2 }"#.unindent(),
+ |settings| settings.key1 = Some("x".into()),
+ r#"{ "key1": "x", "one": 1, "two": 2 }"#.unindent(),
+ cx,
+ );
+
+ // empty object
+ check_settings_update::<UserSettings>(
+ &mut store,
+ r#"{
+ "user": {}
+ }"#
+ .unindent(),
+ |settings| settings.age = Some(37),
+ r#"{
+ "user": {
+ "age": 37
+ }
+ }"#
+ .unindent(),
+ cx,
+ );
+
+ // no content
+ check_settings_update::<UserSettings>(
+ &mut store,
+ r#""#.unindent(),
+ |settings| settings.age = Some(37),
+ r#"{
+ "user": {
+ "age": 37
+ }
+ }
+ "#
+ .unindent(),
+ cx,
+ );
+ }
+
+ fn check_settings_update<T: Setting>(
+ store: &mut SettingsStore,
+ old_json: String,
+ update: fn(&mut T::FileContent),
+ expected_new_json: String,
+ cx: &mut AppContext,
+ ) {
+ store.set_user_settings(&old_json, cx).ok();
+ let edits = store.edits_for_update::<T>(&old_json, update);
+ let mut new_json = old_json;
+ for (range, replacement) in edits.into_iter() {
+ new_json.replace_range(range, &replacement);
+ }
+ pretty_assertions::assert_eq!(new_json, expected_new_json);
+ }
+
+ #[derive(Debug, PartialEq, Deserialize)]
+ struct UserSettings {
+ name: String,
+ age: u32,
+ staff: bool,
+ }
+
+ #[derive(Clone, Serialize, Deserialize, JsonSchema)]
+ struct UserSettingsJson {
+ name: Option<String>,
+ age: Option<u32>,
+ staff: Option<bool>,
+ }
+
+ impl Setting for UserSettings {
+ const KEY: Option<&'static str> = Some("user");
+ type FileContent = UserSettingsJson;
+
+ fn load(
+ default_value: &UserSettingsJson,
+ user_values: &[&UserSettingsJson],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+ }
+
+ #[derive(Debug, Deserialize, PartialEq)]
+ struct TurboSetting(bool);
+
+ impl Setting for TurboSetting {
+ const KEY: Option<&'static str> = Some("turbo");
+ type FileContent = Option<bool>;
+
+ fn load(
+ default_value: &Option<bool>,
+ user_values: &[&Option<bool>],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+ }
+
+ #[derive(Clone, Debug, PartialEq, Deserialize)]
+ struct MultiKeySettings {
+ #[serde(default)]
+ key1: String,
+ #[serde(default)]
+ key2: String,
+ }
+
+ #[derive(Clone, Serialize, Deserialize, JsonSchema)]
+ struct MultiKeySettingsJson {
+ key1: Option<String>,
+ key2: Option<String>,
+ }
+
+ impl Setting for MultiKeySettings {
+ const KEY: Option<&'static str> = None;
+
+ type FileContent = MultiKeySettingsJson;
+
+ fn load(
+ default_value: &MultiKeySettingsJson,
+ user_values: &[&MultiKeySettingsJson],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+ }
+
+ #[derive(Debug, Deserialize)]
+ struct JournalSettings {
+ pub path: String,
+ pub hour_format: HourFormat,
+ }
+
+ #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+ #[serde(rename_all = "snake_case")]
+ enum HourFormat {
+ Hour12,
+ Hour24,
+ }
+
+ #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+ struct JournalSettingsJson {
+ pub path: Option<String>,
+ pub hour_format: Option<HourFormat>,
+ }
+
+ impl Setting for JournalSettings {
+ const KEY: Option<&'static str> = Some("journal");
+
+ type FileContent = JournalSettingsJson;
+
+ fn load(
+ default_value: &JournalSettingsJson,
+ user_values: &[&JournalSettingsJson],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+ }
+
+ #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+ struct LanguageSettings {
+ #[serde(default)]
+ languages: HashMap<String, LanguageSettingEntry>,
+ }
+
+ #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+ struct LanguageSettingEntry {
+ is_enabled: bool,
+ }
+
+ impl Setting for LanguageSettings {
+ const KEY: Option<&'static str> = None;
+
+ type FileContent = Self;
+
+ fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+ }
+}
@@ -1,126 +0,0 @@
-use fs::Fs;
-use futures::StreamExt;
-use gpui::{executor, AppContext};
-use postage::sink::Sink as _;
-use postage::{prelude::Stream, watch};
-use serde::Deserialize;
-
-use std::{path::Path, sync::Arc, time::Duration};
-use theme::ThemeRegistry;
-use util::ResultExt;
-
-use crate::{parse_json_with_comments, KeymapFileContent, Settings, SettingsFileContent};
-
-#[derive(Clone)]
-pub struct WatchedJsonFile<T>(pub watch::Receiver<T>);
-
-impl<T> WatchedJsonFile<T>
-where
- T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
-{
- pub async fn new(
- fs: Arc<dyn Fs>,
- executor: &executor::Background,
- path: impl Into<Arc<Path>>,
- ) -> Self {
- let path = path.into();
- let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
- let mut events = fs.watch(&path, Duration::from_millis(500)).await;
- let (mut tx, rx) = watch::channel_with(settings);
- executor
- .spawn(async move {
- while events.next().await.is_some() {
- if let Some(settings) = Self::load(fs.clone(), &path).await {
- if tx.send(settings).await.is_err() {
- break;
- }
- }
- }
- })
- .detach();
- Self(rx)
- }
-
- ///Loads the given watched JSON file. In the special case that the file is
- ///empty (ignoring whitespace) or is not a file, this will return T::default()
- async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
- if !fs.is_file(path).await {
- return Some(T::default());
- }
-
- fs.load(path).await.log_err().and_then(|data| {
- if data.trim().is_empty() {
- Some(T::default())
- } else {
- parse_json_with_comments(&data).log_err()
- }
- })
- }
-
- pub fn current(&self) -> T {
- self.0.borrow().clone()
- }
-}
-
-pub fn watch_files(
- defaults: Settings,
- settings_file: WatchedJsonFile<SettingsFileContent>,
- theme_registry: Arc<ThemeRegistry>,
- keymap_file: WatchedJsonFile<KeymapFileContent>,
- cx: &mut AppContext,
-) {
- watch_settings_file(defaults, settings_file, theme_registry, cx);
- watch_keymap_file(keymap_file, cx);
-}
-
-pub(crate) fn watch_settings_file(
- defaults: Settings,
- mut file: WatchedJsonFile<SettingsFileContent>,
- theme_registry: Arc<ThemeRegistry>,
- cx: &mut AppContext,
-) {
- settings_updated(&defaults, file.0.borrow().clone(), &theme_registry, cx);
- cx.spawn(|mut cx| async move {
- while let Some(content) = file.0.recv().await {
- cx.update(|cx| settings_updated(&defaults, content, &theme_registry, cx));
- }
- })
- .detach();
-}
-
-fn keymap_updated(content: KeymapFileContent, cx: &mut AppContext) {
- cx.clear_bindings();
- KeymapFileContent::load_defaults(cx);
- content.add_to_cx(cx).log_err();
-}
-
-fn settings_updated(
- defaults: &Settings,
- content: SettingsFileContent,
- theme_registry: &Arc<ThemeRegistry>,
- cx: &mut AppContext,
-) {
- let mut settings = defaults.clone();
- settings.set_user_settings(content, theme_registry, cx.font_cache());
- cx.set_global(settings);
- cx.refresh_windows();
-}
-
-fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut AppContext) {
- cx.spawn(|mut cx| async move {
- let mut settings_subscription = None;
- while let Some(content) = file.0.recv().await {
- cx.update(|cx| {
- let old_base_keymap = cx.global::<Settings>().base_keymap;
- keymap_updated(content.clone(), cx);
- settings_subscription = Some(cx.observe_global::<Settings, _>(move |cx| {
- let settings = cx.global::<Settings>();
- if settings.base_keymap != old_base_keymap {
- keymap_updated(content.clone(), cx);
- }
- }));
- });
- }
- })
- .detach();
-}
@@ -5,7 +5,7 @@ use arrayvec::ArrayVec;
pub use cursor::{Cursor, FilterCursor, Iter};
use std::marker::PhantomData;
use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc};
-pub use tree_map::{TreeMap, TreeSet};
+pub use tree_map::{MapSeekTarget, TreeMap, TreeSet};
#[cfg(test)]
const TREE_BASE: usize = 2;
@@ -1,14 +1,14 @@
use std::{cmp::Ordering, fmt::Debug};
-use crate::{Bias, Dimension, Item, KeyedItem, SeekTarget, SumTree, Summary};
+use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary};
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
where
K: Clone + Debug + Default + Ord,
V: Clone + Debug;
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MapEntry<K, V> {
key: K,
value: V,
@@ -73,6 +73,17 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
removed
}
+ pub fn remove_range(&mut self, start: &impl MapSeekTarget<K>, end: &impl MapSeekTarget<K>) {
+ let start = MapSeekTargetAdaptor(start);
+ let end = MapSeekTargetAdaptor(end);
+ let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
+ let mut new_tree = cursor.slice(&start, Bias::Left, &());
+ cursor.seek(&end, Bias::Left, &());
+ new_tree.push_tree(cursor.suffix(&()), &());
+ drop(cursor);
+ self.0 = new_tree;
+ }
+
/// Returns the key-value pair with the greatest key less than or equal to the given key.
pub fn closest(&self, key: &K) -> Option<(&K, &V)> {
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
@@ -82,6 +93,16 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
cursor.item().map(|item| (&item.key, &item.value))
}
+ pub fn iter_from<'a>(&'a self, from: &'a K) -> impl Iterator<Item = (&K, &V)> + '_ {
+ let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
+ let from_key = MapKeyRef(Some(from));
+ cursor.seek(&from_key, Bias::Left, &());
+
+ cursor
+ .into_iter()
+ .map(|map_entry| (&map_entry.key, &map_entry.value))
+ }
+
pub fn update<F, T>(&mut self, key: &K, f: F) -> Option<T>
where
F: FnOnce(&mut V) -> T,
@@ -125,6 +146,45 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
pub fn values(&self) -> impl Iterator<Item = &V> + '_ {
self.0.iter().map(|entry| &entry.value)
}
+
+ pub fn insert_tree(&mut self, other: TreeMap<K, V>) {
+ let edits = other
+ .iter()
+ .map(|(key, value)| {
+ Edit::Insert(MapEntry {
+ key: key.to_owned(),
+ value: value.to_owned(),
+ })
+ })
+ .collect();
+
+ self.0.edit(edits, &());
+ }
+}
+
+#[derive(Debug)]
+struct MapSeekTargetAdaptor<'a, T>(&'a T);
+
+impl<'a, K: Debug + Clone + Default + Ord, T: MapSeekTarget<K>>
+ SeekTarget<'a, MapKey<K>, MapKeyRef<'a, K>> for MapSeekTargetAdaptor<'_, T>
+{
+ fn cmp(&self, cursor_location: &MapKeyRef<K>, _: &()) -> Ordering {
+ if let Some(key) = &cursor_location.0 {
+ MapSeekTarget::cmp_cursor(self.0, key)
+ } else {
+ Ordering::Greater
+ }
+ }
+}
+
+pub trait MapSeekTarget<K>: Debug {
+ fn cmp_cursor(&self, cursor_location: &K) -> Ordering;
+}
+
+impl<K: Debug + Ord> MapSeekTarget<K> for K {
+ fn cmp_cursor(&self, cursor_location: &K) -> Ordering {
+ self.cmp(cursor_location)
+ }
}
impl<K, V> Default for TreeMap<K, V>
@@ -186,7 +246,7 @@ where
K: Clone + Debug + Default + Ord,
{
fn cmp(&self, cursor_location: &MapKeyRef<K>, _: &()) -> Ordering {
- self.0.cmp(&cursor_location.0)
+ Ord::cmp(&self.0, &cursor_location.0)
}
}
@@ -272,4 +332,112 @@ mod tests {
map.retain(|key, _| *key % 2 == 0);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&4, &"d"), (&6, &"f")]);
}
+
+ #[test]
+ fn test_iter_from() {
+ let mut map = TreeMap::default();
+
+ map.insert("a", 1);
+ map.insert("b", 2);
+ map.insert("baa", 3);
+ map.insert("baaab", 4);
+ map.insert("c", 5);
+
+ let result = map
+ .iter_from(&"ba")
+ .take_while(|(key, _)| key.starts_with(&"ba"))
+ .collect::<Vec<_>>();
+
+ assert_eq!(result.len(), 2);
+ assert!(result.iter().find(|(k, _)| k == &&"baa").is_some());
+ assert!(result.iter().find(|(k, _)| k == &&"baaab").is_some());
+
+ let result = map
+ .iter_from(&"c")
+ .take_while(|(key, _)| key.starts_with(&"c"))
+ .collect::<Vec<_>>();
+
+ assert_eq!(result.len(), 1);
+ assert!(result.iter().find(|(k, _)| k == &&"c").is_some());
+ }
+
+ #[test]
+ fn test_insert_tree() {
+ let mut map = TreeMap::default();
+ map.insert("a", 1);
+ map.insert("b", 2);
+ map.insert("c", 3);
+
+ let mut other = TreeMap::default();
+ other.insert("a", 2);
+ other.insert("b", 2);
+ other.insert("d", 4);
+
+ map.insert_tree(other);
+
+ assert_eq!(map.iter().count(), 4);
+ assert_eq!(map.get(&"a"), Some(&2));
+ assert_eq!(map.get(&"b"), Some(&2));
+ assert_eq!(map.get(&"c"), Some(&3));
+ assert_eq!(map.get(&"d"), Some(&4));
+ }
+
+ #[test]
+ fn test_remove_between_and_path_successor() {
+ use std::path::{Path, PathBuf};
+
+ #[derive(Debug)]
+ pub struct PathDescendants<'a>(&'a Path);
+
+ impl MapSeekTarget<PathBuf> for PathDescendants<'_> {
+ fn cmp_cursor(&self, key: &PathBuf) -> Ordering {
+ if key.starts_with(&self.0) {
+ Ordering::Greater
+ } else {
+ self.0.cmp(key)
+ }
+ }
+ }
+
+ let mut map = TreeMap::default();
+
+ map.insert(PathBuf::from("a"), 1);
+ map.insert(PathBuf::from("a/a"), 1);
+ map.insert(PathBuf::from("b"), 2);
+ map.insert(PathBuf::from("b/a/a"), 3);
+ map.insert(PathBuf::from("b/a/a/a/b"), 4);
+ map.insert(PathBuf::from("c"), 5);
+ map.insert(PathBuf::from("c/a"), 6);
+
+ map.remove_range(
+ &PathBuf::from("b/a"),
+ &PathDescendants(&PathBuf::from("b/a")),
+ );
+
+ assert_eq!(map.get(&PathBuf::from("a")), Some(&1));
+ assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1));
+ assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
+ assert_eq!(map.get(&PathBuf::from("b/a/a")), None);
+ assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None);
+ assert_eq!(map.get(&PathBuf::from("c")), Some(&5));
+ assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6));
+
+ map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c")));
+
+ assert_eq!(map.get(&PathBuf::from("a")), Some(&1));
+ assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1));
+ assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
+ assert_eq!(map.get(&PathBuf::from("c")), None);
+ assert_eq!(map.get(&PathBuf::from("c/a")), None);
+
+ map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a")));
+
+ assert_eq!(map.get(&PathBuf::from("a")), None);
+ assert_eq!(map.get(&PathBuf::from("a/a")), None);
+ assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
+
+ map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b")));
+
+ assert_eq!(map.get(&PathBuf::from("b")), None);
+ }
}
@@ -15,6 +15,7 @@ settings = { path = "../settings" }
db = { path = "../db" }
theme = { path = "../theme" }
util = { path = "../util" }
+
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.workspace = true
@@ -27,6 +28,7 @@ dirs = "4.0.0"
shellexpand = "2.1.0"
libc = "0.2"
anyhow.workspace = true
+schemars.workspace = true
thiserror.workspace = true
lazy_static.workspace = true
serde.workspace = true
@@ -31,8 +31,8 @@ use mappings::mouse::{
};
use procinfo::LocalProcessInfo;
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use util::truncate_and_trailoff;
use std::{
@@ -48,11 +48,12 @@ use std::{
use thiserror::Error;
use gpui::{
+ fonts,
geometry::vector::{vec2f, Vector2F},
keymap_matcher::Keystroke,
platform::{MouseButton, MouseMovedEvent, TouchPhase},
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
- ClipboardItem, Entity, ModelContext, Task,
+ AppContext, ClipboardItem, Entity, ModelContext, Task,
};
use crate::mappings::{
@@ -114,6 +115,125 @@ impl EventListener for ZedListener {
}
}
+pub fn init(cx: &mut AppContext) {
+ settings::register::<TerminalSettings>(cx);
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub enum TerminalDockPosition {
+ Left,
+ Bottom,
+ Right,
+}
+
+#[derive(Deserialize)]
+pub struct TerminalSettings {
+ pub shell: Shell,
+ pub working_directory: WorkingDirectory,
+ font_size: Option<f32>,
+ pub font_family: Option<String>,
+ pub line_height: TerminalLineHeight,
+ pub font_features: Option<fonts::Features>,
+ pub env: HashMap<String, String>,
+ pub blinking: TerminalBlink,
+ pub alternate_scroll: AlternateScroll,
+ pub option_as_meta: bool,
+ pub copy_on_select: bool,
+ pub dock: TerminalDockPosition,
+ pub default_width: f32,
+ pub default_height: f32,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct TerminalSettingsContent {
+ pub shell: Option<Shell>,
+ pub working_directory: Option<WorkingDirectory>,
+ pub font_size: Option<f32>,
+ pub font_family: Option<String>,
+ pub line_height: Option<TerminalLineHeight>,
+ pub font_features: Option<fonts::Features>,
+ pub env: Option<HashMap<String, String>>,
+ pub blinking: Option<TerminalBlink>,
+ pub alternate_scroll: Option<AlternateScroll>,
+ pub option_as_meta: Option<bool>,
+ pub copy_on_select: Option<bool>,
+ pub dock: Option<TerminalDockPosition>,
+ pub default_width: Option<f32>,
+ pub default_height: Option<f32>,
+}
+
+impl TerminalSettings {
+ pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
+ self.font_size
+ .map(|size| theme::adjusted_font_size(size, cx))
+ }
+}
+
+impl settings::Setting for TerminalSettings {
+ const KEY: Option<&'static str> = Some("terminal");
+
+ type FileContent = TerminalSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalLineHeight {
+ #[default]
+ Comfortable,
+ Standard,
+ Custom(f32),
+}
+
+impl TerminalLineHeight {
+ pub fn value(&self) -> f32 {
+ match self {
+ TerminalLineHeight::Comfortable => 1.618,
+ TerminalLineHeight::Standard => 1.3,
+ TerminalLineHeight::Custom(line_height) => *line_height,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalBlink {
+ Off,
+ TerminalControlled,
+ On,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Shell {
+ System,
+ Program(String),
+ WithArguments { program: String, args: Vec<String> },
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AlternateScroll {
+ On,
+ Off,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WorkingDirectory {
+ CurrentProjectDirectory,
+ FirstProjectDirectory,
+ AlwaysHome,
+ Always { directory: String },
+}
+
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct TerminalSize {
pub cell_width: f32,
@@ -599,7 +719,7 @@ impl Terminal {
match event {
InternalEvent::ColorRequest(index, format) => {
let color = term.colors()[*index].unwrap_or_else(|| {
- let term_style = &cx.global::<Settings>().theme.terminal;
+ let term_style = &theme::current(cx).terminal;
to_alac_rgb(get_color_at_index(index, &term_style))
});
self.write_to_pty(format(color))
@@ -1049,16 +1169,7 @@ impl Terminal {
}
pub fn mouse_up(&mut self, e: &MouseUp, origin: Vector2F, cx: &mut ModelContext<Self>) {
- let settings = cx.global::<Settings>();
- let copy_on_select = settings
- .terminal_overrides
- .copy_on_select
- .unwrap_or_else(|| {
- settings
- .terminal_defaults
- .copy_on_select
- .expect("Should be set in defaults")
- });
+ let setting = settings::get::<TerminalSettings>(cx);
let position = e.position.sub(origin);
if self.mouse_mode(e.shift) {
@@ -1072,7 +1183,7 @@ impl Terminal {
self.pty_tx.notify(bytes);
}
} else {
- if e.button == MouseButton::Left && copy_on_select {
+ if e.button == MouseButton::Left && setting.copy_on_select {
self.copy();
}
@@ -39,6 +39,7 @@ serde_derive.workspace = true
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
client = { path = "../client", features = ["test-support"]}
project = { path = "../project", features = ["test-support"]}
@@ -16,7 +16,6 @@ use gpui::{
use itertools::Itertools;
use language::CursorShape;
use ordered_float::OrderedFloat;
-use settings::Settings;
use terminal::{
alacritty_terminal::{
ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
@@ -25,9 +24,9 @@ use terminal::{
term::{cell::Flags, TermMode},
},
mappings::colors::convert_color,
- IndexedCell, Terminal, TerminalContent, TerminalSize,
+ IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize,
};
-use theme::TerminalStyle;
+use theme::{TerminalStyle, ThemeSettings};
use util::ResultExt;
use std::{fmt::Debug, ops::RangeInclusive};
@@ -510,38 +509,47 @@ impl TerminalElement {
scene.push_mouse_region(region);
}
+}
+
+impl Element<TerminalView> for TerminalElement {
+ type LayoutState = LayoutState;
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: gpui::SizeConstraint,
+ view: &mut TerminalView,
+ cx: &mut LayoutContext<TerminalView>,
+ ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+ let settings = settings::get::<ThemeSettings>(cx);
+ let terminal_settings = settings::get::<TerminalSettings>(cx);
+
+ //Setup layout information
+ let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
+ let link_style = settings.theme.editor.link_definition;
+ let tooltip_style = settings.theme.tooltip.clone();
- ///Configures a text style from the current settings.
- pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
- let font_family_name = settings
- .terminal_overrides
+ let font_cache = cx.font_cache();
+ let font_size = terminal_settings
+ .font_size(cx)
+ .unwrap_or(settings.buffer_font_size(cx));
+ let font_family_name = terminal_settings
.font_family
.as_ref()
- .or(settings.terminal_defaults.font_family.as_ref())
.unwrap_or(&settings.buffer_font_family_name);
- let font_features = settings
- .terminal_overrides
+ let font_features = terminal_settings
.font_features
.as_ref()
- .or(settings.terminal_defaults.font_features.as_ref())
.unwrap_or(&settings.buffer_font_features);
-
let family_id = font_cache
.load_family(&[font_family_name], &font_features)
.log_err()
.unwrap_or(settings.buffer_font_family);
-
- let font_size = settings
- .terminal_overrides
- .font_size
- .or(settings.terminal_defaults.font_size)
- .unwrap_or(settings.buffer_font_size);
-
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
- TextStyle {
+ let text_style = TextStyle {
color: settings.theme.editor.text_color,
font_family_id: family_id,
font_family_name: font_cache.family_name(family_id).unwrap(),
@@ -549,34 +557,12 @@ impl TerminalElement {
font_size,
font_properties: Default::default(),
underline: Default::default(),
- }
- }
-}
-
-impl Element<TerminalView> for TerminalElement {
- type LayoutState = LayoutState;
- type PaintState = ();
-
- fn layout(
- &mut self,
- constraint: gpui::SizeConstraint,
- view: &mut TerminalView,
- cx: &mut LayoutContext<TerminalView>,
- ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
- let settings = cx.global::<Settings>();
- let font_cache = cx.font_cache();
-
- //Setup layout information
- let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
- let link_style = settings.theme.editor.link_definition;
- let tooltip_style = settings.theme.tooltip.clone();
-
- let text_style = TerminalElement::make_text_style(font_cache, settings);
+ };
let selection_color = settings.theme.editor.selection.selection;
let match_color = settings.theme.search.match_background;
let gutter;
let dimensions = {
- let line_height = text_style.font_size * settings.terminal_line_height();
+ let line_height = text_style.font_size * terminal_settings.line_height.value();
let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
gutter = cell_width;
@@ -1,11 +1,15 @@
+use std::sync::Arc;
+
use crate::TerminalView;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, anyhow::Result, elements::*, serde_json, AppContext, AsyncAppContext, Entity,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
+use project::Fs;
use serde::{Deserialize, Serialize};
-use settings::{settings_file::SettingsFile, Settings, TerminalDockPosition, WorkingDirectory};
+use settings::SettingsStore;
+use terminal::{TerminalDockPosition, TerminalSettings};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},
@@ -31,6 +35,7 @@ pub enum Event {
pub struct TerminalPanel {
pane: ViewHandle<Pane>,
+ fs: Arc<dyn Fs>,
workspace: WeakViewHandle<Workspace>,
pending_serialization: Task<Option<()>>,
_subscriptions: Vec<Subscription>,
@@ -38,22 +43,13 @@ pub struct TerminalPanel {
impl TerminalPanel {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
- let mut old_dock_position = cx.global::<Settings>().terminal_overrides.dock;
- cx.observe_global::<Settings, _>(move |_, cx| {
- let new_dock_position = cx.global::<Settings>().terminal_overrides.dock;
- if new_dock_position != old_dock_position {
- old_dock_position = new_dock_position;
- cx.emit(Event::DockPositionChanged);
- }
- })
- .detach();
-
- let this = cx.weak_handle();
+ let weak_self = cx.weak_handle();
let pane = cx.add_view(|cx| {
let window_id = cx.window_id();
let mut pane = Pane::new(
workspace.weak_handle(),
workspace.app_state().background_actions,
+ Default::default(),
cx,
);
pane.set_can_split(false, cx);
@@ -65,7 +61,7 @@ impl TerminalPanel {
})
});
pane.set_render_tab_bar_buttons(cx, move |_, cx| {
- let this = this.clone();
+ let this = weak_self.clone();
Pane::render_tab_bar_button(
0,
"icons/plus_12.svg",
@@ -89,12 +85,23 @@ impl TerminalPanel {
cx.observe(&pane, |_, _, cx| cx.notify()),
cx.subscribe(&pane, Self::handle_pane_event),
];
- Self {
+ let this = Self {
pane,
+ fs: workspace.app_state().fs.clone(),
workspace: workspace.weak_handle(),
pending_serialization: Task::ready(None),
_subscriptions: subscriptions,
- }
+ };
+ let mut old_dock_position = this.position(cx);
+ cx.observe_global::<SettingsStore, _>(move |this, cx| {
+ let new_dock_position = this.position(cx);
+ if new_dock_position != old_dock_position {
+ old_dock_position = new_dock_position;
+ cx.emit(Event::DockPositionChanged);
+ }
+ })
+ .detach();
+ this
}
pub fn load(
@@ -187,12 +194,9 @@ impl TerminalPanel {
cx.spawn(|this, mut cx| async move {
let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
workspace.update(&mut cx, |workspace, cx| {
- let working_directory_strategy = cx
- .global::<Settings>()
- .terminal_overrides
+ let working_directory_strategy = settings::get::<TerminalSettings>(cx)
.working_directory
- .clone()
- .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
+ .clone();
let working_directory =
crate::get_working_directory(workspace, cx, working_directory_strategy);
let window_id = cx.window_id();
@@ -262,17 +266,10 @@ impl View for TerminalPanel {
impl Panel for TerminalPanel {
fn position(&self, cx: &WindowContext) -> DockPosition {
- let settings = cx.global::<Settings>();
- let dock = settings
- .terminal_overrides
- .dock
- .or(settings.terminal_defaults.dock)
- .unwrap()
- .into();
- match dock {
- settings::TerminalDockPosition::Left => DockPosition::Left,
- settings::TerminalDockPosition::Bottom => DockPosition::Bottom,
- settings::TerminalDockPosition::Right => DockPosition::Right,
+ match settings::get::<TerminalSettings>(cx).dock {
+ TerminalDockPosition::Left => DockPosition::Left,
+ TerminalDockPosition::Bottom => DockPosition::Bottom,
+ TerminalDockPosition::Right => DockPosition::Right,
}
}
@@ -281,21 +278,21 @@ impl Panel for TerminalPanel {
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
- SettingsFile::update(cx, move |settings| {
+ settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
let dock = match position {
DockPosition::Left => TerminalDockPosition::Left,
DockPosition::Bottom => TerminalDockPosition::Bottom,
DockPosition::Right => TerminalDockPosition::Right,
};
- settings.terminal.dock = Some(dock);
+ settings.dock = Some(dock);
});
}
fn default_size(&self, cx: &WindowContext) -> f32 {
- let settings = &cx.global::<Settings>().terminal_overrides;
+ let settings = settings::get::<TerminalSettings>(cx);
match self.position(cx) {
- DockPosition::Left | DockPosition::Right => settings.default_width.unwrap_or(640.),
- DockPosition::Bottom => settings.default_height.unwrap_or(320.),
+ DockPosition::Left | DockPosition::Right => settings.default_width,
+ DockPosition::Bottom => settings.default_height,
}
}
@@ -2,6 +2,7 @@ mod persistence;
pub mod terminal_element;
pub mod terminal_panel;
+use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
use context_menu::{ContextMenu, ContextMenuItem};
use dirs::home_dir;
use gpui::{
@@ -16,7 +17,6 @@ use gpui::{
};
use project::{LocalWorktree, Project};
use serde::Deserialize;
-use settings::{Settings, TerminalBlink, WorkingDirectory};
use smallvec::{smallvec, SmallVec};
use smol::Timer;
use std::{
@@ -30,7 +30,7 @@ use terminal::{
index::Point,
term::{search::RegexSearch, TermMode},
},
- Event, Terminal,
+ Event, Terminal, TerminalBlink, WorkingDirectory,
};
use util::ResultExt;
use workspace::{
@@ -41,7 +41,7 @@ use workspace::{
Pane, ToolbarItemLocation, Workspace, WorkspaceId,
};
-use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
+pub use terminal::TerminalSettings;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -64,6 +64,8 @@ impl_actions!(terminal, [SendText, SendKeystroke]);
pub fn init(cx: &mut AppContext) {
terminal_panel::init(cx);
+ terminal::init(cx);
+
cx.add_action(TerminalView::deploy);
register_deserializable_item::<TerminalView>(cx);
@@ -102,9 +104,9 @@ impl TerminalView {
_: &workspace::NewTerminal,
cx: &mut ViewContext<Workspace>,
) {
- let strategy = cx.global::<Settings>().terminal_strategy();
-
- let working_directory = get_working_directory(workspace, cx, strategy);
+ let strategy = settings::get::<TerminalSettings>(cx);
+ let working_directory =
+ get_working_directory(workspace, cx, strategy.working_directory.clone());
let window_id = cx.window_id();
let terminal = workspace
@@ -216,10 +218,7 @@ impl TerminalView {
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&Keystroke::parse("ctrl-cmd-space").unwrap(),
- cx.global::<Settings>()
- .terminal_overrides
- .option_as_meta
- .unwrap_or(false),
+ settings::get::<TerminalSettings>(cx).option_as_meta,
)
});
}
@@ -245,16 +244,7 @@ impl TerminalView {
return true;
}
- let setting = {
- let settings = cx.global::<Settings>();
- settings
- .terminal_overrides
- .blinking
- .clone()
- .unwrap_or(TerminalBlink::TerminalControlled)
- };
-
- match setting {
+ match settings::get::<TerminalSettings>(cx).blinking {
//If the user requested to never blink, don't blink it.
TerminalBlink::Off => true,
//If the terminal is controlling it, check terminal mode
@@ -347,10 +337,7 @@ impl TerminalView {
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&keystroke,
- cx.global::<Settings>()
- .terminal_overrides
- .option_as_meta
- .unwrap_or(false),
+ settings::get::<TerminalSettings>(cx).option_as_meta,
);
});
}
@@ -413,10 +400,7 @@ impl View for TerminalView {
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&event.keystroke,
- cx.global::<Settings>()
- .terminal_overrides
- .option_as_meta
- .unwrap_or(false),
+ settings::get::<TerminalSettings>(cx).option_as_meta,
)
})
}
@@ -618,7 +602,9 @@ impl Item for TerminalView {
.flatten()
.or_else(|| {
cx.read(|cx| {
- let strategy = cx.global::<Settings>().terminal_strategy();
+ let strategy = settings::get::<TerminalSettings>(cx)
+ .working_directory
+ .clone();
workspace
.upgrade(cx)
.map(|workspace| {
@@ -802,22 +788,18 @@ fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
#[cfg(test)]
mod tests {
-
use super::*;
use gpui::TestAppContext;
use project::{Entry, Project, ProjectPath, Worktree};
- use workspace::AppState;
-
use std::path::Path;
+ use workspace::AppState;
- ///Working directory calculation tests
+ // Working directory calculation tests
- ///No Worktrees in project -> home_dir()
+ // No Worktrees in project -> home_dir()
#[gpui::test]
async fn no_worktree(cx: &mut TestAppContext) {
- //Setup variables
- let (project, workspace) = blank_workspace(cx).await;
- //Test
+ let (project, workspace) = init_test(cx).await;
cx.read(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
@@ -833,14 +815,12 @@ mod tests {
});
}
- ///No active entry, but a worktree, worktree is a file -> home_dir()
+ // No active entry, but a worktree, worktree is a file -> home_dir()
#[gpui::test]
async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
- //Setup variables
+ let (project, workspace) = init_test(cx).await;
- let (project, workspace) = blank_workspace(cx).await;
create_file_wt(project.clone(), "/root.txt", cx).await;
-
cx.read(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
@@ -856,14 +836,12 @@ mod tests {
});
}
- //No active entry, but a worktree, worktree is a folder -> worktree_folder
+ // No active entry, but a worktree, worktree is a folder -> worktree_folder
#[gpui::test]
async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
- //Setup variables
- let (project, workspace) = blank_workspace(cx).await;
- let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
+ let (project, workspace) = init_test(cx).await;
- //Test
+ let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
cx.update(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
@@ -878,17 +856,15 @@ mod tests {
});
}
- //Active entry with a work tree, worktree is a file -> home_dir()
+ // Active entry with a work tree, worktree is a file -> home_dir()
#[gpui::test]
async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
- //Setup variables
+ let (project, workspace) = init_test(cx).await;
- let (project, workspace) = blank_workspace(cx).await;
let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
insert_active_entry_for(wt2, entry2, project.clone(), cx);
- //Test
cx.update(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
@@ -902,16 +878,15 @@ mod tests {
});
}
- //Active entry, with a worktree, worktree is a folder -> worktree_folder
+ // Active entry, with a worktree, worktree is a folder -> worktree_folder
#[gpui::test]
async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
- //Setup variables
- let (project, workspace) = blank_workspace(cx).await;
+ let (project, workspace) = init_test(cx).await;
+
let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
insert_active_entry_for(wt2, entry2, project.clone(), cx);
- //Test
cx.update(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
@@ -925,11 +900,12 @@ mod tests {
});
}
- ///Creates a worktree with 1 file: /root.txt
- pub async fn blank_workspace(
+ /// Creates a worktree with 1 file: /root.txt
+ pub async fn init_test(
cx: &mut TestAppContext,
) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
let params = cx.update(AppState::test);
+ cx.update(|cx| theme::init((), cx));
let project = Project::test(params.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -937,7 +913,7 @@ mod tests {
(project, workspace)
}
- ///Creates a worktree with 1 folder: /root{suffix}/
+ /// Creates a worktree with 1 folder: /root{suffix}/
async fn create_folder_wt(
project: ModelHandle<Project>,
path: impl AsRef<Path>,
@@ -946,7 +922,7 @@ mod tests {
create_wt(project, true, path, cx).await
}
- ///Creates a worktree with 1 file: /root{suffix}.txt
+ /// Creates a worktree with 1 file: /root{suffix}.txt
async fn create_file_wt(
project: ModelHandle<Project>,
path: impl AsRef<Path>,
@@ -1783,6 +1783,19 @@ impl BufferSnapshot {
where
D: 'a + TextDimension,
A: 'a + IntoIterator<Item = &'a Anchor>,
+ {
+ let anchors = anchors.into_iter();
+ self.summaries_for_anchors_with_payload::<D, _, ()>(anchors.map(|a| (a, ())))
+ .map(|d| d.0)
+ }
+
+ pub fn summaries_for_anchors_with_payload<'a, D, A, T>(
+ &'a self,
+ anchors: A,
+ ) -> impl 'a + Iterator<Item = (D, T)>
+ where
+ D: 'a + TextDimension,
+ A: 'a + IntoIterator<Item = (&'a Anchor, T)>,
{
let anchors = anchors.into_iter();
let mut insertion_cursor = self.insertions.cursor::<InsertionFragmentKey>();
@@ -1790,11 +1803,11 @@ impl BufferSnapshot {
let mut text_cursor = self.visible_text.cursor(0);
let mut position = D::default();
- anchors.map(move |anchor| {
+ anchors.map(move |(anchor, payload)| {
if *anchor == Anchor::MIN {
- return D::default();
+ return (D::default(), payload);
} else if *anchor == Anchor::MAX {
- return D::from_text_summary(&self.visible_text.summary());
+ return (D::from_text_summary(&self.visible_text.summary()), payload);
}
let anchor_key = InsertionFragmentKey {
@@ -1825,7 +1838,7 @@ impl BufferSnapshot {
}
position.add_assign(&text_cursor.summary(fragment_offset));
- position.clone()
+ (position.clone(), payload)
})
}
@@ -4,16 +4,33 @@ version = "0.1.0"
edition = "2021"
publish = false
+[features]
+test-support = [
+ "gpui/test-support",
+ "fs/test-support",
+ "settings/test-support"
+]
+
[lib]
path = "src/theme.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
+fs = { path = "../fs" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+
anyhow.workspace = true
indexmap = "1.6.2"
parking_lot.workspace = true
+schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
-toml = "0.5"
+toml.workspace = true
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
@@ -1,19 +1,40 @@
mod theme_registry;
+mod theme_settings;
+pub mod ui;
use gpui::{
color::Color,
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle},
fonts::{HighlightStyle, TextStyle},
- platform, Border, MouseState,
+ platform, AppContext, AssetSource, Border, MouseState,
};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
+use settings::SettingsStore;
use std::{collections::HashMap, sync::Arc};
use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle};
-pub mod ui;
-
pub use theme_registry::*;
+pub use theme_settings::*;
+
+pub fn current(cx: &AppContext) -> Arc<Theme> {
+ settings::get::<ThemeSettings>(cx).theme.clone()
+}
+
+pub fn init(source: impl AssetSource, cx: &mut AppContext) {
+ cx.set_global(ThemeRegistry::new(source, cx.font_cache().clone()));
+ settings::register::<ThemeSettings>(cx);
+
+ let mut prev_buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
+ cx.observe_global::<SettingsStore, _>(move |cx| {
+ let buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
+ if buffer_font_size != prev_buffer_font_size {
+ prev_buffer_font_size = buffer_font_size;
+ reset_font_size(cx);
+ }
+ })
+ .detach();
+}
#[derive(Deserialize, Default)]
pub struct Theme {
@@ -22,13 +22,25 @@ pub struct ThemeRegistry {
impl ThemeRegistry {
pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
- Arc::new(Self {
+ let this = Arc::new(Self {
assets: Box::new(source),
themes: Default::default(),
theme_data: Default::default(),
next_theme_id: Default::default(),
font_cache,
- })
+ });
+
+ this.themes.lock().insert(
+ settings::EMPTY_THEME_NAME.to_string(),
+ gpui::fonts::with_font_cache(this.font_cache.clone(), || {
+ let mut theme = Theme::default();
+ theme.meta.id = this.next_theme_id.fetch_add(1, SeqCst);
+ theme.meta.name = settings::EMPTY_THEME_NAME.into();
+ Arc::new(theme)
+ }),
+ );
+
+ this
}
pub fn list(&self, staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
@@ -0,0 +1,184 @@
+use crate::{Theme, ThemeRegistry};
+use anyhow::Result;
+use gpui::{font_cache::FamilyId, fonts, AppContext};
+use schemars::{
+ gen::SchemaGenerator,
+ schema::{InstanceType, Schema, SchemaObject},
+ JsonSchema,
+};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use settings::SettingsJsonSchemaParams;
+use std::sync::Arc;
+use util::ResultExt as _;
+
+const MIN_FONT_SIZE: f32 = 6.0;
+
+#[derive(Clone)]
+pub struct ThemeSettings {
+ pub buffer_font_family_name: String,
+ pub buffer_font_features: fonts::Features,
+ pub buffer_font_family: FamilyId,
+ pub(crate) buffer_font_size: f32,
+ pub theme: Arc<Theme>,
+}
+
+pub struct AdjustedBufferFontSize(pub f32);
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ThemeSettingsContent {
+ #[serde(default)]
+ pub buffer_font_family: Option<String>,
+ #[serde(default)]
+ pub buffer_font_size: Option<f32>,
+ #[serde(default)]
+ pub buffer_font_features: Option<fonts::Features>,
+ #[serde(default)]
+ pub theme: Option<String>,
+}
+
+impl ThemeSettings {
+ pub fn buffer_font_size(&self, cx: &AppContext) -> f32 {
+ if cx.has_global::<AdjustedBufferFontSize>() {
+ cx.global::<AdjustedBufferFontSize>().0
+ } else {
+ self.buffer_font_size
+ }
+ .max(MIN_FONT_SIZE)
+ }
+}
+
+pub fn adjusted_font_size(size: f32, cx: &AppContext) -> f32 {
+ if cx.has_global::<AdjustedBufferFontSize>() {
+ let buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
+ let delta = cx.global::<AdjustedBufferFontSize>().0 - buffer_font_size;
+ size + delta
+ } else {
+ size
+ }
+ .max(MIN_FONT_SIZE)
+}
+
+pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut f32)) {
+ if !cx.has_global::<AdjustedBufferFontSize>() {
+ let buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
+ cx.set_global(AdjustedBufferFontSize(buffer_font_size));
+ }
+
+ cx.update_global::<AdjustedBufferFontSize, _, _>(|delta, cx| {
+ f(&mut delta.0);
+ delta.0 = delta
+ .0
+ .max(MIN_FONT_SIZE - settings::get::<ThemeSettings>(cx).buffer_font_size);
+ });
+ cx.refresh_windows();
+}
+
+pub fn reset_font_size(cx: &mut AppContext) {
+ if cx.has_global::<AdjustedBufferFontSize>() {
+ cx.remove_global::<AdjustedBufferFontSize>();
+ cx.refresh_windows();
+ }
+}
+
+impl settings::Setting for ThemeSettings {
+ const KEY: Option<&'static str> = None;
+
+ type FileContent = ThemeSettingsContent;
+
+ fn load(
+ defaults: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ cx: &AppContext,
+ ) -> Result<Self> {
+ let buffer_font_features = defaults.buffer_font_features.clone().unwrap();
+ let themes = cx.global::<Arc<ThemeRegistry>>();
+
+ let mut this = Self {
+ buffer_font_family: cx
+ .font_cache()
+ .load_family(
+ &[defaults.buffer_font_family.as_ref().unwrap()],
+ &buffer_font_features,
+ )
+ .unwrap(),
+ buffer_font_family_name: defaults.buffer_font_family.clone().unwrap(),
+ buffer_font_features,
+ buffer_font_size: defaults.buffer_font_size.unwrap(),
+ theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
+ };
+
+ for value in user_values.into_iter().copied().cloned() {
+ let font_cache = cx.font_cache();
+ let mut family_changed = false;
+ if let Some(value) = value.buffer_font_family {
+ this.buffer_font_family_name = value;
+ family_changed = true;
+ }
+ if let Some(value) = value.buffer_font_features {
+ this.buffer_font_features = value;
+ family_changed = true;
+ }
+ if family_changed {
+ if let Some(id) = font_cache
+ .load_family(&[&this.buffer_font_family_name], &this.buffer_font_features)
+ .log_err()
+ {
+ this.buffer_font_family = id;
+ }
+ }
+
+ if let Some(value) = &value.theme {
+ if let Some(theme) = themes.get(value).log_err() {
+ this.theme = theme;
+ }
+ }
+
+ merge(&mut this.buffer_font_size, value.buffer_font_size);
+ }
+
+ Ok(this)
+ }
+
+ fn json_schema(
+ generator: &mut SchemaGenerator,
+ params: &SettingsJsonSchemaParams,
+ cx: &AppContext,
+ ) -> schemars::schema::RootSchema {
+ let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
+ let theme_names = cx
+ .global::<Arc<ThemeRegistry>>()
+ .list(params.staff_mode)
+ .map(|theme| Value::String(theme.name.clone()))
+ .collect();
+
+ let theme_name_schema = SchemaObject {
+ instance_type: Some(InstanceType::String.into()),
+ enum_values: Some(theme_names),
+ ..Default::default()
+ };
+
+ root_schema
+ .definitions
+ .extend([("ThemeName".into(), theme_name_schema.into())]);
+
+ root_schema
+ .schema
+ .object
+ .as_mut()
+ .unwrap()
+ .properties
+ .extend([(
+ "theme".to_owned(),
+ Schema::new_ref("#/definitions/ThemeName".into()),
+ )]);
+
+ root_schema
+ }
+}
+
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+ if let Some(value) = value {
+ *target = value;
+ }
+}
@@ -1,9 +1,10 @@
use std::borrow::Cow;
+use fs::repository::GitFileStatus;
use gpui::{
color::Color,
elements::{
- ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
+ ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, LabelStyle,
MouseEventHandler, ParentElement, Stack, Svg,
},
fonts::TextStyle,
@@ -11,11 +12,11 @@ use gpui::{
platform,
platform::MouseButton,
scene::MouseClick,
- Action, Element, EventContext, MouseState, View, ViewContext,
+ Action, AnyElement, Element, EventContext, MouseState, View, ViewContext,
};
use serde::Deserialize;
-use crate::{ContainedText, Interactive};
+use crate::{ContainedText, Interactive, Theme};
#[derive(Clone, Deserialize, Default)]
pub struct CheckboxStyle {
@@ -252,3 +253,53 @@ where
.constrained()
.with_height(style.dimensions().y())
}
+
+pub struct FileName {
+ filename: String,
+ git_status: Option<GitFileStatus>,
+ style: FileNameStyle,
+}
+
+pub struct FileNameStyle {
+ template_style: LabelStyle,
+ git_inserted: Color,
+ git_modified: Color,
+ git_deleted: Color,
+}
+
+impl FileName {
+ pub fn new(filename: String, git_status: Option<GitFileStatus>, style: FileNameStyle) -> Self {
+ FileName {
+ filename,
+ git_status,
+ style,
+ }
+ }
+
+ pub fn style<I: Into<LabelStyle>>(style: I, theme: &Theme) -> FileNameStyle {
+ FileNameStyle {
+ template_style: style.into(),
+ git_inserted: theme.editor.diff.inserted,
+ git_modified: theme.editor.diff.modified,
+ git_deleted: theme.editor.diff.deleted,
+ }
+ }
+}
+
+impl<V: View> gpui::elements::Component<V> for FileName {
+ fn render(&self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
+ // Prepare colors for git statuses
+ let mut filename_text_style = self.style.template_style.text.clone();
+ filename_text_style.color = self
+ .git_status
+ .as_ref()
+ .map(|status| match status {
+ GitFileStatus::Added => self.style.git_inserted,
+ GitFileStatus::Modified => self.style.git_modified,
+ GitFileStatus::Conflict => self.style.git_deleted,
+ })
+ .unwrap_or(self.style.template_style.text.color);
+
+ Label::new(self.filename.clone(), filename_text_style).into_any()
+ }
+}
@@ -11,6 +11,7 @@ doctest = false
[dependencies]
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
+fs = { path = "../fs" }
gpui = { path = "../gpui" }
picker = { path = "../picker" }
theme = { path = "../theme" }
@@ -22,3 +23,6 @@ log.workspace = true
parking_lot.workspace = true
postage.workspace = true
smol.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -1,10 +1,11 @@
+use fs::Fs;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext};
use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::{settings_file::SettingsFile, Settings};
+use settings::{update_settings_file, SettingsStore};
use staff_mode::StaffMode;
use std::sync::Arc;
-use theme::{Theme, ThemeMeta, ThemeRegistry};
+use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
use util::ResultExt;
use workspace::Workspace;
@@ -17,16 +18,17 @@ pub fn init(cx: &mut AppContext) {
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
- let themes = workspace.app_state().themes.clone();
- cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx))
+ let fs = workspace.app_state().fs.clone();
+ cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(fs, cx), cx))
});
}
#[cfg(debug_assertions)]
-pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
- let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
- themes.clear();
- match themes.get(¤t_theme_name) {
+pub fn reload(cx: &mut AppContext) {
+ let current_theme_name = theme::current(cx).meta.name.clone();
+ let registry = cx.global::<Arc<ThemeRegistry>>();
+ registry.clear();
+ match registry.get(¤t_theme_name) {
Ok(theme) => {
ThemeSelectorDelegate::set_theme(theme, cx);
log::info!("reloaded theme {}", current_theme_name);
@@ -40,7 +42,7 @@ pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
pub type ThemeSelector = Picker<ThemeSelectorDelegate>;
pub struct ThemeSelectorDelegate {
- registry: Arc<ThemeRegistry>,
+ fs: Arc<dyn Fs>,
theme_data: Vec<ThemeMeta>,
matches: Vec<StringMatch>,
original_theme: Arc<Theme>,
@@ -49,14 +51,12 @@ pub struct ThemeSelectorDelegate {
}
impl ThemeSelectorDelegate {
- fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<ThemeSelector>) -> Self {
- let settings = cx.global::<Settings>();
+ fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<ThemeSelector>) -> Self {
+ let original_theme = theme::current(cx).clone();
- let original_theme = settings.theme.clone();
-
- let mut theme_names = registry
- .list(**cx.default_global::<StaffMode>())
- .collect::<Vec<_>>();
+ let staff_mode = **cx.default_global::<StaffMode>();
+ let registry = cx.global::<Arc<ThemeRegistry>>();
+ let mut theme_names = registry.list(staff_mode).collect::<Vec<_>>();
theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
let matches = theme_names
.iter()
@@ -68,7 +68,7 @@ impl ThemeSelectorDelegate {
})
.collect();
let mut this = Self {
- registry,
+ fs,
theme_data: theme_names,
matches,
original_theme: original_theme.clone(),
@@ -81,7 +81,8 @@ impl ThemeSelectorDelegate {
fn show_selected_theme(&mut self, cx: &mut ViewContext<ThemeSelector>) {
if let Some(mat) = self.matches.get(self.selected_index) {
- match self.registry.get(&mat.string) {
+ let registry = cx.global::<Arc<ThemeRegistry>>();
+ match registry.get(&mat.string) {
Ok(theme) => {
Self::set_theme(theme, cx);
}
@@ -101,8 +102,10 @@ impl ThemeSelectorDelegate {
}
fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
- cx.update_global::<Settings, _, _>(|settings, cx| {
- settings.theme = theme;
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ let mut theme_settings = store.get::<ThemeSettings>(None).clone();
+ theme_settings.theme = theme;
+ store.override_global(theme_settings);
cx.refresh_windows();
});
}
@@ -120,9 +123,9 @@ impl PickerDelegate for ThemeSelectorDelegate {
fn confirm(&mut self, cx: &mut ViewContext<ThemeSelector>) {
self.selection_completed = true;
- let theme_name = cx.global::<Settings>().theme.meta.name.clone();
- SettingsFile::update(cx, |settings_content| {
- settings_content.theme = Some(theme_name);
+ let theme_name = theme::current(cx).meta.name.clone();
+ update_settings_file::<ThemeSettings>(self.fs.clone(), cx, |settings| {
+ settings.theme = Some(theme_name);
});
cx.emit(PickerEvent::Dismiss);
@@ -204,11 +207,10 @@ impl PickerDelegate for ThemeSelectorDelegate {
selected: bool,
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
- let settings = cx.global::<Settings>();
- let theme = &settings.theme;
- let theme_match = &self.matches[ix];
+ let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
+ let theme_match = &self.matches[ix];
Label::new(theme_match.string.clone(), style.label.clone())
.with_highlights(theme_match.positions.clone())
.contained()
@@ -10,8 +10,7 @@ use gpui::{
WeakViewHandle,
};
use project::Project;
-use settings::Settings;
-use theme::{ColorScheme, Layer, Style, StyleSet};
+use theme::{ColorScheme, Layer, Style, StyleSet, ThemeSettings};
use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
actions!(theme, [DeployThemeTestbench]);
@@ -220,10 +219,10 @@ impl ThemeTestbench {
}
fn render_label(text: String, style: &Style, cx: &mut ViewContext<Self>) -> Label {
- let settings = cx.global::<Settings>();
+ let settings = settings::get::<ThemeSettings>(cx);
let font_cache = cx.font_cache();
let family_id = settings.buffer_font_family;
- let font_size = settings.buffer_font_size;
+ let font_size = settings.buffer_font_size(cx);
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -252,7 +251,7 @@ impl View for ThemeTestbench {
}
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
- let color_scheme = &cx.global::<Settings>().theme.clone().color_scheme;
+ let color_scheme = &theme::current(cx).clone().color_scheme;
Flex::row()
.with_child(
@@ -26,6 +26,7 @@ serde.workspace = true
serde_json.workspace = true
git2 = { version = "0.15", default-features = false, optional = true }
dirs = "3.0"
+take-until = "0.2.0"
[dev-dependencies]
tempdir.workspace = true
@@ -1,5 +1,7 @@
use std::path::{Path, PathBuf};
+use serde::{Deserialize, Serialize};
+
lazy_static::lazy_static! {
pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
@@ -70,3 +72,208 @@ pub fn compact(path: &Path) -> PathBuf {
path.to_path_buf()
}
}
+
+/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
+pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
+
+/// A representation of a path-like string with optional row and column numbers.
+/// Matching values example: `te`, `test.rs:22`, `te:22:5`, etc.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct PathLikeWithPosition<P> {
+ pub path_like: P,
+ pub row: Option<u32>,
+ // Absent if row is absent.
+ pub column: Option<u32>,
+}
+
+impl<P> PathLikeWithPosition<P> {
+ /// Parses a string that possibly has `:row:column` suffix.
+ /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
+ /// If any of the row/column component parsing fails, the whole string is then parsed as a path like.
+ pub fn parse_str<E>(
+ s: &str,
+ parse_path_like_str: impl Fn(&str) -> Result<P, E>,
+ ) -> Result<Self, E> {
+ let fallback = |fallback_str| {
+ Ok(Self {
+ path_like: parse_path_like_str(fallback_str)?,
+ row: None,
+ column: None,
+ })
+ };
+
+ match s.trim().split_once(FILE_ROW_COLUMN_DELIMITER) {
+ Some((path_like_str, maybe_row_and_col_str)) => {
+ let path_like_str = path_like_str.trim();
+ let maybe_row_and_col_str = maybe_row_and_col_str.trim();
+ if path_like_str.is_empty() {
+ fallback(s)
+ } else if maybe_row_and_col_str.is_empty() {
+ fallback(path_like_str)
+ } else {
+ let (row_parse_result, maybe_col_str) =
+ match maybe_row_and_col_str.split_once(FILE_ROW_COLUMN_DELIMITER) {
+ Some((maybe_row_str, maybe_col_str)) => {
+ (maybe_row_str.parse::<u32>(), maybe_col_str.trim())
+ }
+ None => (maybe_row_and_col_str.parse::<u32>(), ""),
+ };
+
+ match row_parse_result {
+ Ok(row) => {
+ if maybe_col_str.is_empty() {
+ Ok(Self {
+ path_like: parse_path_like_str(path_like_str)?,
+ row: Some(row),
+ column: None,
+ })
+ } else {
+ match maybe_col_str.parse::<u32>() {
+ Ok(col) => Ok(Self {
+ path_like: parse_path_like_str(path_like_str)?,
+ row: Some(row),
+ column: Some(col),
+ }),
+ Err(_) => fallback(s),
+ }
+ }
+ }
+ Err(_) => fallback(s),
+ }
+ }
+ }
+ None => fallback(s),
+ }
+ }
+
+ pub fn map_path_like<P2, E>(
+ self,
+ mapping: impl FnOnce(P) -> Result<P2, E>,
+ ) -> Result<PathLikeWithPosition<P2>, E> {
+ Ok(PathLikeWithPosition {
+ path_like: mapping(self.path_like)?,
+ row: self.row,
+ column: self.column,
+ })
+ }
+
+ pub fn to_string(&self, path_like_to_string: impl Fn(&P) -> String) -> String {
+ let path_like_string = path_like_to_string(&self.path_like);
+ if let Some(row) = self.row {
+ if let Some(column) = self.column {
+ format!("{path_like_string}:{row}:{column}")
+ } else {
+ format!("{path_like_string}:{row}")
+ }
+ } else {
+ path_like_string
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ type TestPath = PathLikeWithPosition<String>;
+
+ fn parse_str(s: &str) -> TestPath {
+ TestPath::parse_str(s, |s| Ok::<_, std::convert::Infallible>(s.to_string()))
+ .expect("infallible")
+ }
+
+ #[test]
+ fn path_with_position_parsing_positive() {
+ let input_and_expected = [
+ (
+ "test_file.rs",
+ PathLikeWithPosition {
+ path_like: "test_file.rs".to_string(),
+ row: None,
+ column: None,
+ },
+ ),
+ (
+ "test_file.rs:1",
+ PathLikeWithPosition {
+ path_like: "test_file.rs".to_string(),
+ row: Some(1),
+ column: None,
+ },
+ ),
+ (
+ "test_file.rs:1:2",
+ PathLikeWithPosition {
+ path_like: "test_file.rs".to_string(),
+ row: Some(1),
+ column: Some(2),
+ },
+ ),
+ ];
+
+ for (input, expected) in input_and_expected {
+ let actual = parse_str(input);
+ assert_eq!(
+ actual, expected,
+ "For positive case input str '{input}', got a parse mismatch"
+ );
+ }
+ }
+
+ #[test]
+ fn path_with_position_parsing_negative() {
+ for input in [
+ "test_file.rs:a",
+ "test_file.rs:a:b",
+ "test_file.rs::",
+ "test_file.rs::1",
+ "test_file.rs:1::",
+ "test_file.rs::1:2",
+ "test_file.rs:1::2",
+ "test_file.rs:1:2:",
+ "test_file.rs:1:2:3",
+ ] {
+ let actual = parse_str(input);
+ assert_eq!(
+ actual,
+ PathLikeWithPosition {
+ path_like: input.to_string(),
+ row: None,
+ column: None,
+ },
+ "For negative case input str '{input}', got a parse mismatch"
+ );
+ }
+ }
+
+ // Trim off trailing `:`s for otherwise valid input.
+ #[test]
+ fn path_with_position_parsing_special() {
+ let input_and_expected = [
+ (
+ "test_file.rs:",
+ PathLikeWithPosition {
+ path_like: "test_file.rs".to_string(),
+ row: None,
+ column: None,
+ },
+ ),
+ (
+ "test_file.rs:1:",
+ PathLikeWithPosition {
+ path_like: "test_file.rs".to_string(),
+ row: Some(1),
+ column: None,
+ },
+ ),
+ ];
+
+ for (input, expected) in input_and_expected {
+ let actual = parse_str(input);
+ assert_eq!(
+ actual, expected,
+ "For special case input str '{input}', got a parse mismatch"
+ );
+ }
+ }
+}
@@ -17,6 +17,8 @@ pub use backtrace::Backtrace;
use futures::Future;
use rand::{seq::SliceRandom, Rng};
+pub use take_until::*;
+
#[macro_export]
macro_rules! debug_panic {
( $($fmt_arg:tt)* ) => {
@@ -93,6 +95,27 @@ pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json:
}
}
+pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
+ use serde_json::Value;
+ if let Value::Object(source_object) = source {
+ let target_object = if let Value::Object(target) = target {
+ target
+ } else {
+ *target = Value::Object(Default::default());
+ target.as_object_mut().unwrap()
+ };
+ for (key, value) in source_object {
+ if let Some(target) = target_object.get_mut(&key) {
+ merge_non_null_json_value_into(value, target);
+ } else if !value.is_null() {
+ target_object.insert(key.clone(), value);
+ }
+ }
+ } else if !source.is_null() {
+ *target = source
+ }
+}
+
pub trait ResultExt {
type Ok;
@@ -12,6 +12,7 @@ doctest = false
neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
[dependencies]
+anyhow.workspace = true
serde.workspace = true
serde_derive.workspace = true
itertools = "0.10"
@@ -17,14 +17,17 @@ pub struct VimTestContext<'a> {
impl<'a> VimTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
+
cx.update(|cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.vim_mode = enabled;
- });
search::init(cx);
crate::init(cx);
+ });
- settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
+ cx.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
+ });
+ settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap();
});
// Setup search toolbars and keypress hook
@@ -52,16 +55,16 @@ impl<'a> VimTestContext<'a> {
pub fn enable_vim(&mut self) {
self.cx.update(|cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.vim_mode = true;
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(true));
});
})
}
pub fn disable_vim(&mut self) {
self.cx.update(|cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.vim_mode = false;
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(false));
});
})
}
@@ -10,8 +10,7 @@ mod state;
mod utils;
mod visual;
-use std::sync::Arc;
-
+use anyhow::Result;
use collections::CommandPaletteFilter;
use editor::{Bias, Cancel, Editor, EditorMode, Event};
use gpui::{
@@ -22,11 +21,14 @@ use language::CursorShape;
use motion::Motion;
use normal::normal_replace;
use serde::Deserialize;
-use settings::Settings;
+use settings::{Setting, SettingsStore};
use state::{Mode, Operator, VimState};
+use std::sync::Arc;
use visual::visual_replace;
use workspace::{self, Workspace};
+struct VimModeSetting(bool);
+
#[derive(Clone, Deserialize, PartialEq)]
pub struct SwitchMode(pub Mode);
@@ -40,6 +42,8 @@ actions!(vim, [Tab, Enter]);
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
pub fn init(cx: &mut AppContext) {
+ settings::register::<VimModeSetting>(cx);
+
editor_events::init(cx);
normal::init(cx);
visual::init(cx);
@@ -91,11 +95,11 @@ pub fn init(cx: &mut AppContext) {
filter.filtered_namespaces.insert("vim");
});
cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
- vim.set_enabled(cx.global::<Settings>().vim_mode, cx)
+ vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
});
- cx.observe_global::<Settings, _>(|cx| {
+ cx.observe_global::<SettingsStore, _>(|cx| {
cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
- vim.set_enabled(cx.global::<Settings>().vim_mode, cx)
+ vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
});
})
.detach();
@@ -330,6 +334,22 @@ impl Vim {
}
}
+impl Setting for VimModeSetting {
+ const KEY: Option<&'static str> = Some("vim_mode");
+
+ type FileContent = Option<bool>;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &AppContext,
+ ) -> Result<Self> {
+ Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
+ default_value.ok_or_else(Self::missing_default)?,
+ )))
+ }
+}
+
fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty {
@@ -11,9 +11,9 @@ path = "src/welcome.rs"
test-support = []
[dependencies]
-anyhow.workspace = true
-log.workspace = true
+client = { path = "../client" }
editor = { path = "../editor" }
+fs = { path = "../fs" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
db = { path = "../db" }
@@ -25,3 +25,11 @@ theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
picker = { path = "../picker" }
workspace = { path = "../workspace" }
+
+anyhow.workspace = true
+log.workspace = true
+schemars.workspace = true
+serde.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -1,5 +1,4 @@
-use std::sync::Arc;
-
+use super::base_keymap_setting::BaseKeymap;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
actions,
@@ -7,7 +6,9 @@ use gpui::{
AppContext, Task, ViewContext,
};
use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::{settings_file::SettingsFile, BaseKeymap, Settings};
+use project::Fs;
+use settings::update_settings_file;
+use std::sync::Arc;
use util::ResultExt;
use workspace::Workspace;
@@ -23,8 +24,9 @@ pub fn toggle(
_: &ToggleBaseKeymapSelector,
cx: &mut ViewContext<Workspace>,
) {
- workspace.toggle_modal(cx, |_, cx| {
- cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(cx), cx))
+ workspace.toggle_modal(cx, |workspace, cx| {
+ let fs = workspace.app_state().fs.clone();
+ cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx))
});
}
@@ -33,18 +35,20 @@ pub type BaseKeymapSelector = Picker<BaseKeymapSelectorDelegate>;
pub struct BaseKeymapSelectorDelegate {
matches: Vec<StringMatch>,
selected_index: usize,
+ fs: Arc<dyn Fs>,
}
impl BaseKeymapSelectorDelegate {
- fn new(cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
- let base = cx.global::<Settings>().base_keymap;
+ fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
+ let base = settings::get::<BaseKeymap>(cx);
let selected_index = BaseKeymap::OPTIONS
.iter()
- .position(|(_, value)| *value == base)
+ .position(|(_, value)| value == base)
.unwrap_or(0);
Self {
matches: Vec::new(),
selected_index,
+ fs,
}
}
}
@@ -119,7 +123,9 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
if let Some(selection) = self.matches.get(self.selected_index) {
let base_keymap = BaseKeymap::from_names(&selection.string);
- SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap));
+ update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
+ *setting = Some(base_keymap)
+ });
}
cx.emit(PickerEvent::Dismiss);
}
@@ -133,7 +139,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
selected: bool,
cx: &gpui::AppContext,
) -> gpui::AnyElement<Picker<Self>> {
- let theme = &cx.global::<Settings>().theme;
+ let theme = &theme::current(cx);
let keymap_match = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected);
@@ -0,0 +1,65 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+pub enum BaseKeymap {
+ #[default]
+ VSCode,
+ JetBrains,
+ SublimeText,
+ Atom,
+ TextMate,
+}
+
+impl BaseKeymap {
+ pub const OPTIONS: [(&'static str, Self); 5] = [
+ ("VSCode (Default)", Self::VSCode),
+ ("Atom", Self::Atom),
+ ("JetBrains", Self::JetBrains),
+ ("Sublime Text", Self::SublimeText),
+ ("TextMate", Self::TextMate),
+ ];
+
+ pub fn asset_path(&self) -> Option<&'static str> {
+ match self {
+ BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
+ BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
+ BaseKeymap::Atom => Some("keymaps/atom.json"),
+ BaseKeymap::TextMate => Some("keymaps/textmate.json"),
+ BaseKeymap::VSCode => None,
+ }
+ }
+
+ pub fn names() -> impl Iterator<Item = &'static str> {
+ Self::OPTIONS.iter().map(|(name, _)| *name)
+ }
+
+ pub fn from_names(option: &str) -> BaseKeymap {
+ Self::OPTIONS
+ .iter()
+ .copied()
+ .find_map(|(name, value)| (name == option).then(|| value))
+ .unwrap_or_default()
+ }
+}
+
+impl Setting for BaseKeymap {
+ const KEY: Option<&'static str> = Some("base_keymap");
+
+ type FileContent = Option<Self>;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &gpui::AppContext,
+ ) -> anyhow::Result<Self>
+ where
+ Self: Sized,
+ {
+ Ok(user_values
+ .first()
+ .and_then(|v| **v)
+ .unwrap_or(default_value.unwrap()))
+ }
+}
@@ -1,24 +1,27 @@
mod base_keymap_picker;
+mod base_keymap_setting;
-use std::{borrow::Cow, sync::Arc};
-
+use crate::base_keymap_picker::ToggleBaseKeymapSelector;
+use client::TelemetrySettings;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
elements::{Flex, Label, ParentElement},
AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
};
-use settings::{settings_file::SettingsFile, Settings};
-
+use settings::{update_settings_file, SettingsStore};
+use std::{borrow::Cow, sync::Arc};
use workspace::{
dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
WorkspaceId,
};
-use crate::base_keymap_picker::ToggleBaseKeymapSelector;
+pub use base_keymap_setting::BaseKeymap;
pub const FIRST_OPEN: &str = "first_open";
pub fn init(cx: &mut AppContext) {
+ settings::register::<BaseKeymap>(cx);
+
cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
workspace.add_item(Box::new(welcome_page), cx)
@@ -58,15 +61,10 @@ impl View for WelcomePage {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
let self_handle = cx.handle();
- let settings = cx.global::<Settings>();
- let theme = settings.theme.clone();
-
+ let theme = theme::current(cx);
let width = theme.welcome.page_width;
- let (diagnostics, metrics) = {
- let telemetry = settings.telemetry();
- (telemetry.diagnostics(), telemetry.metrics())
- };
+ let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
enum Metrics {}
enum Diagnostics {}
@@ -166,13 +164,18 @@ impl View for WelcomePage {
.with_style(theme.welcome.usage_note.container),
),
&theme.welcome.checkbox,
- metrics,
+ telemetry_settings.metrics,
0,
cx,
- |_, checked, cx| {
- SettingsFile::update(cx, move |file| {
- file.telemetry.set_metrics(checked)
- })
+ |this, checked, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ let fs = workspace.read(cx).app_state().fs.clone();
+ update_settings_file::<TelemetrySettings>(
+ fs,
+ cx,
+ move |setting| setting.metrics = Some(checked),
+ )
+ }
},
)
.contained()
@@ -182,13 +185,18 @@ impl View for WelcomePage {
theme::ui::checkbox::<Diagnostics, Self, _>(
"Send crash reports",
&theme.welcome.checkbox,
- diagnostics,
+ telemetry_settings.diagnostics,
0,
cx,
- |_, checked, cx| {
- SettingsFile::update(cx, move |file| {
- file.telemetry.set_diagnostics(checked)
- })
+ |this, checked, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ let fs = workspace.read(cx).app_state().fs.clone();
+ update_settings_file::<TelemetrySettings>(
+ fs,
+ cx,
+ move |setting| setting.diagnostics = Some(checked),
+ )
+ }
},
)
.contained()
@@ -214,7 +222,7 @@ impl WelcomePage {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
WelcomePage {
workspace: workspace.weak_handle(),
- _settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
+ _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
}
}
}
@@ -250,7 +258,7 @@ impl Item for WelcomePage {
) -> Option<Self> {
Some(WelcomePage {
workspace: self.workspace.clone(),
- _settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
+ _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
})
}
}
@@ -38,6 +38,7 @@ theme = { path = "../theme" }
util = { path = "../util" }
async-recursion = "1.0.0"
+itertools = "0.10"
bincode = "1.2.1"
anyhow.workspace = true
futures.workspace = true
@@ -45,6 +46,7 @@ lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
+schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
@@ -6,8 +6,8 @@ use gpui::{
WindowContext,
};
use serde::Deserialize;
-use settings::Settings;
use std::rc::Rc;
+use theme::ThemeSettings;
pub trait Panel: View {
fn position(&self, cx: &WindowContext) -> DockPosition;
@@ -379,7 +379,7 @@ impl Dock {
pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
if let Some(active_entry) = self.active_entry() {
- let style = &cx.global::<Settings>().theme.workspace.dock;
+ let style = &settings::get::<ThemeSettings>(cx).theme.workspace.dock;
Empty::new()
.into_any()
.contained()
@@ -407,7 +407,7 @@ impl View for Dock {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(active_entry) = self.active_entry() {
- let style = &cx.global::<Settings>().theme.workspace.dock;
+ let style = &settings::get::<ThemeSettings>(cx).theme.workspace.dock;
ChildView::new(active_entry.panel.as_any(), cx)
.contained()
.with_style(style.container)
@@ -444,7 +444,7 @@ impl View for PanelButtons {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &cx.global::<Settings>().theme;
+ let theme = &settings::get::<ThemeSettings>(cx).theme;
let tooltip_style = theme.tooltip.clone();
let theme = &theme.workspace.status_bar.panel_buttons;
let button_style = theme.button.clone();
@@ -578,7 +578,7 @@ impl StatusItemView for PanelButtons {
#[cfg(test)]
pub(crate) mod test {
use super::*;
- use gpui::Entity;
+ use gpui::{ViewContext, WindowContext};
pub enum TestPanelEvent {
PositionChanged,
@@ -3,6 +3,7 @@ use crate::{
FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
WorkspaceId,
};
+use crate::{AutosaveSetting, WorkspaceSettings};
use anyhow::Result;
use client::{proto, Client};
use gpui::{
@@ -10,7 +11,6 @@ use gpui::{
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use project::{Project, ProjectEntryId, ProjectPath};
-use settings::{Autosave, Settings};
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
@@ -450,8 +450,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
}
ItemEvent::Edit => {
- if let Autosave::AfterDelay { milliseconds } =
- cx.global::<Settings>().autosave
+ let settings = settings::get::<WorkspaceSettings>(cx);
+ let debounce_delay = settings.git.gutter_debounce;
+
+ if let AutosaveSetting::AfterDelay { milliseconds } =
+ settings.autosave
{
let delay = Duration::from_millis(milliseconds);
let item = item.clone();
@@ -460,9 +463,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
});
}
- let settings = cx.global::<Settings>();
- let debounce_delay = settings.git_overrides.gutter_debounce;
-
let item = item.clone();
if let Some(delay) = debounce_delay {
@@ -500,7 +500,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
}));
cx.observe_focus(self, move |workspace, item, focused, cx| {
- if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
+ if !focused
+ && settings::get::<WorkspaceSettings>(cx).autosave
+ == AutosaveSetting::OnFocusChange
+ {
Pane::autosave_item(&item, workspace.project.clone(), cx)
.detach_and_log_err(cx);
}
@@ -149,6 +149,8 @@ impl Workspace {
}
pub mod simple_message_notification {
+ use super::Notification;
+ use crate::Workspace;
use gpui::{
actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
@@ -158,13 +160,8 @@ pub mod simple_message_notification {
};
use menu::Cancel;
use serde::Deserialize;
- use settings::Settings;
use std::{borrow::Cow, sync::Arc};
- use crate::Workspace;
-
- use super::Notification;
-
actions!(message_notifications, [CancelMessageNotification]);
#[derive(Clone, Default, Deserialize, PartialEq)]
@@ -240,7 +237,7 @@ pub mod simple_message_notification {
}
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let theme = &theme.simple_message_notification;
enum MessageNotificationTag {}
@@ -2,8 +2,8 @@ mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection};
use crate::{
- item::WeakItemHandle, toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, ToggleZoom,
- Workspace,
+ item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal,
+ ToggleZoom, Workspace, WorkspaceSettings,
};
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque};
@@ -27,9 +27,18 @@ use gpui::{
};
use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
-use settings::{Autosave, Settings};
-use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
-use theme::Theme;
+use std::{
+ any::Any,
+ cell::RefCell,
+ cmp, mem,
+ path::Path,
+ rc::Rc,
+ sync::{
+ atomic::{AtomicUsize, Ordering},
+ Arc,
+ },
+};
+use theme::{Theme, ThemeSettings};
use util::ResultExt;
#[derive(Clone, Deserialize, PartialEq)]
@@ -163,6 +172,8 @@ pub struct ItemNavHistory {
item: Rc<dyn WeakItemHandle>,
}
+pub struct PaneNavHistory(Rc<RefCell<NavHistory>>);
+
struct NavHistory {
mode: NavigationMode,
backward_stack: VecDeque<NavigationEntry>,
@@ -170,6 +181,7 @@ struct NavHistory {
closed_stack: VecDeque<NavigationEntry>,
paths_by_item: HashMap<usize, ProjectPath>,
pane: WeakViewHandle<Pane>,
+ next_timestamp: Arc<AtomicUsize>,
}
#[derive(Copy, Clone)]
@@ -191,6 +203,7 @@ impl Default for NavigationMode {
pub struct NavigationEntry {
pub item: Rc<dyn WeakItemHandle>,
pub data: Option<Box<dyn Any>>,
+ pub timestamp: usize,
}
pub struct DraggedItem {
@@ -228,6 +241,7 @@ impl Pane {
pub fn new(
workspace: WeakViewHandle<Workspace>,
background_actions: BackgroundActions,
+ next_timestamp: Arc<AtomicUsize>,
cx: &mut ViewContext<Self>,
) -> Self {
let pane_view_id = cx.view_id();
@@ -252,6 +266,7 @@ impl Pane {
closed_stack: Default::default(),
paths_by_item: Default::default(),
pane: handle.clone(),
+ next_timestamp,
})),
toolbar: cx.add_view(|_| Toolbar::new(handle)),
tab_bar_context_menu: TabBarContextMenu {
@@ -332,6 +347,10 @@ impl Pane {
}
}
+ pub fn nav_history(&self) -> PaneNavHistory {
+ PaneNavHistory(self.nav_history.clone())
+ }
+
pub fn go_back(
workspace: &mut Workspace,
pane: Option<WeakViewHandle<Pane>>,
@@ -756,6 +775,10 @@ impl Pane {
_: &CloseInactiveItems,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
+ if self.items.is_empty() {
+ return None;
+ }
+
let active_item_id = self.items[self.active_item_index].id();
Some(self.close_items(cx, move |item_id| item_id != active_item_id))
}
@@ -778,6 +801,9 @@ impl Pane {
_: &CloseItemsToTheLeft,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
+ if self.items.is_empty() {
+ return None;
+ }
let active_item_id = self.items[self.active_item_index].id();
Some(self.close_items_to_the_left_by_id(active_item_id, cx))
}
@@ -800,6 +826,9 @@ impl Pane {
_: &CloseItemsToTheRight,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
+ if self.items.is_empty() {
+ return None;
+ }
let active_item_id = self.items[self.active_item_index].id();
Some(self.close_items_to_the_right_by_id(active_item_id, cx))
}
@@ -995,8 +1024,8 @@ impl Pane {
} else if is_dirty && (can_save || is_singleton) {
let will_autosave = cx.read(|cx| {
matches!(
- cx.global::<Settings>().autosave,
- Autosave::OnFocusChange | Autosave::OnWindowChange
+ settings::get::<WorkspaceSettings>(cx).autosave,
+ AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
) && Self::can_autosave_item(&*item, cx)
});
let should_save = if should_prompt_for_save && !will_autosave {
@@ -1220,6 +1249,25 @@ impl Pane {
&self.toolbar
}
+ pub fn handle_deleted_project_item(
+ &mut self,
+ entry_id: ProjectEntryId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Option<()> {
+ let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
+ if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
+ Some((i, item.id()))
+ } else {
+ None
+ }
+ })?;
+
+ self.remove_item(item_index_to_delete, false, cx);
+ self.nav_history.borrow_mut().remove_item(item_id);
+
+ Some(())
+ }
+
fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
let active_item = self
.items
@@ -1231,7 +1279,7 @@ impl Pane {
}
fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let pane = cx.handle().downgrade();
let autoscroll = if mem::take(&mut self.autoscroll) {
@@ -1262,7 +1310,7 @@ impl Pane {
let pane = pane.clone();
let detail = detail.clone();
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let mut tooltip_theme = theme.tooltip.clone();
tooltip_theme.max_text_width = None;
let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
@@ -1327,7 +1375,7 @@ impl Pane {
pane: pane.clone(),
},
{
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let detail = detail.clone();
move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
@@ -1532,7 +1580,7 @@ impl Pane {
Stack::new()
.with_child(
MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
- let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
+ let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
let style = theme.pane_button.style_for(mouse_state, false);
Svg::new(icon)
.with_color(style.color)
@@ -1589,7 +1637,7 @@ impl View for Pane {
if let Some(active_item) = self.active_item() {
Flex::column()
.with_child({
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
let mut stack = Stack::new();
@@ -1659,7 +1707,7 @@ impl View for Pane {
.into_any()
} else {
enum EmptyPane {}
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
self.render_blank_pane(&theme, cx)
@@ -1803,6 +1851,7 @@ impl NavHistory {
self.backward_stack.push_back(NavigationEntry {
item,
data: data.map(|data| Box::new(data) as Box<dyn Any>),
+ timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
});
self.forward_stack.clear();
}
@@ -1813,6 +1862,7 @@ impl NavHistory {
self.forward_stack.push_back(NavigationEntry {
item,
data: data.map(|data| Box::new(data) as Box<dyn Any>),
+ timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
});
}
NavigationMode::GoingForward => {
@@ -1822,6 +1872,7 @@ impl NavHistory {
self.backward_stack.push_back(NavigationEntry {
item,
data: data.map(|data| Box::new(data) as Box<dyn Any>),
+ timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
});
}
NavigationMode::ClosingItem => {
@@ -1831,6 +1882,7 @@ impl NavHistory {
self.closed_stack.push_back(NavigationEntry {
item,
data: data.map(|data| Box::new(data) as Box<dyn Any>),
+ timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
});
}
}
@@ -1844,6 +1896,40 @@ impl NavHistory {
});
}
}
+
+ fn remove_item(&mut self, item_id: usize) {
+ self.paths_by_item.remove(&item_id);
+ self.backward_stack
+ .retain(|entry| entry.item.id() != item_id);
+ self.forward_stack
+ .retain(|entry| entry.item.id() != item_id);
+ self.closed_stack.retain(|entry| entry.item.id() != item_id);
+ }
+}
+
+impl PaneNavHistory {
+ pub fn for_each_entry(
+ &self,
+ cx: &AppContext,
+ mut f: impl FnMut(&NavigationEntry, ProjectPath),
+ ) {
+ let borrowed_history = self.0.borrow();
+ borrowed_history
+ .forward_stack
+ .iter()
+ .chain(borrowed_history.backward_stack.iter())
+ .chain(borrowed_history.closed_stack.iter())
+ .for_each(|entry| {
+ if let Some(path) = borrowed_history.paths_by_item.get(&entry.item.id()) {
+ f(entry, path.clone());
+ } else if let Some(item) = entry.item.upgrade(cx) {
+ let path = item.project_path(cx);
+ if let Some(path) = path {
+ f(entry, path);
+ }
+ }
+ })
+ }
}
pub struct PaneBackdrop<V: View> {
@@ -1884,7 +1970,7 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
- let background = cx.global::<Settings>().theme.editor.background;
+ let background = theme::current(cx).editor.background;
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
@@ -1948,10 +2034,11 @@ mod tests {
use crate::item::test::{TestItem, TestProjectItem};
use gpui::{executor::Deterministic, TestAppContext};
use project::FakeFs;
+ use settings::SettingsStore;
#[gpui::test]
async fn test_remove_active_empty(cx: &mut TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -1966,7 +2053,7 @@ mod tests {
#[gpui::test]
async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2054,7 +2141,7 @@ mod tests {
#[gpui::test]
async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2130,7 +2217,7 @@ mod tests {
#[gpui::test]
async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2239,7 +2326,7 @@ mod tests {
#[gpui::test]
async fn test_remove_item_ordering(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2278,7 +2365,7 @@ mod tests {
#[gpui::test]
async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2297,7 +2384,7 @@ mod tests {
#[gpui::test]
async fn test_close_clean_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2322,7 +2409,7 @@ mod tests {
deterministic: Arc<Deterministic>,
cx: &mut TestAppContext,
) {
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2344,7 +2431,7 @@ mod tests {
deterministic: Arc<Deterministic>,
cx: &mut TestAppContext,
) {
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2363,7 +2450,7 @@ mod tests {
#[gpui::test]
async fn test_close_all_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -2381,6 +2468,14 @@ mod tests {
assert_item_labels(&pane, [], cx);
}
+ fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ crate::init_settings(cx);
+ });
+ }
+
fn add_labeled_item(
workspace: &ViewHandle<Workspace>,
pane: &ViewHandle<Pane>,
@@ -1,3 +1,5 @@
+use super::DraggedItem;
+use crate::{Pane, SplitDirection, Workspace};
use drag_and_drop::DragAndDrop;
use gpui::{
color::Color,
@@ -8,11 +10,6 @@ use gpui::{
AppContext, Element, EventContext, MouseState, Quad, View, ViewContext, WeakViewHandle,
};
use project::ProjectEntryId;
-use settings::Settings;
-
-use crate::{Pane, SplitDirection, Workspace};
-
-use super::DraggedItem;
pub fn dragged_item_receiver<Tag, D, F>(
pane: &Pane,
@@ -234,8 +231,5 @@ fn drop_split_direction(
}
fn overlay_color(cx: &AppContext) -> Color {
- cx.global::<Settings>()
- .theme
- .workspace
- .drop_target_overlay_color
+ theme::current(cx).workspace.drop_target_overlay_color
}
@@ -1,6 +1,6 @@
use std::sync::Arc;
-use crate::{AppState, FollowerStatesByLeader, Pane, Workspace};
+use crate::{AppState, FollowerStatesByLeader, Pane, Workspace, WorkspaceSettings};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use gpui::{
@@ -11,7 +11,6 @@ use gpui::{
};
use project::Project;
use serde::Deserialize;
-use settings::Settings;
use theme::Theme;
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -391,7 +390,7 @@ impl PaneAxis {
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
let mut flex = 1.0;
if member.contains(active_pane) {
- flex = cx.global::<Settings>().active_pane_magnification;
+ flex = settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
}
let mut member = member.render(
@@ -497,10 +497,8 @@ impl WorkspaceDb {
#[cfg(test)]
mod tests {
-
- use db::open_test_db;
-
use super::*;
+ use db::open_test_db;
#[gpui::test]
async fn test_next_id_stability() {
@@ -1,4 +1,4 @@
-use crate::{ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
+use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
use anyhow::{anyhow, Context, Result};
use async_recursion::async_recursion;
use db::sqlez::{
@@ -150,17 +150,23 @@ impl SerializedPaneGroup {
workspace_id: WorkspaceId,
workspace: &WeakViewHandle<Workspace>,
cx: &mut AsyncAppContext,
- ) -> Option<(Member, Option<ViewHandle<Pane>>)> {
+ ) -> Option<(
+ Member,
+ Option<ViewHandle<Pane>>,
+ Vec<Option<Box<dyn ItemHandle>>>,
+ )> {
match self {
SerializedPaneGroup::Group { axis, children } => {
let mut current_active_pane = None;
let mut members = Vec::new();
+ let mut items = Vec::new();
for child in children {
- if let Some((new_member, active_pane)) = child
+ if let Some((new_member, active_pane, new_items)) = child
.deserialize(project, workspace_id, workspace, cx)
.await
{
members.push(new_member);
+ items.extend(new_items);
current_active_pane = current_active_pane.or(active_pane);
}
}
@@ -170,7 +176,7 @@ impl SerializedPaneGroup {
}
if members.len() == 1 {
- return Some((members.remove(0), current_active_pane));
+ return Some((members.remove(0), current_active_pane, items));
}
Some((
@@ -179,6 +185,7 @@ impl SerializedPaneGroup {
members,
}),
current_active_pane,
+ items,
))
}
SerializedPaneGroup::Pane(serialized_pane) => {
@@ -186,7 +193,7 @@ impl SerializedPaneGroup {
.update(cx, |workspace, cx| workspace.add_pane(cx).downgrade())
.log_err()?;
let active = serialized_pane.active;
- serialized_pane
+ let new_items = serialized_pane
.deserialize_to(project, &pane, workspace_id, workspace, cx)
.await
.log_err()?;
@@ -196,7 +203,7 @@ impl SerializedPaneGroup {
.log_err()?
{
let pane = pane.upgrade(cx)?;
- Some((Member::Pane(pane.clone()), active.then(|| pane)))
+ Some((Member::Pane(pane.clone()), active.then(|| pane), new_items))
} else {
let pane = pane.upgrade(cx)?;
workspace
@@ -227,7 +234,8 @@ impl SerializedPane {
workspace_id: WorkspaceId,
workspace: &WeakViewHandle<Workspace>,
cx: &mut AsyncAppContext,
- ) -> Result<()> {
+ ) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
+ let mut items = Vec::new();
let mut active_item_index = None;
for (index, item) in self.children.iter().enumerate() {
let project = project.clone();
@@ -245,6 +253,8 @@ impl SerializedPane {
.await
.log_err();
+ items.push(item_handle.clone());
+
if let Some(item_handle) = item_handle {
workspace.update(cx, |workspace, cx| {
let pane_handle = pane_handle
@@ -266,7 +276,7 @@ impl SerializedPane {
})?;
}
- anyhow::Ok(())
+ anyhow::Ok(items)
}
}
@@ -330,40 +340,3 @@ impl Column for SerializedItem {
))
}
}
-
-#[cfg(test)]
-mod tests {
- use db::sqlez::connection::Connection;
-
- // use super::WorkspaceLocation;
-
- #[test]
- fn test_workspace_round_trips() {
- let _db = Connection::open_memory(Some("workspace_id_round_trips"));
-
- todo!();
- // 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
- // ))
- // );
- }
-}
@@ -12,7 +12,6 @@ use gpui::{
platform::MouseButton,
AppContext, Entity, Task, View, ViewContext,
};
-use settings::Settings;
use smallvec::SmallVec;
use std::{
borrow::Cow,
@@ -88,7 +87,7 @@ impl View for SharedScreen {
}
})
.contained()
- .with_style(cx.global::<Settings>().theme.shared_screen)
+ .with_style(theme::current(cx).shared_screen)
})
.on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
.into_any()
@@ -11,7 +11,6 @@ use gpui::{
AnyElement, AnyViewHandle, Entity, LayoutContext, SceneBuilder, SizeConstraint, Subscription,
View, ViewContext, ViewHandle, WindowContext,
};
-use settings::Settings;
pub trait StatusItemView: View {
fn set_active_pane_item(
@@ -47,7 +46,7 @@ impl View for StatusBar {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &cx.global::<Settings>().theme.workspace.status_bar;
+ let theme = &theme::current(cx).workspace.status_bar;
StatusBarElement {
left: Flex::row()
@@ -3,7 +3,6 @@ use gpui::{
elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle,
AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
-use settings::Settings;
pub trait ToolbarItemView: View {
fn set_active_pane_item(
@@ -68,7 +67,7 @@ impl View for Toolbar {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &cx.global::<Settings>().theme.workspace.toolbar;
+ let theme = &theme::current(cx).workspace.toolbar;
let mut primary_left_items = Vec::new();
let mut primary_right_items = Vec::new();
@@ -131,7 +130,7 @@ impl View for Toolbar {
let height = theme.height * primary_items_row_count as f32;
let nav_button_height = theme.height;
let button_style = theme.nav_button;
- let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+ let tooltip_style = theme::current(cx).tooltip.clone();
Flex::column()
.with_child(
@@ -12,6 +12,7 @@ pub mod searchable;
pub mod shared_screen;
mod status_bar;
mod toolbar;
+mod workspace_settings;
use anyhow::{anyhow, Context, Result};
use assets::Assets;
@@ -44,6 +45,7 @@ use gpui::{
WindowContext,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
+use itertools::Itertools;
use language::{LanguageRegistry, Rope};
use std::{
any::TypeId,
@@ -52,7 +54,7 @@ use std::{
future::Future,
path::{Path, PathBuf},
str,
- sync::Arc,
+ sync::{atomic::AtomicUsize, Arc},
time::Duration,
};
@@ -75,13 +77,13 @@ pub use persistence::{
use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use serde::Deserialize;
-use settings::{Autosave, Settings};
use shared_screen::SharedScreen;
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
-use theme::{Theme, ThemeRegistry};
+use theme::Theme;
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use util::{paths, ResultExt};
+use util::{async_iife, paths, ResultExt};
+pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
lazy_static! {
static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
@@ -184,7 +186,12 @@ pub type WorkspaceId = i64;
impl_actions!(workspace, [ActivatePane]);
+pub fn init_settings(cx: &mut AppContext) {
+ settings::register::<WorkspaceSettings>(cx);
+}
+
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+ init_settings(cx);
pane::init(cx);
notifications::init(cx);
@@ -234,7 +241,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
},
);
cx.add_action(Workspace::toggle_panel);
- cx.add_action(Workspace::focus_center);
cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
workspace.activate_previous_pane(cx)
});
@@ -270,7 +276,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.add_action(
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
- Settings::initial_user_settings_content(&Assets)
+ settings::initial_user_settings_content(&Assets)
.as_ref()
.into()
})
@@ -355,7 +361,6 @@ pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
pub struct AppState {
pub languages: Arc<LanguageRegistry>,
- pub themes: Arc<ThemeRegistry>,
pub client: Arc<client::Client>,
pub user_store: ModelHandle<client::UserStore>,
pub fs: Arc<dyn fs::Fs>,
@@ -369,18 +374,24 @@ pub struct AppState {
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> {
- let settings = Settings::test(cx);
- cx.set_global(settings);
+ use settings::SettingsStore;
+
+ if !cx.has_global::<SettingsStore>() {
+ cx.set_global(SettingsStore::test(cx));
+ }
let fs = fs::FakeFs::new(cx.background().clone());
let languages = Arc::new(LanguageRegistry::test());
let http_client = util::http::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());
+
+ theme::init((), cx);
+ client::init(&client, cx);
+ crate::init_settings(cx);
+
Arc::new(Self {
client,
- themes,
fs,
languages,
user_store,
@@ -469,6 +480,7 @@ pub struct Workspace {
_subscriptions: Vec<Subscription>,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<Result<()>>,
+ pane_history_timestamp: Arc<AtomicUsize>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
@@ -492,7 +504,6 @@ struct FollowerState {
impl Workspace {
pub fn new(
- serialized_workspace: Option<SerializedWorkspace>,
workspace_id: WorkspaceId,
project: ModelHandle<Project>,
app_state: Arc<AppState>,
@@ -524,6 +535,14 @@ impl Workspace {
cx.remove_window();
}
+ project::Event::DeletedEntry(entry_id) => {
+ for pane in this.panes.iter() {
+ pane.update(cx, |pane, cx| {
+ pane.handle_deleted_project_item(*entry_id, cx)
+ });
+ }
+ }
+
_ => {}
}
cx.notify()
@@ -531,9 +550,16 @@ impl Workspace {
.detach();
let weak_handle = cx.weak_handle();
-
- let center_pane =
- cx.add_view(|cx| Pane::new(weak_handle.clone(), app_state.background_actions, cx));
+ let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
+
+ let center_pane = cx.add_view(|cx| {
+ Pane::new(
+ weak_handle.clone(),
+ app_state.background_actions,
+ pane_history_timestamp.clone(),
+ cx,
+ )
+ });
cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
cx.focus(¢er_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
@@ -658,16 +684,10 @@ impl Workspace {
_apply_leader_updates,
leader_updates_tx,
_subscriptions: subscriptions,
+ pane_history_timestamp,
};
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
}
@@ -689,18 +709,15 @@ impl Workspace {
);
cx.spawn(|mut cx| async move {
- let mut serialized_workspace =
- persistence::DB.workspace_for_roots(&abs_paths.as_slice());
+ 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));
+ let paths_to_open = 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() {
+ let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
+ Vec::with_capacity(paths_to_open.len());
+ for path in paths_to_open.iter().cloned() {
if let Some((worktree, project_entry)) = cx
.update(|cx| {
Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
@@ -709,9 +726,9 @@ impl Workspace {
.log_err()
{
worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
- project_paths.push(Some(project_entry));
+ project_paths.push((path, Some(project_entry)));
} else {
- project_paths.push(None);
+ project_paths.push((path, None));
}
}
@@ -731,21 +748,13 @@ impl Workspace {
))
});
- let was_deserialized = serialized_workspace.is_some();
+ let build_workspace = |cx: &mut ViewContext<Workspace>| {
+ Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
+ };
let workspace = requesting_window_id
.and_then(|window_id| {
- cx.update(|cx| {
- cx.replace_root_view(window_id, |cx| {
- Workspace::new(
- serialized_workspace.take(),
- workspace_id,
- project_handle.clone(),
- app_state.clone(),
- cx,
- )
- })
- })
+ cx.update(|cx| cx.replace_root_view(window_id, |cx| build_workspace(cx)))
})
.unwrap_or_else(|| {
let (bounds, display) = if let Some(bounds) = window_bounds_override {
@@ -783,22 +792,14 @@ impl Workspace {
// Use the serialized workspace to construct the new window
cx.add_window(
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
- |cx| {
- Workspace::new(
- serialized_workspace,
- workspace_id,
- project_handle.clone(),
- app_state.clone(),
- cx,
- )
- },
+ |cx| build_workspace(cx),
)
.1
});
(app_state.initialize_workspace)(
workspace.downgrade(),
- was_deserialized,
+ serialized_workspace.is_some(),
app_state.clone(),
cx.clone(),
)
@@ -809,37 +810,14 @@ impl Workspace {
let workspace = workspace.downgrade();
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)
- })
- .log_err()?
- .await,
- )
- } else {
- None
- }
- }
- })
- });
-
- let opened_items = futures::future::join_all(tasks.into_iter()).await;
+ let opened_items = open_items(
+ serialized_workspace,
+ &workspace,
+ project_paths,
+ app_state,
+ cx,
+ )
+ .await;
(workspace, opened_items)
})
@@ -936,6 +914,39 @@ impl Workspace {
&self.project
}
+ pub fn recent_navigation_history(
+ &self,
+ limit: Option<usize>,
+ cx: &AppContext,
+ ) -> Vec<ProjectPath> {
+ let mut history: HashMap<ProjectPath, usize> = HashMap::default();
+ for pane in &self.panes {
+ let pane = pane.read(cx);
+ pane.nav_history()
+ .for_each_entry(cx, |entry, project_path| {
+ let timestamp = entry.timestamp;
+ match history.entry(project_path) {
+ hash_map::Entry::Occupied(mut entry) => {
+ if ×tamp > entry.get() {
+ entry.insert(timestamp);
+ }
+ }
+ hash_map::Entry::Vacant(entry) => {
+ entry.insert(timestamp);
+ }
+ }
+ });
+ }
+
+ history
+ .into_iter()
+ .sorted_by_key(|(_, timestamp)| *timestamp)
+ .map(|(project_path, _)| project_path)
+ .rev()
+ .take(limit.unwrap_or(usize::MAX))
+ .collect()
+ }
+
pub fn client(&self) -> &Client {
&self.app_state.client
}
@@ -1193,6 +1204,8 @@ impl Workspace {
visible: bool,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
+ log::info!("open paths {:?}", abs_paths);
+
let fs = self.app_state.fs.clone();
// Sort the paths to ensure we add worktrees for parents before their children.
@@ -1527,14 +1540,15 @@ impl Workspace {
cx.notify();
}
- pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.focus_self();
- cx.notify();
- }
-
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
- let pane =
- cx.add_view(|cx| Pane::new(self.weak_handle(), self.app_state.background_actions, cx));
+ let pane = cx.add_view(|cx| {
+ Pane::new(
+ self.weak_handle(),
+ self.app_state.background_actions,
+ self.pane_history_timestamp.clone(),
+ cx,
+ )
+ });
cx.subscribe(&pane, Self::handle_pane_event).detach();
self.panes.push(pane.clone());
cx.focus(&pane);
@@ -2103,7 +2117,7 @@ impl Workspace {
enum DisconnectedOverlay {}
Some(
MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
- let theme = &cx.global::<Settings>().theme;
+ let theme = &theme::current(cx);
Label::new(
"Your connection to the remote project has been lost.",
theme.workspace.disconnected_overlay.text.clone(),
@@ -2474,8 +2488,8 @@ impl Workspace {
item.workspace_deactivated(cx);
}
if matches!(
- cx.global::<Settings>().autosave,
- Autosave::OnWindowChange | Autosave::OnFocusChange
+ settings::get::<WorkspaceSettings>(cx).autosave,
+ AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
) {
for item in pane.items() {
Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
@@ -2659,91 +2673,125 @@ impl Workspace {
}
}
- fn load_from_serialized_workspace(
+ pub(crate) fn load_workspace(
workspace: WeakViewHandle<Workspace>,
serialized_workspace: SerializedWorkspace,
+ paths_to_open: Vec<Option<ProjectPath>>,
cx: &mut AppContext,
- ) {
+ ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
cx.spawn(|mut cx| async move {
- let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
- (
- workspace.project().clone(),
- workspace.last_active_center_pane.clone(),
- )
- })?;
+ let result = async_iife! {{
+ let (project, old_center_pane) =
+ workspace.read_with(&cx, |workspace, _| {
+ (
+ workspace.project().clone(),
+ workspace.last_active_center_pane.clone(),
+ )
+ })?;
- // Traverse the splits tree and add to things
- let center_group = serialized_workspace
- .center_group
- .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
- .await;
+ let mut center_items = None;
+ let mut center_group = None;
+ // Traverse the splits tree and add to things
+ if let Some((group, active_pane, items)) = serialized_workspace
+ .center_group
+ .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
+ .await {
+ center_items = Some(items);
+ center_group = Some((group, active_pane))
+ }
- // Remove old panes from workspace panes list
- workspace.update(&mut cx, |workspace, cx| {
- if let Some((center_group, active_pane)) = center_group {
- workspace.remove_panes(workspace.center.root.clone(), cx);
+ let resulting_list = cx.read(|cx| {
+ let mut opened_items = center_items
+ .unwrap_or_default()
+ .into_iter()
+ .filter_map(|item| {
+ let item = item?;
+ let project_path = item.project_path(cx)?;
+ Some((project_path, item))
+ })
+ .collect::<HashMap<_, _>>();
- // Swap workspace center group
- workspace.center = PaneGroup::with_root(center_group);
+ paths_to_open
+ .into_iter()
+ .map(|path_to_open| {
+ path_to_open.map(|path_to_open| {
+ Ok(opened_items.remove(&path_to_open))
+ })
+ .transpose()
+ .map(|item| item.flatten())
+ .transpose()
+ })
+ .collect::<Vec<_>>()
+ });
- // Change the focus to the workspace first so that we retrigger focus in on the pane.
- cx.focus_self();
+ // Remove old panes from workspace panes list
+ workspace.update(&mut cx, |workspace, cx| {
+ if let Some((center_group, active_pane)) = center_group {
+ workspace.remove_panes(workspace.center.root.clone(), cx);
- if let Some(active_pane) = active_pane {
- cx.focus(&active_pane);
- } else {
- cx.focus(workspace.panes.last().unwrap());
- }
- } else {
- let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
- if let Some(old_center_handle) = old_center_handle {
- cx.focus(&old_center_handle)
+ // Swap workspace center group
+ workspace.center = PaneGroup::with_root(center_group);
+
+ // Change the focus to the workspace first so that we retrigger focus in on the pane.
+ cx.focus_self();
+
+ if let Some(active_pane) = active_pane {
+ cx.focus(&active_pane);
+ } else {
+ cx.focus(workspace.panes.last().unwrap());
+ }
} else {
- cx.focus_self()
+ let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
+ if let Some(old_center_handle) = old_center_handle {
+ cx.focus(&old_center_handle)
+ } else {
+ cx.focus_self()
+ }
}
- }
- let docks = serialized_workspace.docks;
- workspace.left_dock.update(cx, |dock, cx| {
- dock.set_open(docks.left.visible, cx);
- if let Some(active_panel) = docks.left.active_panel {
- if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
+ let docks = serialized_workspace.docks;
+ workspace.left_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.left.visible, cx);
+ if let Some(active_panel) = docks.left.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
}
- }
- });
- workspace.right_dock.update(cx, |dock, cx| {
- dock.set_open(docks.right.visible, cx);
- if let Some(active_panel) = docks.right.active_panel {
- if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
+ });
+ workspace.right_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.right.visible, cx);
+ if let Some(active_panel) = docks.right.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
}
- }
- });
- workspace.bottom_dock.update(cx, |dock, cx| {
- dock.set_open(docks.bottom.visible, cx);
- if let Some(active_panel) = docks.bottom.active_panel {
- if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
+ });
+ workspace.bottom_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.bottom.visible, cx);
+ if let Some(active_panel) = docks.bottom.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
}
- }
- });
+ });
- cx.notify();
- })?;
+ cx.notify();
+ })?;
- // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
- workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
- anyhow::Ok(())
+ // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
+ workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
+
+ Ok::<_, anyhow::Error>(resulting_list)
+ }};
+
+ result.await.unwrap_or_default()
})
- .detach_and_log_err(cx);
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
- themes: ThemeRegistry::new((), cx.font_cache().clone()),
client: project.read(cx).client(),
user_store: project.read(cx).user_store(),
fs: project.read(cx).fs().clone(),
@@ -2751,7 +2799,7 @@ impl Workspace {
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
});
- Self::new(None, 0, project, app_state, cx)
+ Self::new(0, project, app_state, cx)
}
fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
@@ -2782,6 +2830,95 @@ impl Workspace {
}
}
+async fn open_items(
+ serialized_workspace: Option<SerializedWorkspace>,
+ workspace: &WeakViewHandle<Workspace>,
+ mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
+ app_state: Arc<AppState>,
+ mut cx: AsyncAppContext,
+) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
+ let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
+
+ if let Some(serialized_workspace) = serialized_workspace {
+ let workspace = workspace.clone();
+ let restored_items = cx
+ .update(|cx| {
+ Workspace::load_workspace(
+ workspace,
+ serialized_workspace,
+ project_paths_to_open
+ .iter()
+ .map(|(_, project_path)| project_path)
+ .cloned()
+ .collect(),
+ cx,
+ )
+ })
+ .await;
+
+ let restored_project_paths = cx.read(|cx| {
+ restored_items
+ .iter()
+ .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
+ .collect::<HashSet<_>>()
+ });
+
+ opened_items = restored_items;
+ project_paths_to_open
+ .iter_mut()
+ .for_each(|(_, project_path)| {
+ if let Some(project_path_to_open) = project_path {
+ if restored_project_paths.contains(project_path_to_open) {
+ *project_path = None;
+ }
+ }
+ });
+ } else {
+ for _ in 0..project_paths_to_open.len() {
+ opened_items.push(None);
+ }
+ }
+ assert!(opened_items.len() == project_paths_to_open.len());
+
+ let tasks =
+ project_paths_to_open
+ .into_iter()
+ .enumerate()
+ .map(|(i, (abs_path, project_path))| {
+ let workspace = workspace.clone();
+ cx.spawn(|mut cx| {
+ let fs = app_state.fs.clone();
+ async move {
+ let file_project_path = project_path?;
+ if fs.is_file(&abs_path).await {
+ Some((
+ i,
+ workspace
+ .update(&mut cx, |workspace, cx| {
+ workspace.open_path(file_project_path, None, true, cx)
+ })
+ .log_err()?
+ .await,
+ ))
+ } else {
+ None
+ }
+ }
+ })
+ });
+
+ for maybe_opened_path in futures::future::join_all(tasks.into_iter())
+ .await
+ .into_iter()
+ {
+ if let Some((i, path_open_result)) = maybe_opened_path {
+ opened_items[i] = Some(path_open_result);
+ }
+ }
+
+ opened_items
+}
+
fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
@@ -2826,7 +2963,7 @@ impl View for Workspace {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = cx.global::<Settings>().theme.clone();
+ let theme = theme::current(cx).clone();
Stack::new()
.with_child(
Flex::column()
@@ -2992,8 +3129,6 @@ pub fn open_paths(
Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
)>,
> {
- log::info!("open paths {:?}", abs_paths);
-
let app_state = app_state.clone();
let abs_paths = abs_paths.to_vec();
cx.spawn(|mut cx| async move {
@@ -3105,7 +3240,7 @@ pub fn join_remote_project(
let (_, workspace) = cx.add_window(
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
- |cx| Workspace::new(None, 0, project, app_state.clone(), cx),
+ |cx| Workspace::new(0, project, app_state.clone(), cx),
);
(app_state.initialize_workspace)(
workspace.downgrade(),
@@ -3156,7 +3291,7 @@ pub fn join_remote_project(
}
pub fn restart(_: &Restart, cx: &mut AppContext) {
- let should_confirm = cx.global::<Settings>().confirm_quit;
+ let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
cx.spawn(|mut cx| async move {
let mut workspaces = cx
.window_ids()
@@ -3217,23 +3352,21 @@ fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
#[cfg(test)]
mod tests {
- use std::{cell::RefCell, rc::Rc};
-
+ use super::*;
use crate::{
dock::test::{TestPanel, TestPanelEvent},
item::test::{TestItem, TestItemEvent, TestProjectItem},
};
-
- use super::*;
use fs::FakeFs;
use gpui::{executor::Deterministic, TestAppContext};
use project::{Project, ProjectEntryId};
use serde_json::json;
+ use settings::SettingsStore;
+ use std::{cell::RefCell, rc::Rc};
#[gpui::test]
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
- cx.foreground().forbid_parking();
- Settings::test_async(cx);
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
@@ -3281,8 +3414,8 @@ mod tests {
#[gpui::test]
async fn test_tracking_active_path(cx: &mut TestAppContext) {
- cx.foreground().forbid_parking();
- Settings::test_async(cx);
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root1",
@@ -3385,8 +3518,8 @@ mod tests {
#[gpui::test]
async fn test_close_window(cx: &mut TestAppContext) {
- cx.foreground().forbid_parking();
- Settings::test_async(cx);
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
fs.insert_tree("/root", json!({ "one": "" })).await;
@@ -3421,8 +3554,8 @@ mod tests {
#[gpui::test]
async fn test_close_pane_items(cx: &mut TestAppContext) {
- cx.foreground().forbid_parking();
- Settings::test_async(cx);
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
@@ -3523,8 +3656,8 @@ mod tests {
#[gpui::test]
async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
- cx.foreground().forbid_parking();
- Settings::test_async(cx);
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
@@ -3626,9 +3759,8 @@ mod tests {
#[gpui::test]
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
- deterministic.forbid_parking();
+ init_test(cx);
- Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
@@ -3645,8 +3777,10 @@ mod tests {
// Autosave on window change.
item.update(cx, |item, cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.autosave = Autosave::OnWindowChange;
+ cx.update_global(|settings: &mut SettingsStore, cx| {
+ settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+ settings.autosave = Some(AutosaveSetting::OnWindowChange);
+ })
});
item.is_dirty = true;
});
@@ -3659,8 +3793,10 @@ mod tests {
// Autosave on focus change.
item.update(cx, |item, cx| {
cx.focus_self();
- cx.update_global(|settings: &mut Settings, _| {
- settings.autosave = Autosave::OnFocusChange;
+ cx.update_global(|settings: &mut SettingsStore, cx| {
+ settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+ settings.autosave = Some(AutosaveSetting::OnFocusChange);
+ })
});
item.is_dirty = true;
});
@@ -3683,8 +3819,10 @@ mod tests {
// Autosave after delay.
item.update(cx, |item, cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
+ cx.update_global(|settings: &mut SettingsStore, cx| {
+ settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+ settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
+ })
});
item.is_dirty = true;
cx.emit(TestItemEvent::Edit);
@@ -3700,8 +3838,10 @@ mod tests {
// Autosave on focus change, ensuring closing the tab counts as such.
item.update(cx, |item, cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.autosave = Autosave::OnFocusChange;
+ cx.update_global(|settings: &mut SettingsStore, cx| {
+ settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+ settings.autosave = Some(AutosaveSetting::OnFocusChange);
+ })
});
item.is_dirty = true;
});
@@ -3735,12 +3875,9 @@ mod tests {
}
#[gpui::test]
- async fn test_pane_navigation(
- deterministic: Arc<Deterministic>,
- cx: &mut gpui::TestAppContext,
- ) {
- deterministic.forbid_parking();
- Settings::test_async(cx);
+ async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
@@ -3794,9 +3931,8 @@ mod tests {
}
#[gpui::test]
- async fn test_panels(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
- deterministic.forbid_parking();
- Settings::test_async(cx);
+ async fn test_panels(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
@@ -3942,4 +4078,14 @@ mod tests {
assert!(!left_dock.read(cx).is_open());
});
}
+
+ pub fn init_test(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ language::init(cx);
+ crate::init_settings(cx);
+ });
+ }
}
@@ -0,0 +1,58 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize)]
+pub struct WorkspaceSettings {
+ pub active_pane_magnification: f32,
+ pub confirm_quit: bool,
+ pub show_call_status_icon: bool,
+ pub autosave: AutosaveSetting,
+ pub git: GitSettings,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct WorkspaceSettingsContent {
+ pub active_pane_magnification: Option<f32>,
+ pub confirm_quit: Option<bool>,
+ pub show_call_status_icon: Option<bool>,
+ pub autosave: Option<AutosaveSetting>,
+ pub git: Option<GitSettings>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AutosaveSetting {
+ Off,
+ AfterDelay { milliseconds: u64 },
+ OnFocusChange,
+ OnWindowChange,
+}
+
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct GitSettings {
+ pub git_gutter: Option<GitGutterSetting>,
+ pub gutter_debounce: Option<u64>,
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitGutterSetting {
+ #[default]
+ TrackedFiles,
+ Hide,
+}
+
+impl Setting for WorkspaceSettings {
+ const KEY: Option<&'static str> = None;
+
+ type FileContent = WorkspaceSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &gpui::AppContext,
+ ) -> anyhow::Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
-version = "0.86.0"
+version = "0.88.0"
publish = false
[lib]
@@ -101,7 +101,7 @@ smol.workspace = true
tempdir.workspace = true
thiserror.workspace = true
tiny_http = "0.8"
-toml = "0.5"
+toml.workspace = true
tree-sitter = "0.20"
tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0"
@@ -3,7 +3,6 @@ pub use language::*;
use node_runtime::NodeRuntime;
use rust_embed::RustEmbed;
use std::{borrow::Cow, str, sync::Arc};
-use theme::ThemeRegistry;
mod c;
mod elixir;
@@ -32,11 +31,7 @@ mod yaml;
#[exclude = "*.rs"]
struct LanguageDir;
-pub fn init(
- languages: Arc<LanguageRegistry>,
- themes: Arc<ThemeRegistry>,
- node_runtime: Arc<NodeRuntime>,
-) {
+pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
fn adapter_arc(adapter: impl LspAdapter) -> Arc<dyn LspAdapter> {
Arc::new(adapter)
}
@@ -69,7 +64,6 @@ pub fn init(
vec![adapter_arc(json::JsonLspAdapter::new(
node_runtime.clone(),
languages.clone(),
- themes.clone(),
))],
),
("markdown", tree_sitter_markdown::language(), vec![]),
@@ -249,16 +249,21 @@ impl super::LspAdapter for CLspAdapter {
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
- use language::{AutoindentMode, Buffer};
- use settings::Settings;
+ use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
+ use settings::SettingsStore;
+ use std::num::NonZeroU32;
#[gpui::test]
async fn test_c_autoindent(cx: &mut TestAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.editor_overrides.tab_size = Some(2.try_into().unwrap());
- cx.set_global(settings);
+ cx.set_global(SettingsStore::test(cx));
+ language::init(cx);
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+ s.defaults.tab_size = NonZeroU32::new(2);
+ });
+ });
});
let language = crate::languages::language("c", tree_sitter_c::language(), None).await;
@@ -6,7 +6,7 @@ use gpui::AppContext;
use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
use node_runtime::NodeRuntime;
use serde_json::json;
-use settings::{keymap_file_json_schema, settings_file_json_schema};
+use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore};
use smol::fs;
use staff_mode::StaffMode;
use std::{
@@ -16,7 +16,6 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use theme::ThemeRegistry;
use util::http::HttpClient;
use util::{paths, ResultExt};
@@ -30,20 +29,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
pub struct JsonLspAdapter {
node: Arc<NodeRuntime>,
languages: Arc<LanguageRegistry>,
- themes: Arc<ThemeRegistry>,
}
impl JsonLspAdapter {
- pub fn new(
- node: Arc<NodeRuntime>,
- languages: Arc<LanguageRegistry>,
- themes: Arc<ThemeRegistry>,
- ) -> Self {
- JsonLspAdapter {
- node,
- languages,
- themes,
- }
+ pub fn new(node: Arc<NodeRuntime>, languages: Arc<LanguageRegistry>) -> Self {
+ JsonLspAdapter { node, languages }
}
}
@@ -128,12 +118,15 @@ impl LspAdapter for JsonLspAdapter {
cx: &mut AppContext,
) -> Option<BoxFuture<'static, serde_json::Value>> {
let action_names = cx.all_action_names().collect::<Vec<_>>();
- let theme_names = self
- .themes
- .list(**cx.default_global::<StaffMode>())
- .map(|meta| meta.name)
- .collect();
- let language_names = self.languages.language_names();
+ let staff_mode = cx.default_global::<StaffMode>().0;
+ let language_names = &self.languages.language_names();
+ let settings_schema = cx.global::<SettingsStore>().json_schema(
+ &SettingsJsonSchemaParams {
+ language_names,
+ staff_mode,
+ },
+ cx,
+ );
Some(
future::ready(serde_json::json!({
"json": {
@@ -143,7 +136,7 @@ impl LspAdapter for JsonLspAdapter {
"schemas": [
{
"fileMatch": [schema_file_match(&paths::SETTINGS)],
- "schema": settings_file_json_schema(theme_names, &language_names),
+ "schema": settings_schema,
},
{
"fileMatch": [schema_file_match(&paths::KEYMAP)],
@@ -170,8 +170,9 @@ impl LspAdapter for PythonLspAdapter {
#[cfg(test)]
mod tests {
use gpui::{ModelContext, TestAppContext};
- use language::{AutoindentMode, Buffer};
- use settings::Settings;
+ use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
+ use settings::SettingsStore;
+ use std::num::NonZeroU32;
#[gpui::test]
async fn test_python_autoindent(cx: &mut TestAppContext) {
@@ -179,9 +180,13 @@ mod tests {
let language =
crate::languages::language("python", tree_sitter_python::language(), None).await;
cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.editor_overrides.tab_size = Some(2.try_into().unwrap());
- cx.set_global(settings);
+ cx.set_global(SettingsStore::test(cx));
+ language::init(cx);
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+ s.defaults.tab_size = NonZeroU32::new(2);
+ });
+ });
});
cx.add_model(|cx| {
@@ -253,10 +253,13 @@ impl LspAdapter for RustLspAdapter {
#[cfg(test)]
mod tests {
+ use std::num::NonZeroU32;
+
use super::*;
use crate::languages::language;
use gpui::{color::Color, TestAppContext};
- use settings::Settings;
+ use language::language_settings::AllLanguageSettings;
+ use settings::SettingsStore;
use theme::SyntaxTheme;
#[gpui::test]
@@ -435,9 +438,13 @@ mod tests {
async fn test_rust_autoindent(cx: &mut TestAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
cx.update(|cx| {
- let mut settings = Settings::test(cx);
- settings.editor_overrides.tab_size = Some(2.try_into().unwrap());
- cx.set_global(settings);
+ cx.set_global(SettingsStore::test(cx));
+ language::init(cx);
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+ s.defaults.tab_size = NonZeroU32::new(2);
+ });
+ });
});
let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await;
@@ -2,10 +2,11 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::AppContext;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{
+ language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
+};
use node_runtime::NodeRuntime;
use serde_json::Value;
-use settings::Settings;
use smol::fs;
use std::{
any::Any,
@@ -100,14 +101,13 @@ impl LspAdapter for YamlLspAdapter {
}
fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
- let settings = cx.global::<Settings>();
Some(
future::ready(serde_json::json!({
"yaml": {
"keyOrdering": false
},
"[yaml]": {
- "editor.tabSize": settings.tab_size(Some("YAML"))
+ "editor.tabSize": language_settings(Some("YAML"), cx).tab_size,
}
}))
.boxed(),
@@ -6,55 +6,61 @@ use assets::Assets;
use backtrace::Backtrace;
use cli::{
ipc::{self, IpcSender},
- CliRequest, CliResponse, IpcHandshake,
+ CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
};
-use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE;
-use editor::Editor;
+use editor::{scroll::autoscroll::Autoscroll, Editor};
use futures::{
channel::{mpsc, oneshot},
FutureExt, SinkExt, StreamExt,
};
use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task, ViewContext};
use isahc::{config::Configurable, Request};
-use language::LanguageRegistry;
+use language::{LanguageRegistry, Point};
use log::LevelFilter;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
use project::Fs;
use serde::{Deserialize, Serialize};
-use settings::{
- self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent,
- WorkingDirectory,
-};
+use settings::{default_settings, handle_settings_file_changes, watch_config_file, SettingsStore};
use simplelog::ConfigBuilder;
use smol::process::Command;
use std::{
+ collections::HashMap,
env,
ffi::OsStr,
fs::OpenOptions,
io::Write as _,
os::unix::prelude::OsStrExt,
panic,
- path::PathBuf,
- sync::{Arc, Weak},
+ path::{Path, PathBuf},
+ str,
+ sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc, Weak,
+ },
thread,
time::Duration,
};
-use terminal_view::{get_working_directory, TerminalView};
-use util::http::{self, HttpClient};
+use sum_tree::Bias;
+use terminal_view::{get_working_directory, TerminalSettings, TerminalView};
+use util::{
+ http::{self, HttpClient},
+ paths::PathLikeWithPosition,
+};
use welcome::{show_welcome_experience, FIRST_OPEN};
use fs::RealFs;
-use settings::watched_json::WatchedJsonFile;
#[cfg(debug_assertions)]
use staff_mode::StaffMode;
-use theme::ThemeRegistry;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{
item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace,
};
-use zed::{self, build_window_options, initialize_workspace, languages, menus};
+use zed::{
+ self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
+};
fn main() {
let http = http::client();
@@ -74,10 +80,10 @@ fn main() {
load_embedded_fonts(&app);
let fs = Arc::new(RealFs);
-
- let themes = ThemeRegistry::new(Assets, app.font_cache());
- let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);
- let config_files = load_config_files(&app, fs.clone());
+ let user_settings_file_rx =
+ watch_config_file(app.background(), fs.clone(), paths::SETTINGS.clone());
+ let user_keymap_file_rx =
+ watch_config_file(app.background(), fs.clone(), paths::KEYMAP.clone());
let login_shell_env_loaded = if stdout_is_a_pty() {
Task::ready(())
@@ -88,29 +94,17 @@ fn main() {
};
let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded();
+ let cli_connections_tx = Arc::new(cli_connections_tx);
let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded();
+ let open_paths_tx = Arc::new(open_paths_tx);
+ let urls_callback_triggered = Arc::new(AtomicBool::new(false));
+
+ let callback_cli_connections_tx = Arc::clone(&cli_connections_tx);
+ let callback_open_paths_tx = Arc::clone(&open_paths_tx);
+ let callback_urls_callback_triggered = Arc::clone(&urls_callback_triggered);
app.on_open_urls(move |urls, _| {
- if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
- if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
- cli_connections_tx
- .unbounded_send(cli_connection)
- .map_err(|_| anyhow!("no listener for cli connections"))
- .log_err();
- };
- } else {
- let paths: Vec<_> = urls
- .iter()
- .flat_map(|url| url.strip_prefix("file://"))
- .map(|url| {
- let decoded = urlencoding::decode_binary(url.as_bytes());
- PathBuf::from(OsStr::from_bytes(decoded.as_ref()))
- })
- .collect();
- open_paths_tx
- .unbounded_send(paths)
- .map_err(|_| anyhow!("no listener for open urls requests"))
- .log_err();
- }
+ callback_urls_callback_triggered.store(true, Ordering::Release);
+ open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx);
})
.on_reopen(move |cx| {
if cx.has_global::<Weak<AppState>>() {
@@ -129,26 +123,13 @@ fn main() {
#[cfg(debug_assertions)]
cx.set_global(StaffMode(true));
- let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap();
-
- //Setup settings global before binding actions
- cx.set_global(SettingsFile::new(
- &paths::SETTINGS,
- settings_file_content.clone(),
- fs.clone(),
- ));
-
- settings::watch_files(
- default_settings,
- settings_file_content,
- themes.clone(),
- keymap_file,
- cx,
- );
-
- if !stdout_is_a_pty() {
- upload_previous_panics(http.clone(), cx);
- }
+ let mut store = SettingsStore::default();
+ store
+ .set_default_settings(default_settings().as_ref(), cx)
+ .unwrap();
+ cx.set_global(store);
+ handle_settings_file_changes(user_settings_file_rx, cx);
+ handle_keymap_file_changes(user_keymap_file_rx, cx);
let client = client::Client::new(http.clone(), cx);
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
@@ -157,15 +138,17 @@ fn main() {
let languages = Arc::new(languages);
let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
- languages::init(languages.clone(), themes.clone(), node_runtime.clone());
+ languages::init(languages.clone(), node_runtime.clone());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
cx.set_global(client.clone());
+ theme::init(Assets, cx);
context_menu::init(cx);
- project::Project::init(&client);
- client::init(client.clone(), cx);
+ project::Project::init(&client, cx);
+ client::init(&client, cx);
command_palette::init(cx);
+ language::init(cx);
editor::init(cx);
go_to_line::init(cx);
file_finder::init(cx);
@@ -179,13 +162,12 @@ fn main() {
theme_testbench::init(cx);
copilot::init(http.clone(), node_runtime, cx);
- cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
- .detach();
+ cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
- languages.set_theme(cx.global::<Settings>().theme.clone());
- cx.observe_global::<Settings, _>({
+ languages.set_theme(theme::current(cx).clone());
+ cx.observe_global::<SettingsStore, _>({
let languages = languages.clone();
- move |cx| languages.set_theme(cx.global::<Settings>().theme.clone())
+ move |cx| languages.set_theme(theme::current(cx).clone())
})
.detach();
@@ -193,12 +175,11 @@ fn main() {
client.telemetry().report_mixpanel_event(
"start app",
Default::default(),
- cx.global::<Settings>().telemetry(),
+ *settings::get::<TelemetrySettings>(cx),
);
let app_state = Arc::new(AppState {
languages,
- themes,
client: client.clone(),
user_store,
fs,
@@ -207,7 +188,7 @@ fn main() {
background_actions,
});
cx.set_global(Arc::downgrade(&app_state));
- auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
+ auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx);
recent_projects::init(cx);
@@ -215,10 +196,13 @@ fn main() {
journal::init(app_state.clone(), cx);
language_selector::init(cx);
theme_selector::init(cx);
- zed::init(&app_state, cx);
+ activity_indicator::init(cx);
+ lsp_log::init(cx);
+ call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
collab_ui::init(&app_state, cx);
feedback::init(cx);
welcome::init(cx);
+ zed::init(&app_state, cx);
cx.set_menus(menus::menus());
@@ -232,6 +216,16 @@ fn main() {
workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx);
}
} else {
+ upload_previous_panics(http.clone(), cx);
+
+ // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead
+ // of an *app, hence gets no specific callbacks run. Emulate them here, if needed.
+ if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
+ && !urls_callback_triggered.load(Ordering::Acquire)
+ {
+ open_urls(collect_url_args(), &cli_connections_tx, &open_paths_tx)
+ }
+
if let Ok(Some(connection)) = cli_connections_rx.try_next() {
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
.detach();
@@ -282,6 +276,37 @@ fn main() {
});
}
+fn open_urls(
+ urls: Vec<String>,
+ cli_connections_tx: &mpsc::UnboundedSender<(
+ mpsc::Receiver<CliRequest>,
+ IpcSender<CliResponse>,
+ )>,
+ open_paths_tx: &mpsc::UnboundedSender<Vec<PathBuf>>,
+) {
+ if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
+ if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
+ cli_connections_tx
+ .unbounded_send(cli_connection)
+ .map_err(|_| anyhow!("no listener for cli connections"))
+ .log_err();
+ };
+ } else {
+ let paths: Vec<_> = urls
+ .iter()
+ .flat_map(|url| url.strip_prefix("file://"))
+ .map(|url| {
+ let decoded = urlencoding::decode_binary(url.as_bytes());
+ PathBuf::from(OsStr::from_bytes(decoded.as_ref()))
+ })
+ .collect();
+ open_paths_tx
+ .unbounded_send(paths)
+ .map_err(|_| anyhow!("no listener for open urls requests"))
+ .log_err();
+ }
+}
+
async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
if let Some(location) = workspace::last_opened_workspace_paths().await {
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))
@@ -412,7 +437,7 @@ fn init_panic_hook(app_version: String) {
}
fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
- let diagnostics_telemetry = cx.global::<Settings>().telemetry_diagnostics();
+ let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
cx.background()
.spawn({
@@ -442,7 +467,7 @@ fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
continue;
};
- if diagnostics_telemetry {
+ if telemetry_settings.diagnostics {
let panic_data_text = smol::fs::read_to_string(&child_path)
.await
.context("error reading panic file")?;
@@ -512,7 +537,8 @@ async fn load_login_shell_environment() -> Result<()> {
}
fn stdout_is_a_pty() -> bool {
- unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 }
+ std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none()
+ && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 }
}
fn collect_path_args() -> Vec<PathBuf> {
@@ -525,7 +551,11 @@ fn collect_path_args() -> Vec<PathBuf> {
None
}
})
- .collect::<Vec<_>>()
+ .collect()
+}
+
+fn collect_url_args() -> Vec<String> {
+ env::args().skip(1).collect()
}
fn load_embedded_fonts(app: &App) {
@@ -547,11 +577,7 @@ fn load_embedded_fonts(app: &App) {
}
#[cfg(debug_assertions)]
-async fn watch_themes(
- fs: Arc<dyn Fs>,
- themes: Arc<ThemeRegistry>,
- mut cx: AsyncAppContext,
-) -> Option<()> {
+async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
let mut events = fs
.watch("styles/src".as_ref(), Duration::from_millis(100))
.await;
@@ -563,7 +589,7 @@ async fn watch_themes(
.await
.log_err()?;
if output.status.success() {
- cx.update(|cx| theme_selector::reload(themes.clone(), cx))
+ cx.update(|cx| theme_selector::reload(cx))
} else {
eprintln!(
"build script failed {}",
@@ -575,35 +601,10 @@ async fn watch_themes(
}
#[cfg(not(debug_assertions))]
-async fn watch_themes(
- _fs: Arc<dyn Fs>,
- _themes: Arc<ThemeRegistry>,
- _cx: AsyncAppContext,
-) -> Option<()> {
+async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
None
}
-fn load_config_files(
- app: &App,
- fs: Arc<dyn Fs>,
-) -> oneshot::Receiver<(
- WatchedJsonFile<SettingsFileContent>,
- WatchedJsonFile<KeymapFileContent>,
-)> {
- let executor = app.background();
- let (tx, rx) = oneshot::channel();
- executor
- .clone()
- .spawn(async move {
- let settings_file =
- 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();
- rx
-}
-
fn connect_to_cli(
server_name: &str,
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
@@ -641,13 +642,38 @@ async fn handle_cli_connection(
if let Some(request) = requests.next().await {
match request {
CliRequest::Open { paths, wait } => {
+ let mut caret_positions = HashMap::new();
+
let paths = if paths.is_empty() {
workspace::last_opened_workspace_paths()
.await
.map(|location| location.paths().to_vec())
- .unwrap_or(paths)
+ .unwrap_or_default()
} else {
paths
+ .into_iter()
+ .filter_map(|path_with_position_string| {
+ let path_with_position = PathLikeWithPosition::parse_str(
+ &path_with_position_string,
+ |path_str| {
+ Ok::<_, std::convert::Infallible>(
+ Path::new(path_str).to_path_buf(),
+ )
+ },
+ )
+ .expect("Infallible");
+ let path = path_with_position.path_like;
+ if let Some(row) = path_with_position.row {
+ if path.is_file() {
+ let row = row.saturating_sub(1);
+ let col =
+ path_with_position.column.unwrap_or(0).saturating_sub(1);
+ caret_positions.insert(path.clone(), Point::new(row, col));
+ }
+ }
+ Some(path)
+ })
+ .collect()
};
let mut errored = false;
@@ -657,11 +683,32 @@ async fn handle_cli_connection(
{
Ok((workspace, items)) => {
let mut item_release_futures = Vec::new();
- cx.update(|cx| {
- for (item, path) in items.into_iter().zip(&paths) {
- match item {
- Some(Ok(item)) => {
- let released = oneshot::channel();
+
+ for (item, path) in items.into_iter().zip(&paths) {
+ match item {
+ Some(Ok(item)) => {
+ if let Some(point) = caret_positions.remove(path) {
+ if let Some(active_editor) = item.downcast::<Editor>() {
+ active_editor
+ .downgrade()
+ .update(&mut cx, |editor, cx| {
+ let snapshot =
+ editor.snapshot(cx).display_snapshot;
+ let point = snapshot
+ .buffer_snapshot
+ .clip_point(point, Bias::Left);
+ editor.change_selections(
+ Some(Autoscroll::center()),
+ cx,
+ |s| s.select_ranges([point..point]),
+ );
+ })
+ .log_err();
+ }
+ }
+
+ let released = oneshot::channel();
+ cx.update(|cx| {
item.on_release(
cx,
Box::new(move |_| {
@@ -669,23 +716,20 @@ async fn handle_cli_connection(
}),
)
.detach();
- item_release_futures.push(released.1);
- }
- Some(Err(err)) => {
- responses
- .send(CliResponse::Stderr {
- message: format!(
- "error opening {:?}: {}",
- path, err
- ),
- })
- .log_err();
- errored = true;
- }
- None => {}
+ });
+ item_release_futures.push(released.1);
}
+ Some(Err(err)) => {
+ responses
+ .send(CliResponse::Stderr {
+ message: format!("error opening {:?}: {}", path, err),
+ })
+ .log_err();
+ errored = true;
+ }
+ None => {}
}
- });
+ }
if wait {
let background = cx.background();
@@ -748,13 +792,9 @@ pub fn dock_default_item_factory(
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> Option<Box<dyn ItemHandle>> {
- let strategy = cx
- .global::<Settings>()
- .terminal_overrides
+ let strategy = settings::get::<TerminalSettings>(cx)
.working_directory
- .clone()
- .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
-
+ .clone();
let working_directory = get_working_directory(workspace, cx, strategy);
let window_id = cx.window_id();
@@ -15,7 +15,7 @@ use anyhow::anyhow;
use feedback::{
feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
};
-use futures::StreamExt;
+use futures::{channel::mpsc, StreamExt};
use gpui::{
actions,
anyhow::{self, Result},
@@ -30,15 +30,16 @@ use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar};
use serde::Deserialize;
use serde_json::to_string_pretty;
-use settings::{Settings, DEFAULT_SETTINGS_ASSET_PATH};
+use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH};
use std::{borrow::Cow, str, sync::Arc};
use terminal_view::terminal_panel::{self, TerminalPanel};
use util::{channel::ReleaseChannel, paths, ResultExt};
use uuid::Uuid;
+use welcome::BaseKeymap;
pub use workspace;
use workspace::{
create_and_open_local_file, dock::PanelHandle, open_new, AppState, NewFile, NewWindow,
- Workspace,
+ Workspace, WorkspaceSettings,
};
#[derive(Deserialize, Clone, PartialEq)]
@@ -73,8 +74,6 @@ actions!(
]
);
-const MIN_FONT_SIZE: f32 = 6.0;
-
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
cx.add_action(about);
cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
@@ -118,30 +117,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
cx.add_global_action(quit);
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
- cx.update_global::<Settings, _, _>(|settings, cx| {
- settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
- if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
- *terminal_font_size = (*terminal_font_size + 1.0).max(MIN_FONT_SIZE);
- }
- cx.refresh_windows();
- });
+ theme::adjust_font_size(cx, |size| *size += 1.0)
});
cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
- cx.update_global::<Settings, _, _>(|settings, cx| {
- settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
- if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
- *terminal_font_size = (*terminal_font_size - 1.0).max(MIN_FONT_SIZE);
- }
- cx.refresh_windows();
- });
- });
- cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
- cx.update_global::<Settings, _, _>(|settings, cx| {
- settings.buffer_font_size = settings.default_buffer_font_size;
- settings.terminal_overrides.font_size = settings.terminal_defaults.font_size;
- cx.refresh_windows();
- });
+ theme::adjust_font_size(cx, |size| *size -= 1.0)
});
+ cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx));
cx.add_global_action(move |_: &install_cli::Install, cx| {
cx.spawn(|cx| async move {
install_cli::install_cli(&cx)
@@ -275,10 +256,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
}
}
});
- activity_indicator::init(cx);
- lsp_log::init(cx);
- call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
- settings::KeymapFileContent::load_defaults(cx);
+ load_default_keymap(cx);
}
pub fn initialize_workspace(
@@ -323,7 +301,8 @@ pub fn initialize_workspace(
cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
- let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
+ let copilot =
+ cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
let diagnostic_summary =
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
let activity_indicator = activity_indicator::ActivityIndicator::new(
@@ -408,7 +387,7 @@ pub fn build_window_options(
}
fn quit(_: &Quit, cx: &mut gpui::AppContext) {
- let should_confirm = cx.global::<Settings>().confirm_quit;
+ let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
cx.spawn(|mut cx| async move {
let mut workspaces = cx
.window_ids()
@@ -519,6 +498,51 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
.detach();
}
+pub fn load_default_keymap(cx: &mut AppContext) {
+ for path in ["keymaps/default.json", "keymaps/vim.json"] {
+ KeymapFileContent::load_asset(path, cx).unwrap();
+ }
+
+ if let Some(asset_path) = settings::get::<BaseKeymap>(cx).asset_path() {
+ KeymapFileContent::load_asset(asset_path, cx).unwrap();
+ }
+}
+
+pub fn handle_keymap_file_changes(
+ mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
+ cx: &mut AppContext,
+) {
+ cx.spawn(move |mut cx| async move {
+ let mut settings_subscription = None;
+ while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
+ if let Ok(keymap_content) = KeymapFileContent::parse(&user_keymap_content) {
+ cx.update(|cx| {
+ cx.clear_bindings();
+ load_default_keymap(cx);
+ keymap_content.clone().add_to_cx(cx).log_err();
+ });
+
+ let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
+ drop(settings_subscription);
+ settings_subscription = Some(cx.update(|cx| {
+ cx.observe_global::<SettingsStore, _>(move |cx| {
+ let new_base_keymap = *settings::get::<BaseKeymap>(cx);
+ if new_base_keymap != old_base_keymap {
+ old_base_keymap = new_base_keymap.clone();
+
+ cx.clear_bindings();
+ load_default_keymap(cx);
+ keymap_content.clone().add_to_cx(cx).log_err();
+ }
+ })
+ .detach();
+ }));
+ }
+ }
+ })
+ .detach();
+}
+
fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
workspace.with_local_workspace(cx, move |workspace, cx| {
let app_state = workspace.app_state().clone();
@@ -620,16 +644,21 @@ mod tests {
use super::*;
use assets::Assets;
use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
- use gpui::{executor::Deterministic, AppContext, AssetSource, TestAppContext, ViewHandle};
+ use fs::{FakeFs, Fs};
+ use gpui::{
+ elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, AssetSource,
+ Element, Entity, TestAppContext, View, ViewHandle,
+ };
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use project::{Project, ProjectPath};
use serde_json::json;
+ use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
- use theme::ThemeRegistry;
+ use theme::{ThemeRegistry, ThemeSettings};
use util::http::FakeHttpClient;
use workspace::{
item::{Item, ItemHandle},
@@ -638,7 +667,7 @@ mod tests {
#[gpui::test]
async fn test_open_paths_action(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -738,7 +767,7 @@ mod tests {
#[gpui::test]
async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -819,7 +848,7 @@ mod tests {
#[gpui::test]
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
cx.update(|cx| {
open_new(&app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
@@ -858,7 +887,7 @@ mod tests {
#[gpui::test]
async fn test_open_entry(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -971,7 +1000,7 @@ mod tests {
#[gpui::test]
async fn test_open_paths(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state
.fs
@@ -1141,7 +1170,7 @@ mod tests {
#[gpui::test]
async fn test_save_conflicting_item(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -1185,7 +1214,7 @@ mod tests {
#[gpui::test]
async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
@@ -1274,7 +1303,7 @@ mod tests {
#[gpui::test]
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
let project = Project::test(app_state.fs.clone(), [], cx).await;
@@ -1313,9 +1342,7 @@ mod tests {
#[gpui::test]
async fn test_pane_actions(cx: &mut TestAppContext) {
- init(cx);
-
- let app_state = cx.update(AppState::test);
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -1389,7 +1416,7 @@ mod tests {
#[gpui::test]
async fn test_navigation(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -1665,7 +1692,7 @@ mod tests {
#[gpui::test]
async fn test_reopening_closed_items(cx: &mut TestAppContext) {
- let app_state = init(cx);
+ let app_state = init_test(cx);
app_state
.fs
.as_fake()
@@ -1828,6 +1855,175 @@ mod tests {
}
}
+ #[gpui::test]
+ async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
+ struct TestView;
+
+ impl Entity for TestView {
+ type Event = ();
+ }
+
+ impl View for TestView {
+ fn ui_name() -> &'static str {
+ "TestView"
+ }
+
+ fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+ Empty::new().into_any()
+ }
+ }
+
+ let executor = cx.background();
+ let fs = FakeFs::new(executor.clone());
+
+ actions!(test, [A, B]);
+ // From the Atom keymap
+ actions!(workspace, [ActivatePreviousPane]);
+ // From the JetBrains keymap
+ actions!(pane, [ActivatePrevItem]);
+
+ fs.save(
+ "/settings.json".as_ref(),
+ &r#"
+ {
+ "base_keymap": "Atom"
+ }
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ fs.save(
+ "/keymap.json".as_ref(),
+ &r#"
+ [
+ {
+ "bindings": {
+ "backspace": "test::A"
+ }
+ }
+ ]
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init(Assets, cx);
+ welcome::init(cx);
+
+ cx.add_global_action(|_: &A, _cx| {});
+ cx.add_global_action(|_: &B, _cx| {});
+ cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+ cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+ let settings_rx = watch_config_file(
+ executor.clone(),
+ fs.clone(),
+ PathBuf::from("/settings.json"),
+ );
+ let keymap_rx =
+ watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+ handle_keymap_file_changes(keymap_rx, cx);
+ handle_settings_file_changes(settings_rx, cx);
+ });
+
+ cx.foreground().run_until_parked();
+
+ let (window_id, _view) = cx.add_window(|_| TestView);
+
+ // Test loading the keymap base at all
+ assert_key_bindings_for(
+ window_id,
+ cx,
+ vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+ line!(),
+ );
+
+ // Test modifying the users keymap, while retaining the base keymap
+ fs.save(
+ "/keymap.json".as_ref(),
+ &r#"
+ [
+ {
+ "bindings": {
+ "backspace": "test::B"
+ }
+ }
+ ]
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.foreground().run_until_parked();
+
+ assert_key_bindings_for(
+ window_id,
+ cx,
+ vec![("backspace", &B), ("k", &ActivatePreviousPane)],
+ line!(),
+ );
+
+ // Test modifying the base, while retaining the users keymap
+ fs.save(
+ "/settings.json".as_ref(),
+ &r#"
+ {
+ "base_keymap": "JetBrains"
+ }
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.foreground().run_until_parked();
+
+ assert_key_bindings_for(
+ window_id,
+ cx,
+ vec![("backspace", &B), ("[", &ActivatePrevItem)],
+ line!(),
+ );
+
+ fn assert_key_bindings_for<'a>(
+ window_id: usize,
+ cx: &TestAppContext,
+ actions: Vec<(&'static str, &'a dyn Action)>,
+ line: u32,
+ ) {
+ for (key, action) in actions {
+ // assert that...
+ assert!(
+ cx.available_actions(window_id, 0)
+ .into_iter()
+ .any(|(_, bound_action, b)| {
+ // action names match...
+ bound_action.name() == action.name()
+ && bound_action.namespace() == action.namespace()
+ // and key strokes contain the given key
+ && b.iter()
+ .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+ }),
+ "On {} Failed to find {} with key binding {}",
+ line,
+ action.name(),
+ key
+ );
+ }
+ }
+ }
+
#[gpui::test]
fn test_bundled_settings_and_themes(cx: &mut AppContext) {
cx.platform()
@@ -1846,15 +2042,20 @@ mod tests {
])
.unwrap();
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
- let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
+ let mut settings = SettingsStore::default();
+ settings
+ .set_default_settings(&settings::default_settings(), cx)
+ .unwrap();
+ cx.set_global(settings);
+ theme::init(Assets, cx);
let mut has_default_theme = false;
for theme_name in themes.list(false).map(|meta| meta.name) {
let theme = themes.get(&theme_name).unwrap();
- if theme.meta.name == settings.theme.meta.name {
+ assert_eq!(theme.meta.name, theme_name);
+ if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
has_default_theme = true;
}
- assert_eq!(theme.meta.name, theme_name);
}
assert!(has_default_theme);
}
@@ -1864,25 +2065,26 @@ mod tests {
let mut languages = LanguageRegistry::test();
languages.set_executor(cx.background().clone());
let languages = Arc::new(languages);
- let themes = ThemeRegistry::new((), cx.font_cache().clone());
let http = FakeHttpClient::with_404_response();
let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
- languages::init(languages.clone(), themes, node_runtime);
+ languages::init(languages.clone(), node_runtime);
for name in languages.language_names() {
languages.language_for_name(&name);
}
cx.foreground().run_until_parked();
}
- fn init(cx: &mut TestAppContext) -> Arc<AppState> {
+ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.foreground().forbid_parking();
cx.update(|cx| {
let mut app_state = AppState::test(cx);
let state = Arc::get_mut(&mut app_state).unwrap();
state.initialize_workspace = initialize_workspace;
state.build_window_options = build_window_options;
+ theme::init((), cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
workspace::init(app_state.clone(), cx);
+ language::init(cx);
editor::init(cx);
pane::init(cx);
app_state
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+set -eu
+
+if [[ $# < 1 ]]; then
+ echo "usage: $0 <MAX_SIZE_IN_GB>"
+ exit 1
+fi
+
+max_size_gb=$1
+
+current_size=$(du -s target | cut -f1)
+current_size_gb=$(expr ${current_size} / 1024 / 1024)
+
+echo "target directory size: ${current_size_gb}gb. max size: ${max_size_gb}gb"
+
+if [[ ${current_size_gb} -gt ${max_size_gb} ]]; then
+ echo "clearing target directory"
+ rm -rf target
+fi
@@ -2,8 +2,8 @@
const { execFileSync } = require("child_process");
const { GITHUB_ACCESS_TOKEN } = process.env;
-const PR_REGEX = /pull request #(\d+)/;
-const FIXES_REGEX = /(fixes|closes) (.+[/#]\d+.*)$/im;
+const PR_REGEX = /#\d+/ // Ex: matches on #4241
+const FIXES_REGEX = /(fixes|closes|completes) (.+[/#]\d+.*)$/im;
main();
@@ -15,7 +15,7 @@ async function main() {
{ encoding: "utf8" }
)
.split("\n")
- .filter((t) => t.startsWith("v") && t.endsWith('-pre'));
+ .filter((t) => t.startsWith("v") && t.endsWith("-pre"));
// Print the previous release
console.log(`Changes from ${oldTag} to ${newTag}\n`);
@@ -34,42 +34,16 @@ async function main() {
}
// Get the PRs merged between those two tags.
- const pullRequestNumbers = execFileSync(
- "git",
- [
- "log",
- `${oldTag}..${newTag}`,
- "--oneline",
- "--grep",
- "Merge pull request",
- ],
- { encoding: "utf8" }
- )
- .split("\n")
- .filter((line) => line.length > 0)
- .map((line) => line.match(PR_REGEX)[1]);
+ const pullRequestNumbers = getPullRequestNumbers(oldTag, newTag);
// Get the PRs that were cherry-picked between main and the old tag.
- const existingPullRequestNumbers = new Set(execFileSync(
- "git",
- [
- "log",
- `main..${oldTag}`,
- "--oneline",
- "--grep",
- "Merge pull request",
- ],
- { encoding: "utf8" }
- )
- .split("\n")
- .filter((line) => line.length > 0)
- .map((line) => line.match(PR_REGEX)[1]));
-
+ const existingPullRequestNumbers = new Set(getPullRequestNumbers("main", oldTag));
+
// Filter out those existing PRs from the set of new PRs.
const newPullRequestNumbers = pullRequestNumbers.filter(number => !existingPullRequestNumbers.has(number));
// Fetch the pull requests from the GitHub API.
- console.log("Merged Pull requests:")
+ console.log("Merged Pull requests:");
for (const pullRequestNumber of newPullRequestNumbers) {
const webURL = `https://github.com/zed-industries/zed/pull/${pullRequestNumber}`;
const apiURL = `https://api.github.com/repos/zed-industries/zed/pulls/${pullRequestNumber}`;
@@ -83,13 +57,44 @@ async function main() {
// Print the pull request title and URL.
const pullRequest = await response.json();
console.log("*", pullRequest.title);
- console.log(" URL: ", webURL);
+ console.log(" PR URL: ", webURL);
// If the pull request contains a 'closes' line, print the closed issue.
- const fixesMatch = (pullRequest.body || '').match(FIXES_REGEX);
+ const fixesMatch = (pullRequest.body || "").match(FIXES_REGEX);
if (fixesMatch) {
const fixedIssueURL = fixesMatch[2];
- console.log(" Issue: ", fixedIssueURL);
+ console.log(" Issue URL: ", fixedIssueURL);
}
+
+ let releaseNotes = (pullRequest.body || "").split("Release Notes:")[1];
+
+ if (releaseNotes) {
+ releaseNotes = releaseNotes.trim();
+ console.log(" Release Notes:");
+ console.log(` ${releaseNotes}`);
+ }
+
+ console.log()
}
}
+
+function getPullRequestNumbers(oldTag, newTag) {
+ const pullRequestNumbers = execFileSync(
+ "git",
+ [
+ "log",
+ `${oldTag}..${newTag}`,
+ "--oneline"
+ ],
+ { encoding: "utf8" }
+ )
+ .split("\n")
+ .filter(line => line.length > 0)
+ .map(line => {
+ const match = line.match(/#(\d+)/);
+ return match ? match[1] : null;
+ })
+ .filter(line => line);
+
+ return pullRequestNumbers;
+}